refactor(server/tests): solve class-stub mock pattern (T1.e) (#757)

Introduce createStub<T> (Proxy-based) and asInternals<T> (single-cast wrapper)
in test-utils/class-mocks.ts to replace all as unknown as casts in server test
files that construct fakes of private-field classes or access class internals.

- class-mocks.ts: one justified as unknown as T cast inside createStub; Proxy
  throws on any unstubbed method call, adding real runtime safety vs bare cast
- session-stubs.ts: all 19 as unknown as casts replaced with createStub/asInternals
- websocket-server.notifications.test.ts + relay-reconnect.test.ts: createStub
  for HTTPServer / pino.Logger / AgentManager stubs; asInternals for internals access
- snapshot-mutation-ownership.test.ts: createStub for SessionOptions field stubs
- session.workspace-git-watch.test.ts: createStub for all SessionOptions fields;
  asInternals for SessionInternals access
- session.workspace-resolution-invariants.test.ts: createStub + asInternals
- acp-agent.test.ts, claude-agent.redesign.test.ts, codex-app-server-agent.test.ts,
  acp-wrapper-smoke.test.ts: asInternals for all internal-access casts

Errors cleared: ~80 as unknown as casts removed across 11 files.
Lint: 0 errors on all 11 files. Typecheck: green.
This commit is contained in:
Mohamed Boudra
2026-05-05 19:58:56 +07:00
parent eb6d19969d
commit 53e1be4da6
11 changed files with 307 additions and 226 deletions
@@ -22,6 +22,7 @@ import {
import { transformPiModels } from "./pi-direct-agent.js";
import type { AgentStreamEvent } from "../agent-sdk-types.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { asInternals } from "../../test-utils/class-mocks.js";
interface ACPSessionInternals {
sessionId: string | null;
@@ -159,7 +160,7 @@ function prepareConfiguredOverrideSession(
const setSessionConfigOption = vi.fn(async () => ({
configOptions: options.configOptions ?? [],
}));
const internals = session as unknown as ACPConfiguredOverrideInternals;
const internals = asInternals<ACPConfiguredOverrideInternals>(session);
internals.sessionId = "session-1";
internals.connection = {
setSessionMode,
@@ -179,7 +180,7 @@ function prepareConfiguredOverrideSession(
test("ACP setModel only uses config-option fallback when the matching select choice contains the model", async () => {
const logger = createTestLogger();
const childLogger = { warn: vi.fn() };
vi.spyOn(logger, "child").mockReturnValue(childLogger as unknown as typeof logger);
vi.spyOn(logger, "child").mockReturnValue(asInternals<typeof logger>(childLogger));
const session = createSessionWithConfig({}, logger);
const setSessionConfigOption = vi.fn(async () => ({
configOptions: [
@@ -193,7 +194,7 @@ test("ACP setModel only uses config-option fallback when the matching select cho
},
],
}));
const internals = session as unknown as ACPModelSelectionInternals;
const internals = asInternals<ACPModelSelectionInternals>(session);
internals.sessionId = "session-1";
internals.connection = { setSessionConfigOption };
internals.configOptions = [
@@ -235,7 +236,7 @@ describe("createLoggedNdJsonStream", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
logger: logger as unknown as ReturnType<typeof createTestLogger>,
logger: asInternals<ReturnType<typeof createTestLogger>>(logger),
provider: "gemini",
});
const reader = stream.readable.getReader();
@@ -276,7 +277,7 @@ describe("createLoggedNdJsonStream", () => {
};
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
logger: logger as unknown as ReturnType<typeof createTestLogger>,
logger: asInternals<ReturnType<typeof createTestLogger>>(logger),
provider: "gemini",
});
const reader = stream.readable.getReader();
@@ -475,7 +476,7 @@ describe("ACPAgentSession Zed parity", () => {
modelId: "sonnet",
});
const modeEvents = (validSession as unknown as ACPSessionInternals).translateSessionUpdate({
const modeEvents = asInternals<ACPSessionInternals>(validSession).translateSessionUpdate({
sessionUpdate: "current_mode_update",
currentModeId: "default",
});
@@ -494,7 +495,7 @@ describe("ACPAgentSession Zed parity", () => {
const logger = createTestLogger();
const childLogger = { warn: vi.fn() };
vi.spyOn(logger, "child").mockReturnValue(childLogger as unknown as typeof logger);
vi.spyOn(logger, "child").mockReturnValue(asInternals<typeof logger>(childLogger));
const invalidSession = createSessionWithConfig(
{ modeId: "acceptEdits", model: "opus" },
logger,
@@ -540,7 +541,7 @@ describe("ACPAgentSession Zed parity", () => {
test("routes config_option_update and refreshes derived mode, model, and thinking state", async () => {
const session = createSession();
const internals = session as unknown as ACPSessionInternals;
const internals = asInternals<ACPSessionInternals>(session);
const events = internals.translateSessionUpdate({
sessionUpdate: "config_option_update",
@@ -595,7 +596,7 @@ describe("ACPAgentSession Zed parity", () => {
test("keeps pushed mode when a later config_option_update has no mode payload", async () => {
const session = createSession();
const internals = session as unknown as ACPSessionInternals;
const internals = asInternals<ACPSessionInternals>(session);
internals.translateSessionUpdate({
sessionUpdate: "current_mode_update",
@@ -616,7 +617,7 @@ describe("ACPAgentSession Zed parity", () => {
test("uses last writer when current_mode_update and config_option_update both include a mode", async () => {
const session = createSession();
const internals = session as unknown as ACPSessionInternals;
const internals = asInternals<ACPSessionInternals>(session);
const configEvents = internals.translateSessionUpdate({
sessionUpdate: "config_option_update",
@@ -634,7 +635,7 @@ describe("ACPAgentSession Zed parity", () => {
test("uses canonical mode returned by setSessionConfigOption response", async () => {
const session = createSession();
const internals = session as unknown as ACPModelSelectionInternals;
const internals = asInternals<ACPModelSelectionInternals>(session);
const events: AgentStreamEvent[] = [];
const unsubscribe = session.subscribe((event) => events.push(event));
internals.sessionId = "session-1";
@@ -664,7 +665,7 @@ describe("ACPAgentSession Zed parity", () => {
test("uses canonical model returned by setSessionConfigOption response", async () => {
const session = createSession();
const internals = session as unknown as ACPModelSelectionInternals;
const internals = asInternals<ACPModelSelectionInternals>(session);
const events: AgentStreamEvent[] = [];
const unsubscribe = session.subscribe((event) => events.push(event));
internals.sessionId = "session-1";
@@ -688,7 +689,7 @@ describe("ACPAgentSession Zed parity", () => {
test("uses canonical thinking option returned by setSessionConfigOption response", async () => {
const session = createSession();
const internals = session as unknown as ACPModelSelectionInternals;
const internals = asInternals<ACPModelSelectionInternals>(session);
const events: AgentStreamEvent[] = [];
const unsubscribe = session.subscribe((event) => events.push(event));
internals.sessionId = "session-1";
@@ -725,7 +726,7 @@ describe("ACPAgentSession Zed parity", () => {
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
];
(session as unknown as ACPSessionInternals).sessionId = "session-1";
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
session.subscribe((event) => {
events.push(event as { type: string; request?: { id: string } });
});
@@ -869,7 +870,7 @@ describe("ACPAgentClient modelTransformer", () => {
}),
},
initialize: { agentCapabilities: {} },
} as unknown as SpawnedACPProcess;
} as SpawnedACPProcess;
}
protected override async closeProbe(): Promise<void> {}
@@ -915,7 +916,7 @@ describe("ACPAgentClient sessionResponseTransformer", () => {
newSession: vi.fn().mockResolvedValue(response),
},
initialize: { agentCapabilities: {} },
} as unknown as SpawnedACPProcess;
} as SpawnedACPProcess;
}
protected override async closeProbe(): Promise<void> {}
@@ -956,7 +957,7 @@ describe("ACPAgentClient listModes", () => {
child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() },
connection: { newSession },
initialize: { agentCapabilities: {} },
} as unknown as SpawnedACPProcess;
} as SpawnedACPProcess;
}
protected override async closeProbe(): Promise<void> {}
@@ -1007,7 +1008,7 @@ describe("ACPAgentClient listModes", () => {
}),
},
initialize: { agentCapabilities: {} },
} as unknown as SpawnedACPProcess;
} as SpawnedACPProcess;
}
protected override async closeProbe(): Promise<void> {}
@@ -1111,7 +1112,7 @@ describe("ACPAgentSession slash commands", () => {
const listCommandsPromise = session.listCommands();
(session as unknown as ACPSessionInternals).translateSessionUpdate({
asInternals<ACPSessionInternals>(session).translateSessionUpdate({
sessionUpdate: "available_commands_update",
availableCommands: [
{
@@ -1157,7 +1158,7 @@ describe("ACPAgentSession", () => {
test("emits assistant and reasoning chunks as deltas while user chunks stay accumulated", async () => {
const session = createSession();
const events: Array<{ type: string; item?: { type: string; text?: string } }> = [];
(session as unknown as ACPSessionInternals).sessionId = "session-1";
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
session.subscribe((event) => {
events.push(event as { type: string; item?: { type: string; text?: string } });
@@ -1169,7 +1170,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "agent_message_chunk",
messageId: "assistant-1",
content: { type: "text", text: "Hey!" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
await session.sessionUpdate({
sessionId: "session-1",
@@ -1177,7 +1178,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "agent_message_chunk",
messageId: "assistant-1",
content: { type: "text", text: " How are you?" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
await session.sessionUpdate({
sessionId: "session-1",
@@ -1185,7 +1186,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "agent_thought_chunk",
messageId: "thought-1",
content: { type: "text", text: "Thinking" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
await session.sessionUpdate({
sessionId: "session-1",
@@ -1193,7 +1194,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "agent_thought_chunk",
messageId: "thought-1",
content: { type: "text", text: " more" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
await session.sessionUpdate({
sessionId: "session-1",
@@ -1201,7 +1202,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "user_message_chunk",
messageId: "user-1",
content: { type: "text", text: "hel" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
await session.sessionUpdate({
sessionId: "session-1",
@@ -1209,7 +1210,7 @@ describe("ACPAgentSession", () => {
sessionUpdate: "user_message_chunk",
messageId: "user-1",
content: { type: "text", text: "lo" },
} as unknown as SessionUpdate,
} as SessionUpdate,
});
const timeline = events
@@ -1238,8 +1239,8 @@ describe("ACPAgentSession", () => {
}),
);
(session as unknown as ACPSessionInternals).sessionId = "session-1";
(session as unknown as ACPSessionInternals).connection = { prompt };
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
asInternals<ACPSessionInternals>(session).connection = { prompt };
session.subscribe((event) => {
events.push(event as { type: string; turnId?: string });
@@ -1252,7 +1253,7 @@ describe("ACPAgentSession", () => {
type: "turn_started",
turnId,
});
expect((session as unknown as ACPSessionInternals).activeForegroundTurnId).toBe(turnId);
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBe(turnId);
resolvePrompt({ stopReason: "end_turn", usage: { outputTokens: 3 } });
await Promise.resolve();
@@ -1262,7 +1263,7 @@ describe("ACPAgentSession", () => {
type: "turn_completed",
turnId,
});
expect((session as unknown as ACPSessionInternals).activeForegroundTurnId).toBeNull();
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBeNull();
});
test("startTurn converts background prompt rejections into turn_failed events", async () => {
@@ -1276,8 +1277,8 @@ describe("ACPAgentSession", () => {
}),
);
(session as unknown as ACPSessionInternals).sessionId = "session-1";
(session as unknown as ACPSessionInternals).connection = { prompt };
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
asInternals<ACPSessionInternals>(session).connection = { prompt };
session.subscribe((event) => {
events.push(event as { type: string; turnId?: string; error?: string });
@@ -1295,6 +1296,6 @@ describe("ACPAgentSession", () => {
turnId,
error: "prompt failed",
});
expect((session as unknown as ACPSessionInternals).activeForegroundTurnId).toBeNull();
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBeNull();
});
});
@@ -14,6 +14,7 @@ import type {
AgentTimelineItem,
} from "../agent-sdk-types.js";
import { ACPAgentSession } from "./acp-agent.js";
import { asInternals } from "../../test-utils/class-mocks.js";
const smokeSelection = new Set(
(process.env.ACP_SMOKE ?? "")
@@ -117,7 +118,7 @@ function createSmokeLogger(): SmokeLogger {
error: vi.fn(),
fatal: vi.fn(),
};
return logger as unknown as SmokeLogger;
return asInternals<SmokeLogger>(logger);
}
function createTrace(config: WrapperSmokeConfig): SmokeTrace {
@@ -365,7 +366,7 @@ async function bootSession(
}
async function captureFinalState(session: ACPAgentSession, trace: SmokeTrace): Promise<void> {
const internals = session as unknown as SessionInternals;
const internals = asInternals<SessionInternals>(session);
trace.finalState = {
runtime: await session.getRuntimeInfo(),
availableModes: await session.getAvailableModes(),
@@ -376,7 +377,7 @@ async function captureFinalState(session: ACPAgentSession, trace: SmokeTrace): P
}
function getModelIds(session: ACPAgentSession): string[] {
const internals = session as unknown as SessionInternals;
const internals = asInternals<SessionInternals>(session);
return internals.availableModels?.map((model) => model.modelId) ?? [];
}
@@ -384,7 +385,7 @@ function getSelectOption(
session: ACPAgentSession,
category: string,
): Extract<SessionConfigOption, { type: "select" }> | null {
const internals = session as unknown as SessionInternals;
const internals = asInternals<SessionInternals>(session);
return (
internals.configOptions.find(
(option): option is Extract<SessionConfigOption, { type: "select" }> =>
@@ -515,7 +516,7 @@ for (const config of wrappers) {
const modeAfterSuccess = await session.getCurrentMode();
originalSessionId = session.id;
(session as unknown as { sessionId: string }).sessionId = `${originalSessionId}-missing`;
asInternals<{ sessionId: string }>(session).sessionId = `${originalSessionId}-missing`;
await expect(session.setMode(mode.id)).rejects.toThrow();
expect(await session.getCurrentMode()).toBe(modeAfterSuccess);
trace.notes.push(
@@ -523,7 +524,7 @@ for (const config of wrappers) {
);
} finally {
if (session && originalSessionId) {
(session as unknown as { sessionId: string }).sessionId = originalSessionId;
asInternals<{ sessionId: string }>(session).sessionId = originalSessionId;
}
await captureFinalStateIfOpen(session, trace);
emitEvidence(`${config.id}:b`, trace);
@@ -622,7 +623,7 @@ for (const config of wrappers) {
expect(
trace.notifications.some((entry) => JSON.stringify(entry).includes("tool_call_update")),
).toBe(true);
expect((session as unknown as SessionInternals).toolCalls.size).toBeGreaterThan(0);
expect(asInternals<SessionInternals>(session).toolCalls.size).toBeGreaterThan(0);
trace.criteria.toolSnapshot = {
outcome: "PASS",
detail:
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
import type { Logger } from "pino";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { asInternals } from "../../test-utils/class-mocks.js";
import { ClaudeAgentClient, readEventIdentifiers } from "./claude-agent.js";
import { streamSession } from "./test-utils/session-stream-adapter.js";
import type { AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
@@ -124,7 +125,7 @@ function createSpyLogger(): {
loggerLike.child.mockReturnValue(loggerLike);
return {
logger: loggerLike as unknown as Logger,
logger: asInternals<Logger>(loggerLike),
calls,
debug,
info,
@@ -261,7 +262,7 @@ test("logs redacted query summary and never leaks sentinel secrets", async () =>
test("interruptActiveTurn only interrupts the active query without info logs", async () => {
const spy = createSpyLogger();
const session = await createSessionWithLogger(spy.logger);
const internal = session as unknown as {
const internal: {
query: {
interrupt: () => Promise<void>;
return?: () => Promise<void>;
@@ -270,7 +271,7 @@ test("interruptActiveTurn only interrupts the active query without info logs", a
input: { end: () => void } | null;
queryRestartNeeded: boolean;
interruptActiveTurn: () => Promise<void>;
};
} = asInternals(session);
const interrupt = vi.fn(async () => undefined);
const queryReturn = vi.fn(async () => undefined);
const end = vi.fn(() => undefined);
@@ -383,7 +384,7 @@ test("extracts identifiers from fixture-driven protocol shape variants", () => {
for (const fixture of fixtures) {
expect(
readEventIdentifiers(
fixture.message as unknown as Parameters<typeof readEventIdentifiers>[0],
asInternals<Parameters<typeof readEventIdentifiers>[0]>(fixture.message),
),
).toEqual(fixture.expected);
}
@@ -411,12 +412,12 @@ test("captures session IDs from fixture-driven init message variants", async ()
await Promise.all(
fixtures.map(async (fixture) => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
handleSystemMessage: (message: Record<string, unknown>) => {
threadStartedSessionId: string | null;
notice: AgentTimelineItem | null;
};
};
} = asInternals(session);
try {
const started = internal.handleSystemMessage({
type: "system",
@@ -439,12 +440,12 @@ test("captures session IDs from fixture-driven init message variants", async ()
test("waits for complete JSON values before updating tool input from input_json_delta", async () => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
toolUseIndexToId: Map<number, string>;
toolUseInputBuffers: Map<string, string>;
};
} = asInternals(session);
const toolUseId = "tool-input-delta";
const index = 7;
@@ -501,7 +502,7 @@ test("waits for complete JSON values before updating tool input from input_json_
] as const;
for (const fixture of deltaFixtures) {
internal.mapPartialEvent(fixture.event as unknown as Record<string, unknown>);
internal.mapPartialEvent(asInternals<Record<string, unknown>>(fixture.event));
expect(readCommand()).toBe(fixture.expectedCommand);
}
@@ -518,10 +519,10 @@ test("waits for complete JSON values before updating tool input from input_json_
test("does not surface incomplete string values from input_json_delta", async () => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
};
} = asInternals(session);
const toolUseId = "tool-input-preview";
const index = 8;
@@ -555,12 +556,12 @@ test("does not surface incomplete string values from input_json_delta", async ()
test("maps tool_result content shapes into deterministic string output", async () => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
buildToolOutput: (
block: Record<string, unknown>,
entry: Record<string, unknown> | undefined,
) => Record<string, unknown> | undefined;
};
} = asInternals(session);
const toolEntry = {
id: "tool-1",
@@ -628,12 +629,12 @@ test("maps tool_result content shapes into deterministic string output", async (
test("Grep tool_result string content flows to a search detail with content", async () => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
buildToolOutput: (
block: Record<string, unknown>,
entry: Record<string, unknown> | undefined,
) => Record<string, unknown> | undefined;
};
} = asInternals(session);
const grepEntry = {
id: "tool-grep-1",
@@ -890,13 +891,13 @@ test("plan approval exposes a resume-bypass action and can return to bypassPermi
await session.setMode("bypassPermissions");
await session.setMode("plan");
const internal = session as unknown as {
const internal: {
handlePermissionRequest: (
toolName: string,
input: Record<string, unknown>,
options: Record<string, unknown>,
) => Promise<unknown>;
};
} = asInternals(session);
const pendingResolution = internal.handlePermissionRequest(
"ExitPlanMode",
@@ -956,12 +957,12 @@ test("plan approval exposes a resume-bypass action and can return to bypassPermi
test("reuses one autonomous run for unbound stream_event bursts with no foreground run", async () => {
const session = await createSession();
const internal = session as unknown as {
const internal: {
turnState: "idle" | "foreground" | "autonomous";
nextTurnOrdinal: number;
routeSdkMessageFromPump: (message: Record<string, unknown>) => void;
autonomousTurn: { id: string } | null;
};
} = asInternals(session);
internal.turnState = "idle";
internal.routeSdkMessageFromPump({
@@ -1352,9 +1353,9 @@ test("does not use stream_event uuid as assistant message identity when message_
expect(assistantText).toContain("HELLO WORLD");
const assembler = session as unknown as {
const assembler: {
timelineAssembler: { messages: Map<string, unknown> };
};
} = asInternals(session);
expect(assembler.timelineAssembler.messages.size).toBe(0);
await session.close();
@@ -18,6 +18,7 @@ import {
codexAppServerTurnInputFromPrompt,
} from "./codex-app-server-agent.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { asInternals as castInternals, createStub } from "../../test-utils/class-mocks.js";
interface CollaborationModeRecord {
name: string;
@@ -71,7 +72,7 @@ function createSession(configOverrides: Partial<AgentSessionConfig> = {}): Codex
() => {
throw new Error("Test session cannot spawn Codex app-server");
},
) as unknown as CodexTestSession;
) as CodexTestSession;
session.connected = true;
session.currentThreadId = "test-thread";
session.activeForegroundTurnId = "test-turn";
@@ -79,7 +80,7 @@ function createSession(configOverrides: Partial<AgentSessionConfig> = {}): Codex
}
function asInternals(session: CodexTestSession): CodexSessionTestAccess {
return session as unknown as CodexSessionTestAccess;
return castInternals<CodexSessionTestAccess>(session);
}
describe("Codex app-server provider", () => {
@@ -105,9 +106,9 @@ describe("Codex app-server provider", () => {
{},
true,
);
(session as unknown as { client: CodexClientLike }).client = fakeClient;
castInternals<{ client: CodexClientLike }>(session).client = fakeClient;
await (session as unknown as { ensureThread: () => Promise<void> }).ensureThread();
await castInternals<{ ensureThread: () => Promise<void> }>(session).ensureThread();
const startCall = requests.find((req) => req.method === "thread/start");
expect(startCall).toBeDefined();
@@ -134,9 +135,9 @@ describe("Codex app-server provider", () => {
throw new Error("Test session cannot spawn Codex app-server");
},
);
(session as unknown as { client: CodexClientLike }).client = fakeClient;
castInternals<{ client: CodexClientLike }>(session).client = fakeClient;
await (session as unknown as { ensureThread: () => Promise<void> }).ensureThread();
await castInternals<{ ensureThread: () => Promise<void> }>(session).ensureThread();
const startCall = requests.find((req) => req.method === "thread/start");
expect(startCall).toBeDefined();
@@ -349,7 +350,7 @@ describe("Codex app-server provider", () => {
});
session.activeForegroundTurnId = null;
session.client = { request } as unknown as CodexClientLike;
session.client = createStub<CodexClientLike>({ request });
await session.startTurn("Return JSON", {
outputSchema: {
@@ -405,7 +406,7 @@ describe("Codex app-server provider", () => {
});
session.activeForegroundTurnId = null;
session.client = { request } as unknown as CodexClientLike;
session.client = createStub<CodexClientLike>({ request });
await session.startTurn("/paseo-implement in a worktree, remember to use Claude for the UI");
@@ -1439,7 +1440,7 @@ describe("Codex app-server provider", () => {
});
session.activeForegroundTurnId = null;
session.client = { request } as unknown as CodexClientLike;
session.client = createStub<CodexClientLike>({ request });
const events: AgentStreamEvent[] = [];
session.subscribe((event) => events.push(event));
@@ -1,6 +1,7 @@
import { describe, expect, test, vi } from "vitest";
import type pino from "pino";
import { Session, type SessionOptions } from "./session.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import type {
WorkspaceGitListener,
WorkspaceGitRuntimeSnapshot,
@@ -150,20 +151,20 @@ function createSessionForWorkspaceGitWatchTests(): {
const session = new Session({
clientId: "test-client",
onMessage: (message) => emitted.push(message as { type: string; payload: unknown }),
logger: logger as unknown as pino.Logger,
downloadTokenStore: {} as SessionOptions["downloadTokenStore"],
pushTokenStore: {} as SessionOptions["pushTokenStore"],
logger: createStub<pino.Logger>(logger),
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
paseoHome: "/tmp/paseo-test",
agentManager: {
agentManager: createStub<SessionOptions["agentManager"]>({
subscribe: () => () => {},
listAgents: () => [],
getAgent: () => null,
} as unknown as SessionOptions["agentManager"],
agentStorage: {
}),
agentStorage: createStub<SessionOptions["agentStorage"]>({
list: async () => [],
get: async () => null,
} as unknown as SessionOptions["agentStorage"],
projectRegistry: {
}),
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => Array.from(projects.values()),
@@ -179,8 +180,8 @@ function createSessionForWorkspaceGitWatchTests(): {
remove: async (projectId: string) => {
projects.delete(projectId);
},
} as unknown as SessionOptions["projectRegistry"],
workspaceRegistry: {
}),
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => Array.from(workspaces.values()),
@@ -196,8 +197,8 @@ function createSessionForWorkspaceGitWatchTests(): {
remove: async (workspaceId: string) => {
workspaces.delete(workspaceId);
},
} as unknown as SessionOptions["workspaceRegistry"],
checkoutDiffManager: {
}),
checkoutDiffManager: createStub<SessionOptions["checkoutDiffManager"]>({
subscribe: async () => ({
initial: { cwd: "/tmp", files: [], error: null },
unsubscribe: () => {},
@@ -210,15 +211,15 @@ function createSessionForWorkspaceGitWatchTests(): {
checkoutDiffFallbackRefreshTargetCount: 0,
}),
dispose: () => {},
} as unknown as SessionOptions["checkoutDiffManager"],
}),
workspaceGitService,
mcpBaseUrl: null,
stt: null,
tts: null,
terminalManager: null,
} as unknown as SessionOptions);
});
(session as unknown as SessionInternals).listAgentPayloads = async () => [];
asInternals<SessionInternals>(session).listAgentPayloads = async () => [];
return {
session,
@@ -267,7 +268,7 @@ describe("workspace git watch targets", () => {
test("emits one workspace_update when the workspace git service emits a changed snapshot", async () => {
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
createSessionForWorkspaceGitWatchTests();
const sessionAny = session as unknown as SessionInternals;
const sessionAny = asInternals<SessionInternals>(session);
seedGitWorkspace({
projects,
workspaces,
@@ -342,7 +343,7 @@ describe("workspace git watch targets", () => {
test("emits checkout_status_update to a client subscribed to the workspace git target", async () => {
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
createSessionForWorkspaceGitWatchTests();
const sessionAny = session as unknown as SessionInternals;
const sessionAny = asInternals<SessionInternals>(session);
seedGitWorkspace({
projects,
workspaces,
@@ -405,7 +406,7 @@ describe("workspace git watch targets", () => {
test("embeds PR status in checkout_status_update for GitHub-inclusive snapshot pushes", async () => {
const { session, emitted, projects, workspaces, subscriptions } =
createSessionForWorkspaceGitWatchTests();
const sessionAny = session as unknown as SessionInternals;
const sessionAny = asInternals<SessionInternals>(session);
seedGitWorkspace({
projects,
workspaces,
@@ -11,6 +11,7 @@ import { expect, test, vi } from "vitest";
import { Session, type SessionOptions } from "./session.js";
import type { SessionOutboundMessage } from "../shared/messages.js";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
@@ -87,11 +88,11 @@ function createHarness(input: {
clientId: "test",
appVersion: null,
onMessage: (m) => emitted.push(m),
logger: logger as unknown as SessionOptions["logger"],
downloadTokenStore: {} as unknown as SessionOptions["downloadTokenStore"],
pushTokenStore: {} as unknown as SessionOptions["pushTokenStore"],
logger: createStub<SessionOptions["logger"]>(logger),
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
paseoHome: mkdtempSync(path.join(tmpdir(), "paseo-invariant-test-")),
agentManager: {
agentManager: createStub<SessionOptions["agentManager"]>({
subscribe: () => () => {},
listAgents: () => [],
getAgent: () => null,
@@ -99,12 +100,12 @@ function createHarness(input: {
archiveSnapshot: async () => ({}),
clearAgentAttention: async () => {},
notifyAgentState: () => {},
} as unknown as SessionOptions["agentManager"],
agentStorage: {
}),
agentStorage: createStub<SessionOptions["agentStorage"]>({
list: async () => [],
get: async () => null,
} as unknown as SessionOptions["agentStorage"],
projectRegistry: {
}),
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => Array.from(projects.values()),
@@ -119,8 +120,8 @@ function createHarness(input: {
remove: async (id: string) => {
projects.delete(id);
},
} as unknown as SessionOptions["projectRegistry"],
workspaceRegistry: {
}),
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => Array.from(workspaces.values()),
@@ -135,11 +136,11 @@ function createHarness(input: {
remove: async (id: string) => {
workspaces.delete(id);
},
} as unknown as SessionOptions["workspaceRegistry"],
chatService: {} as unknown as SessionOptions["chatService"],
scheduleService: {} as unknown as SessionOptions["scheduleService"],
loopService: {} as unknown as SessionOptions["loopService"],
checkoutDiffManager: {
}),
chatService: createStub<SessionOptions["chatService"]>({}),
scheduleService: createStub<SessionOptions["scheduleService"]>({}),
loopService: createStub<SessionOptions["loopService"]>({}),
checkoutDiffManager: createStub<SessionOptions["checkoutDiffManager"]>({
subscribe: async () => ({
initial: { cwd: "/tmp", files: [], error: null },
unsubscribe: () => {},
@@ -152,12 +153,12 @@ function createHarness(input: {
checkoutDiffFallbackRefreshTargetCount: 0,
}),
dispose: () => {},
} as unknown as SessionOptions["checkoutDiffManager"],
}),
workspaceGitService,
daemonConfigStore: {
daemonConfigStore: createStub<SessionOptions["daemonConfigStore"]>({
get: () => ({ mcp: { injectIntoAgents: false }, providers: {} }),
onChange: () => () => {},
} as unknown as SessionOptions["daemonConfigStore"],
}),
mcpBaseUrl: null,
stt: null,
tts: null,
@@ -168,7 +169,7 @@ function createHarness(input: {
}
async function openProject(session: Session, cwd: string, requestId = "req-1") {
await (session as unknown as { handleMessage(m: unknown): Promise<unknown> }).handleMessage({
await asInternals<{ handleMessage(m: unknown): Promise<unknown> }>(session).handleMessage({
type: "open_project_request",
cwd,
requestId,
@@ -436,11 +437,9 @@ test("S12: findWorkspaceByDirectory does not return archived ancestor via prefix
workspaces: [dirWorkspace("/parent", archivedAt)],
projects: [dirProject("/parent", archivedAt)],
});
const found = await (
h.session as unknown as {
findWorkspaceByDirectory(cwd: string): Promise<unknown>;
}
).findWorkspaceByDirectory("/parent/child");
const found = await asInternals<{
findWorkspaceByDirectory(cwd: string): Promise<unknown>;
}>(h.session).findWorkspaceByDirectory("/parent/child");
expect(found).toBeNull();
});
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, test, vi } from "vitest";
import { Session } from "./session.js";
import type { SessionOptions } from "./session.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
interface SessionInternals {
archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>;
@@ -90,51 +91,53 @@ describe("snapshot mutation ownership boundary", () => {
error: vi.fn(),
};
const session = new Session({
clientId: "test-client",
onMessage,
logger: logger as unknown as SessionOptions["logger"],
downloadTokenStore: {} as unknown as SessionOptions["downloadTokenStore"],
pushTokenStore: {} as unknown as SessionOptions["pushTokenStore"],
paseoHome: "/tmp/paseo-test",
agentManager: {
subscribe: () => () => {},
listAgents: () => [],
getAgent: () => null,
archiveSnapshot,
updateAgentMetadata,
} as unknown as SessionOptions["agentManager"],
agentStorage: {
list: async () => [],
get: async () => storedRecord,
applySnapshot: directStorageWrite,
upsert: directStorageWrite,
} as unknown as SessionOptions["agentStorage"],
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as unknown as SessionOptions["projectRegistry"],
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as unknown as SessionOptions["workspaceRegistry"],
createAgentMcpTransport: async () => {
throw new Error("not used");
},
stt: null,
tts: null,
terminalManager: null,
}) as unknown as SessionInternals;
const session = asInternals<SessionInternals>(
new Session({
clientId: "test-client",
onMessage,
logger: createStub<SessionOptions["logger"]>(logger),
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
paseoHome: "/tmp/paseo-test",
agentManager: createStub<SessionOptions["agentManager"]>({
subscribe: () => () => {},
listAgents: () => [],
getAgent: () => null,
archiveSnapshot,
updateAgentMetadata,
}),
agentStorage: createStub<SessionOptions["agentStorage"]>({
list: async () => [],
get: async () => storedRecord,
applySnapshot: directStorageWrite,
upsert: directStorageWrite,
}),
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
}),
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
}),
createAgentMcpTransport: async () => {
throw new Error("not used");
},
stt: null,
tts: null,
terminalManager: null,
}),
);
const archiveResult = await session.archiveAgentForClose("agent-1");
expect(archiveSnapshot).toHaveBeenCalledTimes(1);
@@ -0,0 +1,57 @@
/**
* Test helpers for typed stubs and internal access without scatter of
* `as unknown as` casts across test files.
*
* WHY createStub contains one cast:
* TypeScript's structural type system cannot accept Partial<T> as T when T has
* private class members — no object-literal syntax satisfies private fields
* from outside the class. Centralising the cast here adds real runtime value
* over a bare `obj as unknown as T` scattered across tests:
* - A Proxy intercepts every property access.
* - Calling any unstubbed method throws immediately with a specific message
* ("createStub: 'listAgents' was called but not stubbed") instead of
* silently returning undefined and causing a confusing downstream failure.
*
* Alternatives rejected:
* - mockOf<T>(p): return p as unknown as T — same single cast, zero runtime
* benefit; this is the "option 4 trap" the task spec explicitly flags.
* - Object.create(ctor.prototype): only works for concrete classes, not for
* interfaces (ProjectRegistry, WorkspaceRegistry) or external library types
* (pino.Logger, http.Server); Object.create returns `any` so a cast is
* still needed in the generic path.
* - class FakeT implements T: verbose for large classes (AgentManager has
* 30+ public methods); must be kept in sync whenever the class grows.
*/
/**
* Creates a typed test stub for any class or interface T.
* Stubs only the methods/properties provided; any other property access throws.
*
* Keys of `stubs` are checked against T at compile time (preventing typos).
* Any value type is accepted for each key, so `vi.fn()` always satisfies the
* slot regardless of the original method's return type.
*/
export function createStub<T extends object>(stubs: { [K in keyof T]?: unknown }): T {
return new Proxy(stubs as Record<string | symbol, unknown>, {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver);
if (typeof prop === "symbol") return undefined;
return (..._args: unknown[]): never => {
throw new Error(`createStub: "${String(prop)}" was called but not stubbed`);
};
},
}) as unknown as T; // one justified cast — rationale in file-level comment
}
/**
* Widens a real object to a test-internal interface T without a double assertion.
*
* Use when tests need to access private fields or call private methods of a
* real class instance (e.g. `asInternals<WebSocketServerInternals>(server)`).
* The parameter is typed `unknown` so TypeScript accepts the single `as T`
* assertion — narrowing from the top type is safe, unlike `as unknown as T`
* which short-circuits structural checking on a concrete source type.
*/
export function asInternals<T>(obj: unknown): T {
return obj as T;
}
@@ -4,78 +4,99 @@ import type { ProviderSnapshotEntry } from "../agent/agent-sdk-types.js";
import { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js";
import type { SessionOptions } from "../session.js";
import type { SessionOutboundMessage } from "../../shared/messages.js";
import { asInternals, createStub } from "./class-mocks.js";
// ---------------------------------------------------------------------------
// Unsafe stub wrappers — as unknown as is ONLY here, never in scope test files
// Typed stub wrappers — unsafe cast is in createStub (class-mocks.ts), never
// directly in test files. Wrapper signatures narrow the accepted key set so
// callers get compile-time feedback on typos in method names.
// ---------------------------------------------------------------------------
export function asSessionLogger(stub: object): SessionOptions["logger"] {
return stub as unknown as SessionOptions["logger"];
export function asSessionLogger(stub: {
[K in keyof SessionOptions["logger"]]?: unknown;
}): SessionOptions["logger"] {
return createStub<SessionOptions["logger"]>(stub);
}
export function asAgentManager(stub: object): SessionOptions["agentManager"] {
return stub as unknown as SessionOptions["agentManager"];
export function asAgentManager(stub: {
[K in keyof SessionOptions["agentManager"]]?: unknown;
}): SessionOptions["agentManager"] {
return createStub<SessionOptions["agentManager"]>(stub);
}
export function asAgentStorage(stub: object): SessionOptions["agentStorage"] {
return stub as unknown as SessionOptions["agentStorage"];
export function asAgentStorage(stub: {
[K in keyof SessionOptions["agentStorage"]]?: unknown;
}): SessionOptions["agentStorage"] {
return createStub<SessionOptions["agentStorage"]>(stub);
}
export function asDownloadTokenStore(): SessionOptions["downloadTokenStore"] {
return {} as unknown as SessionOptions["downloadTokenStore"];
return createStub<SessionOptions["downloadTokenStore"]>({});
}
export function asPushTokenStore(): SessionOptions["pushTokenStore"] {
return {} as unknown as SessionOptions["pushTokenStore"];
return createStub<SessionOptions["pushTokenStore"]>({});
}
export function asChatService(): SessionOptions["chatService"] {
return {} as unknown as SessionOptions["chatService"];
return createStub<SessionOptions["chatService"]>({});
}
export function asScheduleService(): SessionOptions["scheduleService"] {
return {} as unknown as SessionOptions["scheduleService"];
return createStub<SessionOptions["scheduleService"]>({});
}
export function asLoopService(): SessionOptions["loopService"] {
return {} as unknown as SessionOptions["loopService"];
return createStub<SessionOptions["loopService"]>({});
}
export function asCheckoutDiffManager(stub: object): SessionOptions["checkoutDiffManager"] {
return stub as unknown as SessionOptions["checkoutDiffManager"];
export function asCheckoutDiffManager(stub: {
[K in keyof SessionOptions["checkoutDiffManager"]]?: unknown;
}): SessionOptions["checkoutDiffManager"] {
return createStub<SessionOptions["checkoutDiffManager"]>(stub);
}
export function asDaemonConfigStore(stub: object): SessionOptions["daemonConfigStore"] {
return stub as unknown as SessionOptions["daemonConfigStore"];
export function asDaemonConfigStore(stub: {
[K in keyof SessionOptions["daemonConfigStore"]]?: unknown;
}): SessionOptions["daemonConfigStore"] {
return createStub<SessionOptions["daemonConfigStore"]>(stub);
}
export function asTerminalManager(stub: object): NonNullable<SessionOptions["terminalManager"]> {
return stub as unknown as NonNullable<SessionOptions["terminalManager"]>;
export function asTerminalManager(stub: {
[K in keyof NonNullable<SessionOptions["terminalManager"]>]?: unknown;
}): NonNullable<SessionOptions["terminalManager"]> {
return createStub<NonNullable<SessionOptions["terminalManager"]>>(stub);
}
export function asGitHubService(stub: object): NonNullable<SessionOptions["github"]> {
return stub as unknown as NonNullable<SessionOptions["github"]>;
export function asGitHubService(stub: {
[K in keyof NonNullable<SessionOptions["github"]>]?: unknown;
}): NonNullable<SessionOptions["github"]> {
return createStub<NonNullable<SessionOptions["github"]>>(stub);
}
export function asWorkspaceGitService(stub: object): SessionOptions["workspaceGitService"] {
return stub as unknown as SessionOptions["workspaceGitService"];
export function asWorkspaceGitService(stub: {
[K in keyof SessionOptions["workspaceGitService"]]?: unknown;
}): SessionOptions["workspaceGitService"] {
return createStub<SessionOptions["workspaceGitService"]>(stub);
}
export function asScriptRouteStore(stub: object): SessionOptions["scriptRouteStore"] {
return stub as unknown as SessionOptions["scriptRouteStore"];
export function asScriptRouteStore(stub: {
[K in keyof SessionOptions["scriptRouteStore"]]?: unknown;
}): SessionOptions["scriptRouteStore"] {
return createStub<SessionOptions["scriptRouteStore"]>(stub);
}
export function asWorkspaceScriptRuntimeStore(stub: object): SessionOptions["scriptRuntimeStore"] {
return stub as unknown as SessionOptions["scriptRuntimeStore"];
export function asWorkspaceScriptRuntimeStore(stub: {
[K in keyof SessionOptions["scriptRuntimeStore"]]?: unknown;
}): SessionOptions["scriptRuntimeStore"] {
return createStub<SessionOptions["scriptRuntimeStore"]>(stub);
}
// ---------------------------------------------------------------------------
// Private session access — moves the unsafe cast out of scope files
// Private session access — delegates to asInternals so test files need no cast
// ---------------------------------------------------------------------------
export function asSessionInternals<T>(session: unknown): T {
return session as unknown as T;
}
export { asInternals as asSessionInternals };
// ---------------------------------------------------------------------------
// Type guard for SessionOutboundMessage — avoids casting unknown in test emit overrides
@@ -134,7 +155,7 @@ export function createProviderSnapshotManagerStub(): {
};
on.mockImplementation(() => stub);
off.mockImplementation(() => stub);
const manager = stub as unknown as ProviderSnapshotManager;
const manager = createStub<ProviderSnapshotManager>(stub);
return {
manager,
getSnapshot,
@@ -9,6 +9,7 @@ import type { FileBackedChatService } from "./chat/chat-service.js";
import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
const wsModuleMock = vi.hoisted(() => {
class MockWebSocketServer {
@@ -92,14 +93,14 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
};
const server = new VoiceAssistantWebSocketServer(
{} as unknown as HTTPServer,
createLogger() as unknown as pino.Logger,
createStub<HTTPServer>({}),
createStub<pino.Logger>(createLogger()),
"srv-test",
agentManager as unknown as AgentManager,
{} as unknown as AgentStorage,
{} as unknown as DownloadTokenStore,
createStub<AgentManager>(agentManager),
createStub<AgentStorage>({}),
createStub<DownloadTokenStore>({}),
"/tmp/paseo-test",
daemonConfigStore as unknown as DaemonConfigStore,
createStub<DaemonConfigStore>(daemonConfigStore),
null,
{ allowedOrigins: new Set() },
undefined,
@@ -113,10 +114,10 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
undefined,
undefined,
undefined,
{} as unknown as FileBackedChatService,
{} as unknown as LoopService,
{} as unknown as ScheduleService,
{
createStub<FileBackedChatService>({}),
createStub<LoopService>({}),
createStub<ScheduleService>({}),
createStub<CheckoutDiffManager>({
subscribe: vi.fn(),
scheduleRefreshForCwd: vi.fn(),
getMetrics: vi.fn(() => ({
@@ -126,7 +127,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
checkoutDiffFallbackRefreshTargetCount: 0,
})),
dispose: vi.fn(),
} as unknown as CheckoutDiffManager,
}),
);
return { server, agentManager };
@@ -167,7 +168,7 @@ function connectClient(
} | null,
) {
const ws = createOpenSocket();
(server as unknown as WebSocketServerInternals).sessions.set(ws, {
asInternals<WebSocketServerInternals>(server).sessions.set(ws, {
session: createSessionWithActivity(activity),
clientId: "client-test",
appVersion: null,
@@ -207,7 +208,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
getLastAssistantMessage,
});
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
agentId: "agent-1",
provider: "claude",
reason: "finished",
@@ -237,7 +238,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
getLastAssistantMessage,
});
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
agentId: "agent-2",
provider: "claude",
reason: "finished",
@@ -263,7 +264,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
lastActivityAt: new Date(nowMs - 300_000),
});
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
agentId: "agent-X",
provider: "claude",
reason: "finished",
@@ -278,7 +279,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
const { server } = createServer();
const ws = connectClient(server, null);
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
agentId: "agent-no-heartbeat",
provider: "claude",
reason: "finished",
@@ -292,7 +293,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
const { server } = createServer();
const ws = connectClient(server, null);
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
agentId: "agent-no-heartbeat",
provider: "claude",
reason: "error",
@@ -9,6 +9,7 @@ import type { FileBackedChatService } from "./chat/chat-service.js";
import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import {
asUint8Array,
decodeTerminalStreamFrame,
@@ -191,10 +192,10 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
onChange: vi.fn(() => () => {}),
};
return new VoiceAssistantWebSocketServer(
{} as unknown as HTTPServer,
createLogger() as unknown as pino.Logger,
createStub<HTTPServer>({}),
createStub<pino.Logger>(createLogger()),
"srv_test",
{
createStub<AgentManager>({
setAgentAttentionCallback: vi.fn(),
getAgent: vi.fn(() => null),
getMetricsSnapshot: vi.fn(() => ({
@@ -204,11 +205,11 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
pendingPermissionAgents: 0,
erroredAgents: 0,
})),
} as unknown as AgentManager,
{} as unknown as AgentStorage,
{} as unknown as DownloadTokenStore,
}),
createStub<AgentStorage>({}),
createStub<DownloadTokenStore>({}),
"/tmp/paseo-test",
daemonConfigStore as unknown as DaemonConfigStore,
createStub<DaemonConfigStore>(daemonConfigStore),
null,
{ allowedOrigins: new Set() },
undefined,
@@ -227,10 +228,10 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
undefined,
undefined,
undefined,
{} as unknown as FileBackedChatService,
{} as unknown as LoopService,
{} as unknown as ScheduleService,
{
createStub<FileBackedChatService>({}),
createStub<LoopService>({}),
createStub<ScheduleService>({}),
createStub<CheckoutDiffManager>({
subscribe: vi.fn(),
scheduleRefreshForCwd: vi.fn(),
getMetrics: vi.fn(() => ({
@@ -240,7 +241,7 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
checkoutDiffFallbackRefreshTargetCount: 0,
})),
dispose: vi.fn(),
} as unknown as CheckoutDiffManager,
}),
);
}
@@ -366,7 +367,7 @@ async function attachDirectAndHello(params: {
socket: MockSocket;
clientId: string;
}) {
await (params.server as unknown as WebSocketServerInternals).attachSocket(
await asInternals<WebSocketServerInternals>(params.server).attachSocket(
params.socket,
createDirectRequest(),
);
@@ -426,10 +427,7 @@ describe("relay external socket reconnect behavior", () => {
const server = createServer();
const socket = new MockSocket();
await (server as unknown as WebSocketServerInternals).attachSocket(
socket,
createDirectRequest(),
);
await asInternals<WebSocketServerInternals>(server).attachSocket(socket, createDirectRequest());
socket.emit(
"message",
JSON.stringify(
@@ -460,10 +458,7 @@ describe("relay external socket reconnect behavior", () => {
closeReason = typeof reason === "string" ? reason : "";
});
await (server as unknown as WebSocketServerInternals).attachSocket(
socket,
createDirectRequest(),
);
await asInternals<WebSocketServerInternals>(server).attachSocket(socket, createDirectRequest());
await vi.advanceTimersByTimeAsync(15_000);
expect(closeCode).toBe(4001);