The phaseWatchdog at 10s fired "STUCK phase=session.prompt" on every healthy LLM call longer than 10 seconds. Verified via strace on the running dogfood sf: bytes were actively flowing on the TLS socket (fd 29) to the LLM provider while STUCK was being logged — the session.prompt was never actually stuck, the watchdog was just diagnostic-only and oblivious to stream activity. The noOutputTimeoutMs watchdog (set to 60s for triage in commitd80060fec) is the actual kill mechanism. It is already event-aware: every meaningful subagent event resets the timer via armNoOutputTimer + isMeaningfulSubagentOutputEvent. The 10s STUCK warning was added in commit67e5ac9dbas investigation infrastructure for the sf-mp8e02m1-zpk903 family of bugs, but now it is just noise that makes legitimate 30-200s LLM responses look broken. Keeps the 10s STUCK watchdog for the three setup phases (resourceLoader.reload, createAgentSession, bindExtensions) where 10s of silence is a real hang signal — those phases normally run in sub-second. Also includes: - biome.json: bump $schema URL from 2.4.14 to 2.4.15 to match the current biome CLI (clears the deserialize warning) - scripts/check-test-imports.{,test.}mjs: format + drop a useless regex escape that biome flagged in landed code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
223 lines
6.5 KiB
JavaScript
223 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-test-imports.test.mjs — regression test for the test-import-drift guard.
|
|
*
|
|
* Verifies that check-test-imports.mjs correctly detects the anti-pattern:
|
|
* a test file uses itemized `import { foo }` but references `bar` (not imported).
|
|
*
|
|
* All file paths are absolute to avoid resolution ambiguity when running from
|
|
* different working directories.
|
|
*/
|
|
|
|
import { execSync } from "node:child_process";
|
|
import { writeFileSync, rmdirSync, mkdirSync, existsSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = fileURLToPath(new URL(".", import.meta.url)); // scripts/
|
|
const repoRoot = resolve(__dirname, ".."); // parent = repo root
|
|
const script = resolve(__dirname, "check-test-imports.mjs");
|
|
const tmpDir = resolve(__dirname, "tmp-check-test-imports");
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
/** Run the check script from the repo root, return {exitCode, stdout, stderr}. */
|
|
function run(args) {
|
|
const cmd = ["node", "--", script, ...args].join(" ");
|
|
try {
|
|
const out = execSync(cmd, {
|
|
cwd: repoRoot,
|
|
encoding: "utf-8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
return { exitCode: 0, stdout: out, stderr: "" };
|
|
} catch (err) {
|
|
return {
|
|
exitCode: err.status ?? 1,
|
|
stdout: (err.stdout || "").toString(),
|
|
stderr: (err.stderr || "").toString(),
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Create a temp test file, return absolute path. */
|
|
function makeTempFile(id, content) {
|
|
try {
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
} catch {
|
|
/* */
|
|
}
|
|
const file = resolve(tmpDir, `regression-${id}.test.mjs`);
|
|
writeFileSync(file, content);
|
|
return file;
|
|
}
|
|
|
|
function cleanupTempDir() {
|
|
try {
|
|
rmdirSync(tmpDir);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition, message) {
|
|
if (condition) {
|
|
console.log(` ✅ ${message}`);
|
|
passed++;
|
|
} else {
|
|
console.error(` ❌ ${message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log("check-test-imports regression tests\n");
|
|
|
|
// ── Test 1: Script is loadable and produces valid JSON ───────────────────────
|
|
// Use a real test file that exists under the repo root
|
|
const realFile = resolve(repoRoot, "tests/autonomous-solver.test.mjs");
|
|
if (!existsSync(realFile)) {
|
|
console.error(` ⚠️ Test file not found: ${realFile}`);
|
|
} else {
|
|
const result1 = run([realFile, "--json"]);
|
|
try {
|
|
const parsed = JSON.parse(result1.stdout);
|
|
assert(
|
|
typeof parsed.count === "number",
|
|
`Valid JSON with count: ${parsed.count}`,
|
|
);
|
|
} catch {
|
|
assert(false, `Script should produce valid JSON output`);
|
|
}
|
|
}
|
|
|
|
// ── Test 2: Detects undeclared identifier in anti-pattern file ───────────────
|
|
console.log("\n[2] Detects undeclared identifier in anti-pattern file");
|
|
const file2 = makeTempFile(
|
|
"regression-02",
|
|
`
|
|
// Regression test — generated by check-test-imports.test.mjs
|
|
// DO NOT COMMIT
|
|
import { describe, it, expect } from "vitest";
|
|
import { fn1, fn2, fn3, fn4, fn5, fn6 } from "./fixture.mjs";
|
|
|
|
describe("test", () => {
|
|
it("uses undeclaredFn", () => {
|
|
undeclaredFn();
|
|
});
|
|
});
|
|
`,
|
|
);
|
|
const result2 = run([file2, "--json"]);
|
|
try {
|
|
const parsed = JSON.parse(result2.stdout);
|
|
assert(
|
|
parsed.count > 0,
|
|
`Drift detected in anti-pattern file (count: ${parsed.count})`,
|
|
);
|
|
assert(
|
|
result2.exitCode === 1,
|
|
`Exit code 1 when drift present (got: ${result2.exitCode})`,
|
|
);
|
|
} catch {
|
|
assert(false, `Should not throw when drift is detected`);
|
|
}
|
|
|
|
// ── Test 3: Clean file with namespace import passes ─────────────────────────
|
|
console.log("\n[3] Clean file with namespace import + local vars passes");
|
|
const file3 = makeTempFile(
|
|
"regression-03",
|
|
`
|
|
// Clean: namespace import + only local variables
|
|
// DO NOT COMMIT
|
|
import { describe, it, expect } from "vitest";
|
|
import * as Fixtures from "./fixture.mjs";
|
|
|
|
const myLocalVar = Fixtures.fn1();
|
|
|
|
describe("test", () => {
|
|
it("uses Fixtures methods", () => {
|
|
expect(myLocalVar).toBeDefined();
|
|
});
|
|
});
|
|
`,
|
|
);
|
|
const result3 = run([file3, "--json"]);
|
|
try {
|
|
const parsed = JSON.parse(result3.stdout);
|
|
assert(parsed.count === 0, `Clean file: count ${parsed.count}`);
|
|
} catch {
|
|
assert(false, `Script should not throw on clean file`);
|
|
}
|
|
|
|
// ── Test 4: Local const/let declarations not flagged ───────────────────────
|
|
console.log("\n[4] Local const/let declarations not flagged");
|
|
const file4 = makeTempFile(
|
|
"regression-04",
|
|
`
|
|
// Local variable declarations should not be flagged
|
|
// DO NOT COMMIT
|
|
import { describe, it, expect } from "vitest";
|
|
import { foo } from "./fixture.mjs";
|
|
|
|
const myLocalVar = foo();
|
|
const anotherLocal = foo();
|
|
|
|
describe("test", () => {
|
|
it("uses local variables", () => {
|
|
expect(myLocalVar).toBeDefined();
|
|
});
|
|
});
|
|
`,
|
|
);
|
|
const result4 = run([file4, "--json"]);
|
|
try {
|
|
const parsed = JSON.parse(result4.stdout);
|
|
assert(
|
|
parsed.count === 0,
|
|
`Local variables not flagged (count: ${parsed.count})`,
|
|
);
|
|
} catch {
|
|
assert(false, `Should not throw on clean file`);
|
|
}
|
|
|
|
// ── Test 5: Short lowercase variables not flagged ───────────────────────────
|
|
console.log("\n[5] Short lowercase variables not flagged");
|
|
const file5 = makeTempFile(
|
|
"regression-05",
|
|
`
|
|
// Short lowercase vars like i, fn are common test locals
|
|
// DO NOT COMMIT
|
|
import { describe, it, expect } from "vitest";
|
|
import { fn } from "./fixture.mjs";
|
|
|
|
describe("test", () => {
|
|
it("works with short vars", () => {
|
|
const i = fn();
|
|
const j = i + 1;
|
|
expect(j).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
`,
|
|
);
|
|
const result5 = run([file5, "--json"]);
|
|
try {
|
|
const parsed = JSON.parse(result5.stdout);
|
|
assert(
|
|
parsed.count === 0,
|
|
`Short lowercase vars not flagged (count: ${parsed.count})`,
|
|
);
|
|
} catch {
|
|
assert(false, `Should not throw on clean file`);
|
|
}
|
|
|
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
|
|
console.log(`\n${"-".repeat(50)}`);
|
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
cleanupTempDir();
|
|
process.exit(failed > 0 ? 1 : 0);
|