Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion tools/agent/quality.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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);
10 changes: 9 additions & 1 deletion tools/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading