Skip to content

Commit 750d775

Browse files
committed
chore(visibility): surface previously-silent failure paths via CODEBASE_DEBUG=1
Three places swallowed errors with empty catches, plus a typed mock factory for tests so the ToolContext interface can't drift past them unnoticed. - agent.ts diagnostics after edit: was silent, now stderr under CODEBASE_DEBUG=1. Buried a real case where a checker was hanging on a generated file and edits looked sluggish. - hooks/manager.ts async hook failures: same shape — user-configured hooks that throw on every dispatch are otherwise invisible. - glue/client.ts parseGlueRef: when GLUE_FAST_MODEL or GLUE_SMART_MODEL references a model id that can't be resolved, we fall back to the parent. Useful behaviour, but the silent fallback meant typos went unnoticed and 'smart' calls were quietly using the wrong tier. - New makeMockToolContext factory in tools/__test__/ uses real stores (cheap, in-memory) instead of {} as any. Tests now fail-closed when a new required field is added to ToolContext.
1 parent cd0abbd commit 750d775

5 files changed

Lines changed: 66 additions & 23 deletions

File tree

src/agent/agent.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,16 @@ export function createAgent(opts: CreateAgentOptions = {}): AgentBundle {
217217
timestamp: Date.now(),
218218
});
219219
})
220-
.catch(() => {
221-
// Diagnostics failures are non-fatal — surface nothing.
220+
.catch((err) => {
221+
// Diagnostics failures are non-fatal — surface only when the
222+
// user opted into debug. Silent before; that buried a real
223+
// bug where a checker hung and the user thought the tool
224+
// itself was slow.
225+
if (process.env.CODEBASE_DEBUG === "1") {
226+
process.stderr.write(
227+
`[diagnostics] ${absPath}: ${err instanceof Error ? err.message : String(err)}\n`,
228+
);
229+
}
222230
});
223231
}
224232
return undefined;

src/glue/client.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,15 @@ export function parseGlueRef(ref: string, fallback: Model<string>): Model<string
9292

9393
const [maybeProvider, maybeId] = trimmed.includes(":") ? trimmed.split(":", 2) : [fallback.provider, trimmed];
9494
const found = getModel(maybeProvider as KnownProvider, maybeId as never);
95-
return (found as Model<string> | undefined) ?? fallback;
95+
if (found) return found as Model<string>;
96+
// Lookup miss — typo or unknown model. Falling back is correct, but
97+
// silent fallback meant users who set GLUE_SMART_MODEL=foo-typo
98+
// thought they were getting their smart model and were actually
99+
// getting the parent. Make it visible under CODEBASE_DEBUG=1.
100+
if (process.env.CODEBASE_DEBUG === "1") {
101+
process.stderr.write(
102+
`[glue] no model matched "${trimmed}" — falling back to ${fallback.provider}/${fallback.id}\n`,
103+
);
104+
}
105+
return fallback;
96106
}

src/hooks/manager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,16 @@ export class HookManager {
6565

6666
for (const config of matching) {
6767
if (config.async) {
68-
runHook(config, context, signal).catch(() => {
69-
// fire-and-forget; nothing to do on failure
68+
runHook(config, context, signal).catch((err) => {
69+
// async hooks fire-and-forget by design, but a hook that's
70+
// silently throwing on every event will mystify the user
71+
// who configured it. Surface to stderr under CODEBASE_DEBUG
72+
// so it's visible when someone is actually looking.
73+
if (process.env.CODEBASE_DEBUG === "1") {
74+
process.stderr.write(
75+
`[hook async event=${event}] ${err instanceof Error ? err.message : String(err)}\n`,
76+
);
77+
}
7078
});
7179
ranCount++;
7280
continue;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Typed factory for a ToolContext suitable for unit tests. Uses real
3+
* instances of the in-memory stores (they're cheap to construct) so
4+
* tests catch interface-shape drift the moment a field is added to
5+
* ToolContext — previously this was `{} as any` for every field, which
6+
* meant adding a new required member to ToolContext compiled green
7+
* while every test was secretly missing it.
8+
*/
9+
10+
import { MemoryStore } from "../../memory/store.js";
11+
import { PlanModeStore } from "../../plan/store.js";
12+
import { UserQueryStore } from "../../user-queries/store.js";
13+
import { FileStateCache } from "../file-state-cache.js";
14+
import { TaskStore } from "../task-store.js";
15+
import type { ToolContext } from "../types.js";
16+
17+
export function makeMockToolContext(cwd: string): ToolContext {
18+
return {
19+
cwd,
20+
fileStateCache: new FileStateCache(),
21+
tasks: new TaskStore(),
22+
userQueries: new UserQueryStore(),
23+
planMode: new PlanModeStore(),
24+
memory: new MemoryStore({ cwd }),
25+
// spawnSubagent is the only field a test can't supply a real
26+
// implementation for (it depends on the live agent factory).
27+
// We throw to make the boundary explicit: any test that calls a
28+
// subagent-spawning tool needs to provide its own stub.
29+
spawnSubagent: () => {
30+
throw new Error("mock-tool-context: spawnSubagent not stubbed — provide one in the test if you need it");
31+
},
32+
};
33+
}

src/tools/config.test.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,10 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
import { makeMockToolContext } from "./__test__/mock-tool-context.js";
56
import { createConfig } from "./config.js";
6-
import type { ToolContext } from "./types.js";
77

8-
function makeCtx(cwd: string): ToolContext {
9-
return {
10-
cwd,
11-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub for tool context
12-
fileStateCache: {} as any,
13-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
14-
tasks: {} as any,
15-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
16-
userQueries: {} as any,
17-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
18-
planMode: {} as any,
19-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
20-
memory: {} as any,
21-
// biome-ignore lint/suspicious/noExplicitAny: minimal stub
22-
spawnSubagent: (() => {}) as any,
23-
};
24-
}
8+
const makeCtx = makeMockToolContext;
259

2610
describe("config tool", () => {
2711
let cwd: string;

0 commit comments

Comments
 (0)