From 87efb1fa3ef4f85107fbb89724e7c01b16688e93 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 2 Nov 2026 16:21:21 +0800 Subject: [PATCH] test(e2e): drive the plugin-host smoke test through the public API `@rslint/core` 0.10.0 projects each file's ESTree frame in Go and ships it in the `rslint/pluginLint` request (`EslintPluginLintRequest.files[].frame` is now required). The smoke test hand-built a request with just a path, so the host threw `ERR_INVALID_ARG_TYPE` from `resolvePluginAttachments` and the 0.10.0 toolchain bump failed E2E on the first slice. Drive the pipeline through `Rslint#lintFiles` instead: Go produces the frames, and the project install's `eslint-plugin` entry answers the same reverse request, so the run still proves the packaged plugin path works from a plain project install. The test keeps asserting that both the package entry and `@rslint/core/eslint-plugin` resolve inside one resolution root and that the host entry loads. Verified against `@rslint/core` 0.9.5 and 0.10.0; the VS Code `eslintPlugins` and plugin-host-failure suites pass on 0.10.0. --- .../vscode/e2e/smoke/rslintPluginHost.mjs | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/packages/vscode/e2e/smoke/rslintPluginHost.mjs b/packages/vscode/e2e/smoke/rslintPluginHost.mjs index e622f88b..47124496 100644 --- a/packages/vscode/e2e/smoke/rslintPluginHost.mjs +++ b/packages/vscode/e2e/smoke/rslintPluginHost.mjs @@ -1,14 +1,16 @@ /** * Regression smoke test — "rslint eslint-plugin host path". * - * This is a *verified non-requirement*: with a plain project - * install of `@rslint/core`, a bare-specifier `import('@rslint/core/eslint-plugin')` - * → `createPluginLintHost` → `host.lint()` runs the whole pipeline (worker - * spawned from the sibling `lint-worker.js`, napi parser resolved by walking up - * `node_modules`, an object-form plugin rule producing a diagnostic, clean - * shutdown). Nothing rslint-related ships in the VSIX, so if that ever stops - * being true, this extension's plugin-lint path is dead — hence a regression - * test rather than a one-off manual verification. + * This is a *verified non-requirement*: with a plain project install of + * `@rslint/core`, a bare-specifier `import('@rslint/core/eslint-plugin')` + * resolves to the project's own copy and exports `createPluginLintHost`, and + * the full plugin pipeline runs (Go projects each file's ESTree frame, the + * host's sibling `lint-worker.js` answers the reverse `pluginLint` request, + * the native byte transport resolves by walking up `node_modules`, an + * object-form plugin rule produces a diagnostic, clean shutdown). Nothing + * rslint-related ships in the VSIX, so if that ever stops being true, this + * extension's plugin-lint path is dead — hence a regression test rather than + * a one-off manual verification. * * It needs no VS Code: it is a plain Node script, run by `pnpm test:e2e:smoke` * as well as by the full `pnpm test:e2e`. It deliberately mirrors the @@ -16,11 +18,19 @@ * * 1. `require.resolve('@rslint/core/package.json', { paths: [projectDir] })` * — the project's install, never this repo's; - * 2. `createRequire().resolve('@rslint/core/eslint-plugin')` - * — self-reference resolution, pinning the subpath to that same install; + * 2. `createRequire().resolve(...)` for both the package + * entry and `@rslint/core/eslint-plugin` — self-reference resolution, + * pinning both subpaths to that same install; * 3. the one-resolution-root assertion; - * 4. a dynamic `import()` of the resolved path — the host is ESM and spawns + * 4. dynamic `import()`es of the resolved paths — the host is ESM and spawns * its sibling worker via `import.meta.url`, so it can never be `require`d. + * + * Since `@rslint/core` 0.10.0 Go projects the ESTree frame itself and the host + * only consumes it (`EslintPluginLintRequest.files[].frame` is required), so a + * hand-built `host.lint()` request with just a path is no longer a valid + * request. Driving the pipeline through the public `Rslint` class exercises + * the same reverse `pluginLint` request — real frames included — against the + * entry asserted below, and keeps this test valid across core versions. */ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; @@ -29,9 +39,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const here = path.dirname(fileURLToPath(import.meta.url)); const PROJECT_DIR = path.resolve(here, '../fixtures/rslint'); -const CONFIG_PATH = path.join(PROJECT_DIR, 'rslint.config.mjs'); -const SOURCE_PATH = path.join(PROJECT_DIR, 'src/index.ts'); +const SOURCE_PATTERN = 'src/index.ts'; const RULE_NAME = 'local/no-null'; +const RULE_MESSAGE_ID = 'unexpected'; const nodeRequire = createRequire(import.meta.url); @@ -57,10 +67,14 @@ const main = async () => { ); } const coreDir = path.dirname(corePackageJsonPath); + const projectRequire = createRequire(corePackageJsonPath); - const eslintPluginPath = createRequire(corePackageJsonPath).resolve( - '@rslint/core/eslint-plugin', + const coreEntryPath = projectRequire.resolve('@rslint/core'); + assert.ok( + isInside(coreEntryPath, coreDir), + `one resolution root violated: ${coreEntryPath} is outside ${coreDir}`, ); + const eslintPluginPath = projectRequire.resolve('@rslint/core/eslint-plugin'); assert.ok( isInside(eslintPluginPath, coreDir), `one resolution root violated: ${eslintPluginPath} is outside ${coreDir}`, @@ -70,61 +84,56 @@ const main = async () => { console.log(`[smoke] @rslint/core: ${coreDir}`); console.log(`[smoke] eslint-plugin: ${eslintPluginPath}`); - const logs = []; - const module = await import(pathToFileURL(eslintPluginPath).href); + const coreModule = await import(pathToFileURL(coreEntryPath).href); assert.equal( - typeof module.createPluginLintHost, + typeof coreModule.Rslint, 'function', - `${eslintPluginPath} does not export createPluginLintHost`, + `${coreEntryPath} does not export Rslint`, ); - const host = await module.createPluginLintHost( - [{ configPath: CONFIG_PATH, configDirectory: PROJECT_DIR }], - (record) => { - logs.push(record); - }, + const pluginModule = await import(pathToFileURL(eslintPluginPath).href); + assert.equal( + typeof pluginModule.createPluginLintHost, + 'function', + `${eslintPluginPath} does not export createPluginLintHost`, ); - let result; + let results; + const rslint = new coreModule.Rslint({ cwd: PROJECT_DIR }); try { - result = await host.lint({ - // No `text`: the worker reads the file from disk, the path the CLI uses. - files: [{ path: SOURCE_PATH, configKey: PROJECT_DIR }], - // The rule set Go would have computed from the config for this file. - rules: { [RULE_NAME]: { options: [] } }, - collectFixes: false, - suggestionsMode: 'off', - }); + results = await rslint.lintFiles(SOURCE_PATTERN); } finally { - await host.shutdown(); + await rslint.close(); } - const fileResult = result?.results?.[0]; - assert.ok(fileResult, `no per-file result: ${JSON.stringify(result)}`); - assert.equal( - fileResult.parseError, - undefined, - `the napi parser failed: ${String(fileResult.parseError)}`, - ); + const fileResult = results?.[0]; + assert.ok(fileResult, `no per-file result: ${JSON.stringify(results)}`); + + const messages = fileResult.messages ?? []; + const fatal = messages.filter((message) => message.fatal); assert.deepEqual( - fileResult.ruleErrors ?? [], + fatal, [], - `the plugin rule threw: ${JSON.stringify(fileResult.ruleErrors)}`, + `the linter failed fatally: ${JSON.stringify(fatal)}`, ); - const diagnostics = fileResult.diagnostics ?? []; + // `local/no-null` is an object-form plugin rule: Go cannot run it, so this + // diagnostic proves the project install's plugin host answered the reverse + // request with a Go-projected frame. The VS Code `eslintPlugins` E2E suite + // covers the same path through the extension's own worker. + const diagnostics = messages.filter( + (message) => message.ruleId === RULE_NAME, + ); assert.equal( diagnostics.length, 1, - `expected exactly one ${RULE_NAME} diagnostic, got ${JSON.stringify(diagnostics)}`, + `expected exactly one ${RULE_NAME} diagnostic, got ${JSON.stringify(messages)}`, ); - assert.equal(diagnostics[0].ruleName, RULE_NAME); + assert.equal(diagnostics[0].messageId, RULE_MESSAGE_ID); console.log( - `[smoke] diagnostic: ${diagnostics[0].ruleName} — ${String(diagnostics[0].message)}`, + `[smoke] diagnostic: ${diagnostics[0].ruleId} — ${String(diagnostics[0].message)} (${diagnostics[0].line}:${diagnostics[0].column})`, ); - const errors = logs.filter((record) => record.level === 'error'); - assert.deepEqual(errors, [], `host logged errors: ${JSON.stringify(errors)}`); console.log( '[smoke] OK — project-resolved plugin lint host produced a diagnostic', );