From cb2ef4f76fd7ee5a296efc2ddb77be23eb28d2ad Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 14 Sep 2026 17:33:26 +0800 Subject: [PATCH] fix(vscode): treat any unresolved bare config import as not installed The not-installed classifier proved a bare specifier's package was absent by walking up the physical node_modules from a fixed root (project root, config directory or workspace root). Under pnpm's isolated layout that walk cannot see a plugin's private dependency, so a real missing-subpath error (`dep/missing` where `dep` lives only under the plugin) was reported as "not installed" and the stack went disabled instead of surfacing it. Drop the filesystem lookup. Every Node module-not-found on a bare specifier, subpath included, is now the same disabled state, and the loader's own first line (which names the specifier and the importer) is the one warn line. Recovery is unchanged: the shell's poll already covers disabled states. Relative, absolute and file: specifiers stay real errors. - `classifyMissingDependencyMessage` loses its `resolveFrom` parameter; `missingDependencyCause(code, message)` is the code-gated entry point and `missingDependencyCauseOf(error)` its Error wrapper. - Rslint's config-dependency observer no longer needs `resolveFrom`. - Status wording: " has an import Node cannot resolve". --- packages/vscode/AGENTS.md | 2 +- .../suite-jsconfig/config-transaction.test.ts | 43 ++++--------- .../vscode/src/shared/missingDependency.ts | 61 +++++++------------ packages/vscode/src/shared/notInstalled.ts | 12 ++-- packages/vscode/src/stacks/fmt/index.ts | 1 - .../lint/worker/ConfigTransactionAdapter.ts | 19 ++---- .../vscode/src/stacks/lint/worker/index.ts | 4 -- packages/vscode/src/stacks/test/project.ts | 5 +- packages/vscode/src/stacks/test/types.ts | 6 +- .../vscode/src/stacks/test/worker/index.ts | 4 +- .../tests/shared/missingDependency.test.ts | 58 +++++++----------- .../vscode/tests/shared/notInstalled.test.ts | 2 +- .../vscode/tests/stacks/lint/worker.test.ts | 3 - .../vscode/tests/stacks/test/project.test.ts | 7 ++- 14 files changed, 81 insertions(+), 146 deletions(-) diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index a42f958..f5f74b2 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -27,7 +27,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - **Pre-1.0.0 the extension breaks freely.** No compatibility is owed with earlier unpublished states of this extension — settings, command ids and behavior may change without deprecation paths, and dead compat code for them is removed, not kept. No settings migration exists either — not for earlier states of this extension, and not for the two retired standalone extensions (removed in #15; users re-enter their settings under `rstack.*`). Testing and fixtures track only the latest published releases, pinned exactly and bumped by Renovate; a green E2E run speaks only for those releases. `SUPPORT_MATRIX` floors are the minimum versions the extension accepts: each entry is the lowest release evidence shows works with the current code, and its comment records that evidence. Move a floor only when a change makes older releases stop working, never because a devDependency or fixture moved. Raising a floor needs no transition story; the status names the required version. - **The three tools are treated uniformly by default.** Detection, dependency-change retry, restart semantics, version gating and status reporting follow one shared pattern across the lint/test/fmt stacks; a stack diverges only when its tool forces it, and the divergence is recorded here as a gotcha. When adding behavior to one stack, first ask whether it belongs to all three. This is about behavior, not code — the upstream copies still must not be deduplicated. -- **Not installed is a state, not an error — uniformly.** A folder or project whose dependencies are not installed (no `rstack`, no `@rstest/core`, no `@rslint/core`, a config importing a package that is not there) is the normal state of a fresh clone and of scaffolded templates beside their generator (`create-rstack`'s `template-*`, which declare their own dependencies and are never installed). Every stack reports it the same way: a `disabled` status whose reason keeps the restart command as an explicit fallback, one `warn` line per unresolved episode in the output channel without a stack trace, never a `crashed` status and never a notification. The shell owns one 60-second recursive poll while any controller's raw folder/project/runtime state is disabled, crashed or version-mismatched; it enters the existing serialized queue, forces the same detection notification as a lockfile event, and stops when no failed state remains (ADR 0005). A mid-install retry can read half-written `node_modules` and produce a real syntax error; continuing through failed states makes that transient harmless without a provisional-error heuristic. Real errors remain visible in status and Output, deduplicated by message rather than logged every minute. The words come from one place, `shared/notInstalled.ts` (the `formatVersionMismatch` precedent) — each stack keeps its own status machinery, none its own wording; the restart hint is derived from `stackCommandTitle`, which `tests/extension.test.ts` checks against the manifest. Rstest classifies config-import failures in its worker (`missingDependencyCauseOf`: Node's `code`, a bare package specifier, and for a subpath a walk-up proving the package really is absent) because IPC drops the `code`; Rslint makes the same code-gated decision where its worker still has structured loader results and sends a dedicated verdict to the editor; fmt intercepts only the exact `rs fmt cannot format this workspace:` Error notification and applies the shared message classifier. A typo'd relative import or a missing subpath of an installed package stays a real error in all three. +- **Not installed is a state, not an error — uniformly.** A folder or project whose dependencies are not installed (no `rstack`, no `@rstest/core`, no `@rslint/core`, a config importing a package that is not there) is the normal state of a fresh clone and of scaffolded templates beside their generator (`create-rstack`'s `template-*`, which declare their own dependencies and are never installed). Every stack reports it the same way: a `disabled` status whose reason keeps the restart command as an explicit fallback, one `warn` line per unresolved episode in the output channel without a stack trace, never a `crashed` status and never a notification. The shell owns one 60-second recursive poll while any controller's raw folder/project/runtime state is disabled, crashed or version-mismatched; it enters the existing serialized queue, forces the same detection notification as a lockfile event, and stops when no failed state remains (ADR 0005). A mid-install retry can read half-written `node_modules` and produce a real syntax error; continuing through failed states makes that transient harmless without a provisional-error heuristic. Real errors remain visible in status and Output, deduplicated by message rather than logged every minute. The words come from one place, `shared/notInstalled.ts` (the `formatVersionMismatch` precedent) — each stack keeps its own status machinery, none its own wording; the restart hint is derived from `stackCommandTitle`, which `tests/extension.test.ts` checks against the manifest. Rstest classifies config-import failures in its worker because IPC drops the `code`; Rslint makes the same code-gated decision where its worker still has structured loader results and sends a dedicated verdict to the editor; fmt intercepts only the exact `rs fmt cannot format this workspace:` Error notification and applies the shared message classifier. Any Node module-not-found on a bare specifier is the not-installed state, subpath included; the classifier never touches the filesystem (`shared/missingDependency.ts`, #52). Only relative, absolute and `file:` specifiers stay real errors. - One stack failing to register or crashing must never take another stack (or the shell) down. - The shell always activates; per-folder config detection decides which stacks start, and re-runs on config/lockfile changes without a window reload. The per-stack enable settings are coarse kill switches only. - Reconciles and restarts share one serialized queue (`enqueue`); a reconcile leaves a live stack alone, so the restart path — the commands, and the full pass any relevant settings change triggers — is the only thing that rebuilds one. Do not add a second queue. diff --git a/packages/vscode/e2e/lint/suite-jsconfig/config-transaction.test.ts b/packages/vscode/e2e/lint/suite-jsconfig/config-transaction.test.ts index a17e9dd..b63752d 100644 --- a/packages/vscode/e2e/lint/suite-jsconfig/config-transaction.test.ts +++ b/packages/vscode/e2e/lint/suite-jsconfig/config-transaction.test.ts @@ -255,6 +255,13 @@ function loadRequest(transactionId = 'tx-1'): LoadConfigsRequest { } suite('LSP config discovery transactions', () => { + const rejectsConfigDependencies: ConstructorParameters< + typeof LspConfigTransactionAdapter + >[4] = { + report: () => assert.fail('unexpected missing dependency'), + reportError: () => assert.fail('unexpected config error'), + }; + test('the extension watcher leaves gitignore ownership to Go', () => { assert.match(CONFIG_REFRESH_WATCH_GLOB, /rslint\.config\.js/); assert.match(CONFIG_REFRESH_WATCH_GLOB, /rslint\.config\.mjs/); @@ -282,11 +289,7 @@ suite('LSP config discovery transactions', () => { pool, () => 'fingerprint-1', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); const loaded = await adapter.loadConfigs(loadRequest()); @@ -332,11 +335,7 @@ suite('LSP config discovery transactions', () => { pool, () => 'fingerprint-1', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); await adapter.loadConfigs(loadRequest('tx-abort')); @@ -399,11 +398,7 @@ suite('LSP config discovery transactions', () => { pool, () => 'fingerprint-degraded', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); await adapter.loadConfigs(loadRequest('tx-degraded')); @@ -439,11 +434,7 @@ suite('LSP config discovery transactions', () => { pool, () => 'fingerprint-before-prepare', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); await adapter.loadConfigs(loadRequest('tx-prepare-race')); @@ -468,11 +459,7 @@ suite('LSP config discovery transactions', () => { pool, () => 'fingerprint-1', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); await adapter.loadConfigs(loadRequest('tx-response-lost')); @@ -510,11 +497,7 @@ suite('LSP config discovery transactions', () => { new TestPluginPool(), () => 'fingerprint-1', CONFIG_DISCOVERY_PROTOCOL_VERSION, - { - resolveFrom: (candidate) => candidate.configDirectory, - report: () => assert.fail('unexpected missing dependency'), - reportError: () => assert.fail('unexpected config error'), - }, + rejectsConfigDependencies, ); await assert.rejects(adapter.loadConfigs(loadRequest()), /load failed/); diff --git a/packages/vscode/src/shared/missingDependency.ts b/packages/vscode/src/shared/missingDependency.ts index 675d7e0..1a29828 100644 --- a/packages/vscode/src/shared/missingDependency.ts +++ b/packages/vscode/src/shared/missingDependency.ts @@ -1,29 +1,24 @@ import path from 'node:path'; -import { findPackageJsonUncached } from './packageResolve'; -export function isMissingDependencyCode( +function isMissingDependencyCode( code: unknown, ): code is 'ERR_MODULE_NOT_FOUND' | 'MODULE_NOT_FOUND' { return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND'; } /** - * The classifier behind the "config imports a package that is not installed" - * verdict of the uniform not-installed policy (AGENTS.md). Nothing in it is - * Rstest-specific — it reads Node's loader errors — and lint/fmt will need - * the same verdict where their configs load (#30), which is why it lives in - * `shared/` beside the walk-up it uses rather than in one stack. - * - * Returns the one-line cause when a config evaluation failed on a package - * that is not installed, or `undefined` for a real error. Only a bare - * specifier — a package name, read from the message since CJS carries no - * structured one — counts, and anything unrecognized fails towards the full - * error report. Only the first line comes back: the rest of a CJS message is - * the require stack, and the not-installed state is one warn line without one. + * The "config import cannot be resolved" verdict of the not-installed policy + * (AGENTS.md); in `shared/` because lint/fmt need it too (#30). Returns the + * loader's first line for a bare specifier — the policy's one warn line, with + * the CJS require stack dropped — or `undefined` for a real error. A bare + * specifier failed in the dependency graph, where an install, a lockfile + * event or the poll can change the answer; a relative, absolute or `file:` + * specifier failed inside the user's own source, where nothing external will. + * #52 removed a filesystem walk-up: it could not see a pnpm-isolated private + * dependency. */ export function classifyMissingDependencyMessage( message: string, - resolveFrom: string, ): string | undefined { const [firstLine] = message.split('\n', 1); const specifier = /^Cannot find (?:package|module) '([^']+)'/.exec( @@ -37,35 +32,21 @@ export function classifyMissingDependencyMessage( ) { return undefined; } - // `installed-package/missing-subpath` wears the same bare shape, but the - // package itself is there — installing dependencies cannot fix it either, - // so a subpath is checked against the physical `node_modules` with the - // same uncached walk-up every stack resolves packages with. - const packageName = specifier.startsWith('@') - ? specifier.split('/').slice(0, 2).join('/') - : specifier.split('/', 1)[0]; - if ( - packageName !== specifier && - findPackageJsonUncached(packageName, resolveFrom) !== undefined - ) { - return undefined; - } return firstLine; } -/** - * Error-object entry point used where Node's loader code survives. The code is - * still required there: arbitrary user errors may contain loader-like prose. - * Worker/protocol boundaries that already carry a separately checked code use - * `classifyMissingDependencyMessage` directly because serialization can drop - * custom Error fields. - */ -export function missingDependencyCauseOf( - error: unknown, - resolveFrom: string, +/** Use for a (code, message) pair; bare messages (fmt) use the classifier directly. */ +export function missingDependencyCause( + code: unknown, + message: string, ): string | undefined { + if (!isMissingDependencyCode(code)) return undefined; + return classifyMissingDependencyMessage(message); +} + +/** Use when the caller holds an Error rather than a (code, message) pair. */ +export function missingDependencyCauseOf(error: unknown): string | undefined { if (!(error instanceof Error)) return undefined; const { code } = error as NodeJS.ErrnoException; - if (!isMissingDependencyCode(code)) return undefined; - return classifyMissingDependencyMessage(error.message, resolveFrom); + return missingDependencyCause(code, error.message); } diff --git a/packages/vscode/src/shared/notInstalled.ts b/packages/vscode/src/shared/notInstalled.ts index 7ebd976..96a5827 100644 --- a/packages/vscode/src/shared/notInstalled.ts +++ b/packages/vscode/src/shared/notInstalled.ts @@ -29,20 +29,18 @@ export const formatNotInstalledStatus = ( `${packageName} is not installed (node_modules missing) — install it, ${restartHint(stack)}`; /** - * The `disabled` reason for a config that evaluates but imports a package - * that is not there. `configPath` is workspace-relative: the status has no - * room for more. + * The `disabled` reason for a config with an import Node cannot resolve. + * `configPath` is workspace-relative: the status has no room for more. */ export const formatConfigDependencyMissingStatus = ( stack: StackId, configPath: string, ): string => - `${configPath} imports a package that is not installed — install the project dependencies, ${restartHint(stack)}`; + `${configPath} has an import Node cannot resolve — install the project dependencies, ${restartHint(stack)}`; /** - * The output-channel line for a config that imports a package that is not - * installed. `cause` is the loader's own first line, which names the - * specifier and the importer. + * The output-channel line for the same verdict. `cause` is the loader's own + * first line, which names the specifier and the importer. */ export const formatConfigDependencyMissingLog = ( stack: StackId, diff --git a/packages/vscode/src/stacks/fmt/index.ts b/packages/vscode/src/stacks/fmt/index.ts index 20c7089..b2c9472 100644 --- a/packages/vscode/src/stacks/fmt/index.ts +++ b/packages/vscode/src/stacks/fmt/index.ts @@ -239,7 +239,6 @@ class FmtFolderRuntime { if (configPath !== undefined) { const cause = classifyMissingDependencyMessage( firstLine.replace(/^Error(?: \[[A-Z_]+\])?: /, ''), - this.folderPath, ); if (cause !== undefined) { this.#sessionError.clear(); diff --git a/packages/vscode/src/stacks/lint/worker/ConfigTransactionAdapter.ts b/packages/vscode/src/stacks/lint/worker/ConfigTransactionAdapter.ts index 4576c31..58c0705 100644 --- a/packages/vscode/src/stacks/lint/worker/ConfigTransactionAdapter.ts +++ b/packages/vscode/src/stacks/lint/worker/ConfigTransactionAdapter.ts @@ -2,20 +2,15 @@ import type { ActivateConfigsRequest, ActivateConfigsResponse, ConfigModuleActivationPlan, - ConfigModuleCandidate, ConfigModuleEslintPluginEntry, ConfigModulePluginDescriptor, LoadConfigsRequest, LoadConfigsResponse, } from '@rslint/core/config-loader'; -import { - classifyMissingDependencyMessage, - isMissingDependencyCode, -} from '../../../shared/missingDependency'; +import { missingDependencyCause } from '../../../shared/missingDependency'; import type { ConfigDependencyFailure } from '../../../shared/notInstalled'; interface ConfigDependencyObserver { - resolveFrom(candidate: ConfigModuleCandidate): string; report(failure: ConfigDependencyFailure): void; reportError(message: string): void; } @@ -128,14 +123,10 @@ export class LspConfigTransactionAdapter { results: response.results.map((result, index) => { if (result.status !== 'failed') return result; const candidate = request.candidates[index]; - const cause = - candidate !== undefined && - isMissingDependencyCode(result.error.code) - ? classifyMissingDependencyMessage( - result.error.message, - this.configDependencyObserver.resolveFrom(candidate), - ) - : undefined; + const cause = missingDependencyCause( + result.error.code, + result.error.message, + ); // Scan every failure: a later real error must not be hidden by the // first missing dependency, even though only that result is rewritten. if (cause === undefined || candidate === undefined) { diff --git a/packages/vscode/src/stacks/lint/worker/index.ts b/packages/vscode/src/stacks/lint/worker/index.ts index 772fd22..4d559da 100644 --- a/packages/vscode/src/stacks/lint/worker/index.ts +++ b/packages/vscode/src/stacks/lint/worker/index.ts @@ -234,10 +234,6 @@ export async function runLintWorker( (activation) => fingerprinter.compute(activation), installation.protocolVersion, { - resolveFrom: (candidate) => - candidate.configPath === options.configPath - ? process.cwd() - : candidate.configDirectory, report: (failure) => { configDependencyFailure ??= failure; }, diff --git a/packages/vscode/src/stacks/test/project.ts b/packages/vscode/src/stacks/test/project.ts index bebb5bc..0618685 100644 --- a/packages/vscode/src/stacks/test/project.ts +++ b/packages/vscode/src/stacks/test/project.ts @@ -691,8 +691,9 @@ export class Project implements vscode.Disposable { return `config-deps:${this.sourceUri.toString()}`; } - // The config imports a package that is not installed: the not-installed - // state (AGENTS.md), one step past a missing `@rstest/core` — some install + // The config imports a package Node cannot resolve — a missing package + // or a missing subpath of an installed one: the not-installed state + // (AGENTS.md), one step past a missing `@rstest/core` — some install // *above* the project satisfied the shim, so the config itself is what // failed. A scaffolded template beside its generator is the usual shape. // Latched under this project's key, which `dispose` forgets. diff --git a/packages/vscode/src/stacks/test/types.ts b/packages/vscode/src/stacks/test/types.ts index 660a49a..9725d5a 100644 --- a/packages/vscode/src/stacks/test/types.ts +++ b/packages/vscode/src/stacks/test/types.ts @@ -9,9 +9,9 @@ export type WorkerInitOptions = RstestConfig & { }; /** - * What the worker answers `getNormalizedConfig` with. A config that fails to - * evaluate because a dependency is not installed is a result, not a rejection: - * the IPC channel would strip the error's `code` (see + * What the worker answers `getNormalizedConfig` with. A config that fails + * to evaluate because Node cannot resolve one of its imports is a result, + * not a rejection: the IPC channel would strip the error's `code` (see * `missingDependencyCauseOf`), so the worker classifies it and reports the * loader's own first line as data. */ diff --git a/packages/vscode/src/stacks/test/worker/index.ts b/packages/vscode/src/stacks/test/worker/index.ts index 430f978..fed7381 100644 --- a/packages/vscode/src/stacks/test/worker/index.ts +++ b/packages/vscode/src/stacks/test/worker/index.ts @@ -92,9 +92,7 @@ export class Worker { // Classified here and not in the master: `code` does not survive the // IPC round-trip. Only this unprompted, per-config evaluation gets the // treatment — a run or list the user asked for reports its failure. - // The worker's spawn cwd is the project root (adaptation #5), which is - // where the config's dependencies are installed. - const cause = missingDependencyCauseOf(error, process.cwd()); + const cause = missingDependencyCauseOf(error); if (cause !== undefined) { return { ok: false, message: cause }; } diff --git a/packages/vscode/tests/shared/missingDependency.test.ts b/packages/vscode/tests/shared/missingDependency.test.ts index 7036aac..85fbf37 100644 --- a/packages/vscode/tests/shared/missingDependency.test.ts +++ b/packages/vscode/tests/shared/missingDependency.test.ts @@ -20,11 +20,6 @@ const resolveError = (specifier: string, from: string): unknown => { }; describe('missingDependencyCauseOf', () => { - // The classifier resolves from the worker's cwd — the project root; the - // test directory stands in for it. - const classify = (error: unknown, from = __dirname) => - missingDependencyCauseOf(error, from); - // Same reasoning as `resolveError`: the errors come from Node's own // loaders. const importError = async (specifier: string): Promise => { @@ -38,12 +33,14 @@ describe('missingDependencyCauseOf', () => { it('should name a package an ESM config failed to import', async () => { expect( - classify(await importError('@rstest/definitely-not-installed')), + missingDependencyCauseOf( + await importError('@rstest/definitely-not-installed'), + ), ).toContain("'@rstest/definitely-not-installed'"); }); it('should keep a CJS failure to one line, without the require stack', () => { - const cause = classify( + const cause = missingDependencyCauseOf( resolveError('@rstest/definitely-not-installed', __dirname), ); expect(cause).toContain("'@rstest/definitely-not-installed'"); @@ -58,11 +55,11 @@ describe('missingDependencyCauseOf', () => { // installing dependencies cannot fix it, so it must not be classified as // the not-installed state. The ESM loader reports relative imports as // absolute paths, which the absolute case stands in for. - expect(classify(resolveError('./definitely-missing', __dirname))).toBe( - undefined, - ); expect( - classify( + missingDependencyCauseOf(resolveError('./definitely-missing', __dirname)), + ).toBe(undefined); + expect( + missingDependencyCauseOf( resolveError( path.join(os.tmpdir(), 'definitely-missing.js'), os.tmpdir(), @@ -71,37 +68,36 @@ describe('missingDependencyCauseOf', () => { ).toBe(undefined); }); - it('should tell a missing subpath of an installed package from a missing one', () => { - // `require('installed-package/missing')` fails with the same code and a - // bare-looking specifier, but the package is there — that is a source - // error, not the not-installed state. The same subpath under a package - // that is really absent still is. + it('treats a missing subpath of an installed package as not installed', () => { + // No filesystem lookup since #52; the fixture only proves Node still reports MODULE_NOT_FOUND for a present package's missing subpath. const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rstest-vscode-')); try { const pkgDir = path.join(root, 'node_modules', 'installed-package'); fs.mkdirSync(pkgDir, { recursive: true }); fs.writeFileSync( path.join(pkgDir, 'package.json'), - '{"name":"installed-package","version":"1.0.0","main":"./index.js"}', + '{"name":"installed-package","version":"1.0.0"}', ); - fs.writeFileSync(path.join(pkgDir, 'index.js'), 'module.exports = {};\n'); expect( - classify(resolveError('installed-package/missing', root), root), - ).toBe(undefined); - expect( - classify(resolveError('not-installed-package/missing', root), root), - ).toContain("'not-installed-package/missing'"); + missingDependencyCauseOf( + resolveError('installed-package/missing', root), + ), + ).toContain("'installed-package/missing'"); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); it('should leave every other failure to the full error report', () => { - expect(classify(new SyntaxError('Unexpected token'))).toBe(undefined); - expect(classify(new Error("Cannot find package 'x'"))).toBe(undefined); - expect(classify("Cannot find package 'x'")).toBe(undefined); - expect(classify(undefined)).toBe(undefined); + expect(missingDependencyCauseOf(new SyntaxError('Unexpected token'))).toBe( + undefined, + ); + expect(missingDependencyCauseOf(new Error("Cannot find package 'x'"))).toBe( + undefined, + ); + expect(missingDependencyCauseOf("Cannot find package 'x'")).toBe(undefined); + expect(missingDependencyCauseOf(undefined)).toBe(undefined); }); }); @@ -110,24 +106,16 @@ describe('classifyMissingDependencyMessage', () => { expect( classifyMissingDependencyMessage( "Cannot find package '@scope/missing' imported from /project/config.mjs", - __dirname, ), ).toBe( "Cannot find package '@scope/missing' imported from /project/config.mjs", ); - expect( - classifyMissingDependencyMessage( - "Cannot find module 'missing-package'\nRequire stack:\n- /project/config.cjs", - __dirname, - ), - ).toBe("Cannot find module 'missing-package'"); }); it('rejects non-loader messages even without the Error-code gate', () => { expect( classifyMissingDependencyMessage( "Configuration says Cannot find package 'missing'", - __dirname, ), ).toBe(undefined); }); diff --git a/packages/vscode/tests/shared/notInstalled.test.ts b/packages/vscode/tests/shared/notInstalled.test.ts index 0c81480..bd974c0 100644 --- a/packages/vscode/tests/shared/notInstalled.test.ts +++ b/packages/vscode/tests/shared/notInstalled.test.ts @@ -29,7 +29,7 @@ describe('not-installed wording', () => { 'templates/app/rstack.config.ts', ), ).toBe( - 'templates/app/rstack.config.ts imports a package that is not installed — install the project dependencies, then run "Rstack: Restart Rstest" if this status stays', + 'templates/app/rstack.config.ts has an import Node cannot resolve — install the project dependencies, then run "Rstack: Restart Rstest" if this status stays', ); }); diff --git a/packages/vscode/tests/stacks/lint/worker.test.ts b/packages/vscode/tests/stacks/lint/worker.test.ts index 228ab00..917dde1 100644 --- a/packages/vscode/tests/stacks/lint/worker.test.ts +++ b/packages/vscode/tests/stacks/lint/worker.test.ts @@ -261,7 +261,6 @@ describe('lint worker config dependency classification', () => { let missing: { configPath: string; cause: string } | undefined; let configError: string | undefined; const observer = { - resolveFrom: () => '/project', report: (failure: NonNullable) => { missing = failure; }, @@ -392,7 +391,6 @@ describe('lint worker config dependency classification', () => { (_plan: ConfigModuleActivationPlan) => 'fingerprint', 3, { - resolveFrom: (candidate) => candidate.configDirectory, report: (failure) => failures.push(failure), reportError: () => { throw new Error('unexpected config error'); @@ -473,7 +471,6 @@ describe('lint worker config dependency classification', () => { () => 'fingerprint', 3, { - resolveFrom: () => '/project', report: (failure) => failures.push(failure), reportError: (message) => expect(message).toBe("Cannot find package './relative.js'"), diff --git a/packages/vscode/tests/stacks/test/project.test.ts b/packages/vscode/tests/stacks/test/project.test.ts index a03c40e..5c5ca6c 100644 --- a/packages/vscode/tests/stacks/test/project.test.ts +++ b/packages/vscode/tests/stacks/test/project.test.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; +import { formatConfigDependencyMissingStatus } from '../../../src/shared/notInstalled'; import { ReportedRstestResolutionError } from '../../../src/stacks/test/coreResolution'; import { logger } from '../../../src/stacks/test/logger'; import { status } from '../../../src/stacks/test/status'; @@ -557,8 +558,10 @@ describe('Project config/cwd/package-resolution decoupling', () => { expect(reported).toEqual([ { kind: 'disabled', - reason: - 'templates/app/rstack.config.ts imports a package that is not installed — install the project dependencies, then run "Rstack: Restart Rstest" if this status stays', + reason: formatConfigDependencyMissingStatus( + 'rstest', + 'templates/app/rstack.config.ts', + ), }, ]);