diff --git a/tools/agent/quality.test.ts b/tools/agent/quality.test.ts index d29fea8..06de0c5 100644 --- a/tools/agent/quality.test.ts +++ b/tools/agent/quality.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; -import { join } from "node:path"; +import { existsSync } from "node:fs"; +import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { ESLint } from "../../apps/api/node_modules/eslint"; @@ -37,3 +38,78 @@ test("tooling lint rejects unsafe shortcuts and suppression while accepting type expect(valid?.errorCount).toBe(0); expect(valid?.warningCount).toBe(0); }, 30000); + +/* + * calculateConfigForFile is typed as `any`, so narrow it by hand rather than + * asserting: an unreadable shape yields no resolvers and fails the test. + */ +function declaredResolvers(config: unknown): string[] { + if ( + typeof config !== "object" || + config === null || + !("settings" in config) + ) { + return []; + } + + const { settings } = config; + + if ( + typeof settings !== "object" || + settings === null || + !("import/resolver" in settings) + ) { + return []; + } + + const resolver = settings["import/resolver"]; + + if (typeof resolver !== "object" || resolver === null) { + return []; + } + + return Object.keys(resolver); +} + +test("tooling lint pins its import resolver inside the repo", async () => { + /* + * tools/ has no node_modules. A resolver named only by string ("node") is + * looked up from the linted file's directory, finds no dependency root, and + * escapes to Bun's global install cache, whose version no lockfile here + * pins. Every import rule then reports "Resolve error" instead of running. + * Assert the declared resolver is an absolute path inside the repo, which + * holds regardless of what the ambient cache happens to contain. + */ + const lint = new ESLint({ + cwd: join(ROOT, "tools"), + overrideConfig: { + languageOptions: { + parserOptions: { disallowAutomaticSingleRunInference: true }, + }, + }, + }); + const probePath = join(ROOT, "tools/agent/inventory.ts"); + const config: unknown = await lint.calculateConfigForFile(probePath); + const resolvers = declaredResolvers(config); + + expect(resolvers.length).toBeGreaterThan(0); + + for (const resolver of resolvers) { + expect(isAbsolute(resolver)).toBe(true); + expect(resolver.startsWith(ROOT)).toBe(true); + expect(existsSync(resolver)).toBe(true); + } + + const [result] = await lint.lintText( + 'import { verify } from "./../agent/verification";\n\nexport const probe = verify;\n', + { filePath: probePath } + ); + const messages = result?.messages ?? []; + + expect( + messages.some((message) => message.message.includes("Resolve error")) + ).toBe(false); + expect(messages.map((message) => message.ruleId)).toContain( + "import/no-useless-path-segments" + ); +}, 30000); diff --git a/tools/eslint.config.mjs b/tools/eslint.config.mjs index 0f24038..fe070a1 100644 --- a/tools/eslint.config.mjs +++ b/tools/eslint.config.mjs @@ -17,7 +17,15 @@ export default [ { plugins: scriptConfig.plugins, linterOptions: { ...scriptConfig.linterOptions, noInlineConfig: true }, - settings: scriptConfig.settings ?? {}, + settings: { + ...(scriptConfig.settings ?? {}), + // tools/ has no node_modules, so eslint-plugin-import would resolve its + // named "node" resolver against Bun's global cache instead of the API's + // pinned copy. Point at the installed file so the lockfile decides. + "import/resolver": { + [`${apiRoot}node_modules/eslint-import-resolver-node/index.js`]: {}, + }, + }, files: ["agent/**/*.ts", "agent-evals/**/*.ts"], languageOptions: { ...scriptConfig.languageOptions,