Skip to content

Commit 8ebbda8

Browse files
committed
fix(gate): production-scan every agents workspace
Discover agent packages from agents/ instead of the allowlist, so a new package whose entry is imported only by its own test fails the dead-export guard.
1 parent f15a34a commit 8ebbda8

2 files changed

Lines changed: 108 additions & 30 deletions

File tree

‎scripts/check-dead-exports.test.ts‎

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { spawnSync } from "node:child_process";
22
import {
33
existsSync,
4+
mkdirSync,
5+
mkdtempSync,
46
readdirSync,
57
readFileSync,
68
rmSync,
79
writeFileSync,
810
} from "node:fs";
11+
import { tmpdir } from "node:os";
912
import { join, relative } from "node:path";
1013
import { describe, expect, test } from "bun:test";
1114

@@ -240,7 +243,10 @@ describe("live knip scan semantics", () => {
240243

241244
test("production agent scan removes test-file consumers from the surface", () => {
242245
const rules = loadAllowlist();
243-
const spawn = knipAgentProductionSpawn(knipBin, agentWorkspaceIds(rules));
246+
const spawn = knipAgentProductionSpawn(
247+
knipBin,
248+
agentWorkspaceIds(join(repoRoot, "agents")),
249+
);
244250
const ran = spawnSync(spawn.command, spawn.args, {
245251
cwd: repoRoot,
246252
encoding: "utf8",
@@ -449,19 +455,24 @@ describe("pinned scan invocation", () => {
449455
expect(() => knipAgentProductionSpawn("/repo/x", [])).toThrow();
450456
});
451457

452-
test("agent workspace ids derive from the allowlist and fail closed", () => {
453-
const rules = parseAllowlistText(
454-
"agents/coder/src/index.ts: agent\n" +
455-
"agents/coder/src/index.ts: agent\n" +
456-
"agents/dispatch/src/index.ts: agent\n" +
457-
"src/auth/codex/constants.ts: CODEX_BASE_URL\n",
458-
);
459-
// Parse dedupes nothing at rule level; the derivation dedupes ids.
460-
expect(agentWorkspaceIds(rules)).toEqual([
461-
"agents/coder",
462-
"agents/dispatch",
463-
]);
464-
expect(() => agentWorkspaceIds(parseAllowlistText("vendor/\n"))).toThrow();
458+
test("agent workspace ids are the agents directory and fail closed", () => {
459+
const agentsRoot = join(repoRoot, "agents");
460+
const fromDisk = readdirSync(agentsRoot, { withFileTypes: true })
461+
.filter(
462+
(entry) =>
463+
entry.isDirectory() &&
464+
existsSync(join(agentsRoot, entry.name, "package.json")),
465+
)
466+
.map((entry) => `agents/${entry.name}`)
467+
.sort();
468+
expect(fromDisk.length).toBeGreaterThan(0);
469+
expect(agentWorkspaceIds(agentsRoot)).toEqual(fromDisk);
470+
const empty = mkdtempSync(join(tmpdir(), "dead-export-agents-"));
471+
try {
472+
expect(() => agentWorkspaceIds(empty)).toThrow();
473+
} finally {
474+
rmSync(empty, { recursive: true, force: true });
475+
}
465476
});
466477

467478
test("parseGuardConfig rejects a missing tsconfig or floor and extra keys", () => {
@@ -543,6 +554,65 @@ describe("violation end to end", () => {
543554
rmSync(probePath, { force: true });
544555
}
545556
}, 240_000);
557+
558+
test("a test-only export in an extra agent workspace fails the guard", () => {
559+
const id = `dead-export-guard-agent-${process.pid}`;
560+
const exportName = `deadExportGuardAgentProbe${process.pid}`;
561+
const dir = join(repoRoot, "agents", id);
562+
mkdirSync(join(dir, "src"), { recursive: true });
563+
writeFileSync(
564+
join(dir, "package.json"),
565+
JSON.stringify({
566+
name: `@corbits/code-agent-${id}`,
567+
private: true,
568+
type: "module",
569+
}),
570+
);
571+
writeFileSync(
572+
join(dir, "src", "index.ts"),
573+
`export const ${exportName} = 1;\n`,
574+
);
575+
writeFileSync(
576+
join(dir, "src", "index.test.ts"),
577+
`import { ${exportName} } from "./index.js";\nvoid ${exportName};\n`,
578+
);
579+
try {
580+
const knipBin = join(repoRoot, "node_modules", ".bin", "knip");
581+
const defaultSpawn = knipDefaultSpawn(knipBin);
582+
const defaultRun = spawnSync(defaultSpawn.command, defaultSpawn.args, {
583+
cwd: repoRoot,
584+
encoding: "utf8",
585+
});
586+
expect(defaultRun.status).toBe(0);
587+
const defaultFindings = parseKnipReport(String(defaultRun.stdout));
588+
expect(
589+
defaultFindings.some(
590+
(finding) =>
591+
finding.file === `agents/${id}/src/index.ts` &&
592+
finding.name === exportName,
593+
),
594+
).toBe(false);
595+
596+
const ran = spawnSync(
597+
process.execPath,
598+
["scripts/check-dead-exports.ts"],
599+
{
600+
cwd: repoRoot,
601+
encoding: "utf8",
602+
env: {
603+
...process.env,
604+
DEAD_EXPORT_GUARD_SCANNED_FILES: String(liveScannedFileCount()),
605+
},
606+
},
607+
);
608+
expect(ran.status).toBe(1);
609+
expect(`${ran.stdout}\n${ran.stderr}`).toContain(
610+
`agents/${id}/src/index.ts: ${exportName}`,
611+
);
612+
} finally {
613+
rmSync(dir, { recursive: true, force: true });
614+
}
615+
}, 240_000);
546616
});
547617

548618
// A re-created barrel would silently resurrect a surface the purge removed.

‎scripts/check-dead-exports.ts‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { spawnSync } from "node:child_process";
2-
import { existsSync, readFileSync, realpathSync } from "node:fs";
2+
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
33
import { dirname, join, sep } from "node:path";
44
import { fileURLToPath } from "node:url";
55

@@ -382,22 +382,30 @@ export function knipAgentProductionSpawn(
382382
]);
383383
}
384384

385-
// The agent workspaces the production scan must cover, derived from the
386-
// allowlist's `agents/<id>/src/index.ts` entries so the scan stays pinned to
387-
// the surface the allowlist governs. Fails closed when the surface names no
385+
// Every agents/ workspace, not the allowlist. A package whose entry exports
386+
// are imported only by its own test is invisible to the default scan (knip
387+
// counts test files there) and would skip this production scan if membership
388+
// came from allowlist entries. Fails closed when the directory names no
388389
// workspace: a silent no-op scan cannot be trusted.
389-
export function agentWorkspaceIds(rules: readonly AllowRule[]): string[] {
390-
const ids = new Set<string>();
391-
for (const rule of rules) {
392-
if (rule.kind === "exact" && rule.file.startsWith("agents/")) {
393-
const id = rule.file.slice("agents/".length).split("/")[0];
394-
if (id !== undefined && id !== "") ids.add(`agents/${id}`);
395-
}
390+
export function agentWorkspaceIds(agentsRoot: string): string[] {
391+
let names: string[];
392+
try {
393+
names = readdirSync(agentsRoot, { withFileTypes: true }).flatMap((entry) =>
394+
entry.isDirectory() &&
395+
!entry.name.startsWith(".") &&
396+
existsSync(join(agentsRoot, entry.name, "package.json"))
397+
? [entry.name]
398+
: [],
399+
);
400+
} catch (err) {
401+
throw new Error(
402+
`agent workspace root not readable (${agentsRoot}): ${(err as Error).message}`,
403+
);
396404
}
397-
const sorted = [...ids].sort();
405+
const sorted = names.map((name) => `agents/${name}`).sort();
398406
if (sorted.length === 0) {
399407
throw new Error(
400-
"dead-export allowlist names no agents/<id>/src/index.ts surface: " +
408+
`agents directory names no workspace (${agentsRoot}): ` +
401409
"the agent-workspace production scan has nothing to cover",
402410
);
403411
}
@@ -494,7 +502,7 @@ function fail(message: string): never {
494502
//
495503
// Both run under the pinned spawn contract; a failing or malformed
496504
// report fails the gate rather than narrowing it silently.
497-
export function runKnipScans(rules: readonly AllowRule[]): DeadExport[] {
505+
export function runKnipScans(): DeadExport[] {
498506
requireKnipConfigs();
499507
const merge = (reports: readonly DeadExport[]): DeadExport[] => {
500508
const seen = new Set<string>();
@@ -534,7 +542,7 @@ export function runKnipScans(rules: readonly AllowRule[]): DeadExport[] {
534542
knipDefaultSpawn(knipBin),
535543
"knip default scan",
536544
);
537-
const agentWorkspaces = agentWorkspaceIds(rules);
545+
const agentWorkspaces = agentWorkspaceIds(join(repoRoot, "agents"));
538546
const agentFindings = spawnAndParse(
539547
knipAgentProductionSpawn(knipBin, agentWorkspaces),
540548
"knip production agent scan",
@@ -561,7 +569,7 @@ function main(): void {
561569
);
562570
}
563571
const rules = parseAllowlistText(allowlistText);
564-
const findings = runKnipScans(rules);
572+
const findings = runKnipScans();
565573
const outcome = evaluateGuard(rules, findings);
566574
let scannedFiles: number;
567575
const injectedCount = parseInjectedScannedFiles(process.env[scannedFilesEnv]);

0 commit comments

Comments
 (0)