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
14 changes: 3 additions & 11 deletions scripts/check-dead-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,12 @@ import {
const repoRoot = join(import.meta.dir, "..");

// A probe dead export in one of the scoped files must fail the guard: the
// exact-name exemptions cover only the five deferred-cleanup flags, never
// the whole module.
// exact-name exemptions cover only the remaining deferred-cleanup flags
// (the usage-formatter flags were removed with their exports by the CL-6815
// trim), never the whole module.
describe("scoped exemptions", () => {
test("the real allowlist covers the named flags but not a sibling probe", () => {
const rules = loadAllowlist();
expect(
isAllowlisted(rules, "src/auth/codex/usage.ts", "fetchCodexUsage"),
).toBe(true);
expect(
isAllowlisted(rules, "src/auth/codex/usage.ts", "fetchCodexModels"),
).toBe(true);
expect(isAllowlisted(rules, "src/auth/xai/usage.ts", "fetchXaiUsage")).toBe(
true,
);
expect(
isAllowlisted(
rules,
Expand Down
11 changes: 8 additions & 3 deletions scripts/dead-export-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ vendor/
# flags so a new dead export in these files fails the guard instead of
# hiding under a whole-file exemption. Remove each entry with the export it
# names when the owning cleanup lands.
src/auth/codex/usage.ts: fetchCodexUsage
src/auth/codex/usage.ts: fetchCodexModels
src/auth/xai/usage.ts: fetchXaiUsage
src/auth/codex/constants.ts: CODEX_REFRESH_SKEW_MS
src/auth/codex/constants.ts: CODEX_HEADLESS_REFRESH_INTERVAL_MS

# Plugin fixture entry point. Loaded by file path from the fixture's plugin
# manifest by the plugin-registration tests, so it has no static importers.
tests/fixtures/plugins/implement-feature/src/index.ts

# Eval completion-harness task fixtures (CL-7932 lane). Loaded by file path
# from each task's script.json by the harness runner, so they have no static
# importers.
evals/completion/tasks/decline-refactor/fixture/notes.ts: addNote
evals/completion/tasks/stall-read/fixture/data.ts: data
evals/completion/tasks/sum-fix/fixture/sum.ts: total
evals/completion/tasks/version-endpoint/fixture/service.ts: handleRequest

# ts-prune parser false positives: bare tokens on `as const satisfies ...`
# lines inside live exports. None of these is an export declaration.
src/provider/reasoning-effort.ts: satisfies
Expand Down
122 changes: 122 additions & 0 deletions src/agent/chat-event-subscribers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, test } from "bun:test";
import {
CHAT_TASKS_CHANGED_EVENT,
CHAT_TOOLS_ACTIVATE_EVENT,
} from "./director.js";
import { handleChatDirectorEvent } from "./chat-event-subscribers.js";
import type { Task } from "./tasks.js";

function makeLog() {
const calls: { message: string; fields?: Record<string, unknown> }[] = [];
return {
calls,
log: (message: string, fields?: Record<string, unknown>): void => {
calls.push(fields !== undefined ? { message, fields } : { message });
},
};
}

describe("handleChatDirectorEvent", () => {
test("dispatches a valid tasks-changed payload without logging", () => {
const seen: Task[][] = [];
const log = makeLog();
const handled = handleChatDirectorEvent(
{
type: CHAT_TASKS_CHANGED_EVENT,
data: { tasks: [{ id: "t1", title: "work", status: "doing" }] },
},
{
onTasksChanged: (tasks) => seen.push(tasks),
onToolsActivate: () => {
throw new Error("unexpected tools-activate dispatch");
},
},
log.log,
);
expect(handled).toBe(true);
expect(seen).toEqual([[{ id: "t1", title: "work", status: "doing" }]]);
expect(log.calls).toEqual([]);
});

test("dispatches a valid tools-activate payload without logging", () => {
const seen: string[][] = [];
const log = makeLog();
const handled = handleChatDirectorEvent(
{ type: CHAT_TOOLS_ACTIVATE_EVENT, data: { names: ["lsp"] } },
{
onTasksChanged: () => {
throw new Error("unexpected tasks-changed dispatch");
},
onToolsActivate: (names) => seen.push([...names]),
},
log.log,
);
expect(handled).toBe(true);
expect(seen).toEqual([["lsp"]]);
expect(log.calls).toEqual([]);
});

test("drops an invalid tasks payload with a debug log naming the failure", () => {
let dispatched = false;
const log = makeLog();
const handled = handleChatDirectorEvent(
{ type: CHAT_TASKS_CHANGED_EVENT, data: { tasks: "not-a-list" } },
{
onTasksChanged: () => {
dispatched = true;
},
onToolsActivate: () => {
dispatched = true;
},
},
log.log,
);
expect(handled).toBe(true);
expect(dispatched).toBe(false);
expect(log.calls).toHaveLength(1);
expect(log.calls[0]?.message).toMatch(/tasks-changed/);
expect(typeof log.calls[0]?.fields?.["error"]).toBe("string");
});

test("drops an invalid tools payload with a debug log naming the failure", () => {
let dispatched = false;
const log = makeLog();
const handled = handleChatDirectorEvent(
{ type: CHAT_TOOLS_ACTIVATE_EVENT, data: { names: [42] } },
{
onTasksChanged: () => {
dispatched = true;
},
onToolsActivate: () => {
dispatched = true;
},
},
log.log,
);
expect(handled).toBe(true);
expect(dispatched).toBe(false);
expect(log.calls).toHaveLength(1);
expect(log.calls[0]?.message).toMatch(/tools-activate/);
expect(typeof log.calls[0]?.fields?.["error"]).toBe("string");
});

test("ignores unrelated events without logging or dispatching", () => {
let dispatched = false;
const log = makeLog();
const handled = handleChatDirectorEvent(
{ type: "inference.done", data: {} },
{
onTasksChanged: () => {
dispatched = true;
},
onToolsActivate: () => {
dispatched = true;
},
},
log.log,
);
expect(handled).toBe(false);
expect(dispatched).toBe(false);
expect(log.calls).toEqual([]);
});
});
62 changes: 62 additions & 0 deletions src/agent/chat-event-subscribers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Shared subscriber for the chat-director reactor events. The TUI and exec
* stream sinks both listen for the task-list and tool-activation events the
* chat director emits in place of the former host closures; the parse,
* validation, and invalid-payload handling live here so the two sinks cannot
* drift apart.
*/

import { type } from "arktype";
import {
CHAT_TASKS_CHANGED_EVENT,
CHAT_TOOLS_ACTIVATE_EVENT,
ChatTasksChangedDataSchema,
ChatToolsActivateDataSchema,
} from "./director.js";
import type { Task } from "./tasks.js";

export interface ChatDirectorEventHandlers {
onTasksChanged: (tasks: Task[]) => void;
onToolsActivate: (names: string[]) => void;
}

export type ChatDirectorEventDebugLog = (
message: string,
fields?: Record<string, unknown>,
) => void;

/**
* Dispatch one stream event to the chat-director handlers. Returns true when
* the event is a chat-director event (valid or not) so sinks can fall through
* to their own handling otherwise. Invalid payloads are dropped after a
* debug-level log naming the failure — never silently.
*/
export function handleChatDirectorEvent(
event: { type: string; data: unknown },
handlers: ChatDirectorEventHandlers,
logDebug: ChatDirectorEventDebugLog,
): boolean {
if (event.type === CHAT_TASKS_CHANGED_EVENT) {
const parsed = ChatTasksChangedDataSchema(event.data);
if (parsed instanceof type.errors) {
logDebug("chat tasks-changed event dropped invalid payload: {error}", {
error: parsed.summary,
});
return true;
}
handlers.onTasksChanged(parsed.tasks);
return true;
}
if (event.type === CHAT_TOOLS_ACTIVATE_EVENT) {
const parsed = ChatToolsActivateDataSchema(event.data);
if (parsed instanceof type.errors) {
logDebug("chat tools-activate event dropped invalid payload: {error}", {
error: parsed.summary,
});
return true;
}
handlers.onToolsActivate(parsed.names);
return true;
}
return false;
}
Loading
Loading