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:
@@ -22,6 +22,7 @@ import {
|
|||||||
import { transformPiModels } from "./pi-direct-agent.js";
|
import { transformPiModels } from "./pi-direct-agent.js";
|
||||||
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
import { asInternals } from "../../test-utils/class-mocks.js";
|
||||||
|
|
||||||
interface ACPSessionInternals {
|
interface ACPSessionInternals {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
@@ -159,7 +160,7 @@ function prepareConfiguredOverrideSession(
|
|||||||
const setSessionConfigOption = vi.fn(async () => ({
|
const setSessionConfigOption = vi.fn(async () => ({
|
||||||
configOptions: options.configOptions ?? [],
|
configOptions: options.configOptions ?? [],
|
||||||
}));
|
}));
|
||||||
const internals = session as unknown as ACPConfiguredOverrideInternals;
|
const internals = asInternals<ACPConfiguredOverrideInternals>(session);
|
||||||
internals.sessionId = "session-1";
|
internals.sessionId = "session-1";
|
||||||
internals.connection = {
|
internals.connection = {
|
||||||
setSessionMode,
|
setSessionMode,
|
||||||
@@ -179,7 +180,7 @@ function prepareConfiguredOverrideSession(
|
|||||||
test("ACP setModel only uses config-option fallback when the matching select choice contains the model", async () => {
|
test("ACP setModel only uses config-option fallback when the matching select choice contains the model", async () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
const childLogger = { warn: vi.fn() };
|
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 session = createSessionWithConfig({}, logger);
|
||||||
const setSessionConfigOption = vi.fn(async () => ({
|
const setSessionConfigOption = vi.fn(async () => ({
|
||||||
configOptions: [
|
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.sessionId = "session-1";
|
||||||
internals.connection = { setSessionConfigOption };
|
internals.connection = { setSessionConfigOption };
|
||||||
internals.configOptions = [
|
internals.configOptions = [
|
||||||
@@ -235,7 +236,7 @@ describe("createLoggedNdJsonStream", () => {
|
|||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
|
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
|
||||||
logger: logger as unknown as ReturnType<typeof createTestLogger>,
|
logger: asInternals<ReturnType<typeof createTestLogger>>(logger),
|
||||||
provider: "gemini",
|
provider: "gemini",
|
||||||
});
|
});
|
||||||
const reader = stream.readable.getReader();
|
const reader = stream.readable.getReader();
|
||||||
@@ -276,7 +277,7 @@ describe("createLoggedNdJsonStream", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
|
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
|
||||||
logger: logger as unknown as ReturnType<typeof createTestLogger>,
|
logger: asInternals<ReturnType<typeof createTestLogger>>(logger),
|
||||||
provider: "gemini",
|
provider: "gemini",
|
||||||
});
|
});
|
||||||
const reader = stream.readable.getReader();
|
const reader = stream.readable.getReader();
|
||||||
@@ -475,7 +476,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
modelId: "sonnet",
|
modelId: "sonnet",
|
||||||
});
|
});
|
||||||
|
|
||||||
const modeEvents = (validSession as unknown as ACPSessionInternals).translateSessionUpdate({
|
const modeEvents = asInternals<ACPSessionInternals>(validSession).translateSessionUpdate({
|
||||||
sessionUpdate: "current_mode_update",
|
sessionUpdate: "current_mode_update",
|
||||||
currentModeId: "default",
|
currentModeId: "default",
|
||||||
});
|
});
|
||||||
@@ -494,7 +495,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
|
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
const childLogger = { warn: vi.fn() };
|
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(
|
const invalidSession = createSessionWithConfig(
|
||||||
{ modeId: "acceptEdits", model: "opus" },
|
{ modeId: "acceptEdits", model: "opus" },
|
||||||
logger,
|
logger,
|
||||||
@@ -540,7 +541,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
|
|
||||||
test("routes config_option_update and refreshes derived mode, model, and thinking state", async () => {
|
test("routes config_option_update and refreshes derived mode, model, and thinking state", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPSessionInternals;
|
const internals = asInternals<ACPSessionInternals>(session);
|
||||||
|
|
||||||
const events = internals.translateSessionUpdate({
|
const events = internals.translateSessionUpdate({
|
||||||
sessionUpdate: "config_option_update",
|
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 () => {
|
test("keeps pushed mode when a later config_option_update has no mode payload", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPSessionInternals;
|
const internals = asInternals<ACPSessionInternals>(session);
|
||||||
|
|
||||||
internals.translateSessionUpdate({
|
internals.translateSessionUpdate({
|
||||||
sessionUpdate: "current_mode_update",
|
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 () => {
|
test("uses last writer when current_mode_update and config_option_update both include a mode", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPSessionInternals;
|
const internals = asInternals<ACPSessionInternals>(session);
|
||||||
|
|
||||||
const configEvents = internals.translateSessionUpdate({
|
const configEvents = internals.translateSessionUpdate({
|
||||||
sessionUpdate: "config_option_update",
|
sessionUpdate: "config_option_update",
|
||||||
@@ -634,7 +635,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
|
|
||||||
test("uses canonical mode returned by setSessionConfigOption response", async () => {
|
test("uses canonical mode returned by setSessionConfigOption response", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPModelSelectionInternals;
|
const internals = asInternals<ACPModelSelectionInternals>(session);
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||||
internals.sessionId = "session-1";
|
internals.sessionId = "session-1";
|
||||||
@@ -664,7 +665,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
|
|
||||||
test("uses canonical model returned by setSessionConfigOption response", async () => {
|
test("uses canonical model returned by setSessionConfigOption response", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPModelSelectionInternals;
|
const internals = asInternals<ACPModelSelectionInternals>(session);
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||||
internals.sessionId = "session-1";
|
internals.sessionId = "session-1";
|
||||||
@@ -688,7 +689,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
|
|
||||||
test("uses canonical thinking option returned by setSessionConfigOption response", async () => {
|
test("uses canonical thinking option returned by setSessionConfigOption response", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const internals = session as unknown as ACPModelSelectionInternals;
|
const internals = asInternals<ACPModelSelectionInternals>(session);
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||||
internals.sessionId = "session-1";
|
internals.sessionId = "session-1";
|
||||||
@@ -725,7 +726,7 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
|
{ 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) => {
|
session.subscribe((event) => {
|
||||||
events.push(event as { type: string; request?: { id: string } });
|
events.push(event as { type: string; request?: { id: string } });
|
||||||
});
|
});
|
||||||
@@ -869,7 +870,7 @@ describe("ACPAgentClient modelTransformer", () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
initialize: { agentCapabilities: {} },
|
initialize: { agentCapabilities: {} },
|
||||||
} as unknown as SpawnedACPProcess;
|
} as SpawnedACPProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async closeProbe(): Promise<void> {}
|
protected override async closeProbe(): Promise<void> {}
|
||||||
@@ -915,7 +916,7 @@ describe("ACPAgentClient sessionResponseTransformer", () => {
|
|||||||
newSession: vi.fn().mockResolvedValue(response),
|
newSession: vi.fn().mockResolvedValue(response),
|
||||||
},
|
},
|
||||||
initialize: { agentCapabilities: {} },
|
initialize: { agentCapabilities: {} },
|
||||||
} as unknown as SpawnedACPProcess;
|
} as SpawnedACPProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async closeProbe(): Promise<void> {}
|
protected override async closeProbe(): Promise<void> {}
|
||||||
@@ -956,7 +957,7 @@ describe("ACPAgentClient listModes", () => {
|
|||||||
child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() },
|
child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() },
|
||||||
connection: { newSession },
|
connection: { newSession },
|
||||||
initialize: { agentCapabilities: {} },
|
initialize: { agentCapabilities: {} },
|
||||||
} as unknown as SpawnedACPProcess;
|
} as SpawnedACPProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async closeProbe(): Promise<void> {}
|
protected override async closeProbe(): Promise<void> {}
|
||||||
@@ -1007,7 +1008,7 @@ describe("ACPAgentClient listModes", () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
initialize: { agentCapabilities: {} },
|
initialize: { agentCapabilities: {} },
|
||||||
} as unknown as SpawnedACPProcess;
|
} as SpawnedACPProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async closeProbe(): Promise<void> {}
|
protected override async closeProbe(): Promise<void> {}
|
||||||
@@ -1111,7 +1112,7 @@ describe("ACPAgentSession slash commands", () => {
|
|||||||
|
|
||||||
const listCommandsPromise = session.listCommands();
|
const listCommandsPromise = session.listCommands();
|
||||||
|
|
||||||
(session as unknown as ACPSessionInternals).translateSessionUpdate({
|
asInternals<ACPSessionInternals>(session).translateSessionUpdate({
|
||||||
sessionUpdate: "available_commands_update",
|
sessionUpdate: "available_commands_update",
|
||||||
availableCommands: [
|
availableCommands: [
|
||||||
{
|
{
|
||||||
@@ -1157,7 +1158,7 @@ describe("ACPAgentSession", () => {
|
|||||||
test("emits assistant and reasoning chunks as deltas while user chunks stay accumulated", async () => {
|
test("emits assistant and reasoning chunks as deltas while user chunks stay accumulated", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const events: Array<{ type: string; item?: { type: string; text?: string } }> = [];
|
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) => {
|
session.subscribe((event) => {
|
||||||
events.push(event as { type: string; item?: { type: string; text?: string } });
|
events.push(event as { type: string; item?: { type: string; text?: string } });
|
||||||
@@ -1169,7 +1170,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "agent_message_chunk",
|
sessionUpdate: "agent_message_chunk",
|
||||||
messageId: "assistant-1",
|
messageId: "assistant-1",
|
||||||
content: { type: "text", text: "Hey!" },
|
content: { type: "text", text: "Hey!" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
await session.sessionUpdate({
|
await session.sessionUpdate({
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
@@ -1177,7 +1178,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "agent_message_chunk",
|
sessionUpdate: "agent_message_chunk",
|
||||||
messageId: "assistant-1",
|
messageId: "assistant-1",
|
||||||
content: { type: "text", text: " How are you?" },
|
content: { type: "text", text: " How are you?" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
await session.sessionUpdate({
|
await session.sessionUpdate({
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
@@ -1185,7 +1186,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "agent_thought_chunk",
|
sessionUpdate: "agent_thought_chunk",
|
||||||
messageId: "thought-1",
|
messageId: "thought-1",
|
||||||
content: { type: "text", text: "Thinking" },
|
content: { type: "text", text: "Thinking" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
await session.sessionUpdate({
|
await session.sessionUpdate({
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
@@ -1193,7 +1194,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "agent_thought_chunk",
|
sessionUpdate: "agent_thought_chunk",
|
||||||
messageId: "thought-1",
|
messageId: "thought-1",
|
||||||
content: { type: "text", text: " more" },
|
content: { type: "text", text: " more" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
await session.sessionUpdate({
|
await session.sessionUpdate({
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
@@ -1201,7 +1202,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "user_message_chunk",
|
sessionUpdate: "user_message_chunk",
|
||||||
messageId: "user-1",
|
messageId: "user-1",
|
||||||
content: { type: "text", text: "hel" },
|
content: { type: "text", text: "hel" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
await session.sessionUpdate({
|
await session.sessionUpdate({
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
@@ -1209,7 +1210,7 @@ describe("ACPAgentSession", () => {
|
|||||||
sessionUpdate: "user_message_chunk",
|
sessionUpdate: "user_message_chunk",
|
||||||
messageId: "user-1",
|
messageId: "user-1",
|
||||||
content: { type: "text", text: "lo" },
|
content: { type: "text", text: "lo" },
|
||||||
} as unknown as SessionUpdate,
|
} as SessionUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeline = events
|
const timeline = events
|
||||||
@@ -1238,8 +1239,8 @@ describe("ACPAgentSession", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
(session as unknown as ACPSessionInternals).sessionId = "session-1";
|
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
|
||||||
(session as unknown as ACPSessionInternals).connection = { prompt };
|
asInternals<ACPSessionInternals>(session).connection = { prompt };
|
||||||
|
|
||||||
session.subscribe((event) => {
|
session.subscribe((event) => {
|
||||||
events.push(event as { type: string; turnId?: string });
|
events.push(event as { type: string; turnId?: string });
|
||||||
@@ -1252,7 +1253,7 @@ describe("ACPAgentSession", () => {
|
|||||||
type: "turn_started",
|
type: "turn_started",
|
||||||
turnId,
|
turnId,
|
||||||
});
|
});
|
||||||
expect((session as unknown as ACPSessionInternals).activeForegroundTurnId).toBe(turnId);
|
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBe(turnId);
|
||||||
|
|
||||||
resolvePrompt({ stopReason: "end_turn", usage: { outputTokens: 3 } });
|
resolvePrompt({ stopReason: "end_turn", usage: { outputTokens: 3 } });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -1262,7 +1263,7 @@ describe("ACPAgentSession", () => {
|
|||||||
type: "turn_completed",
|
type: "turn_completed",
|
||||||
turnId,
|
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 () => {
|
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";
|
asInternals<ACPSessionInternals>(session).sessionId = "session-1";
|
||||||
(session as unknown as ACPSessionInternals).connection = { prompt };
|
asInternals<ACPSessionInternals>(session).connection = { prompt };
|
||||||
|
|
||||||
session.subscribe((event) => {
|
session.subscribe((event) => {
|
||||||
events.push(event as { type: string; turnId?: string; error?: string });
|
events.push(event as { type: string; turnId?: string; error?: string });
|
||||||
@@ -1295,6 +1296,6 @@ describe("ACPAgentSession", () => {
|
|||||||
turnId,
|
turnId,
|
||||||
error: "prompt failed",
|
error: "prompt failed",
|
||||||
});
|
});
|
||||||
expect((session as unknown as ACPSessionInternals).activeForegroundTurnId).toBeNull();
|
expect(asInternals<ACPSessionInternals>(session).activeForegroundTurnId).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { ACPAgentSession } from "./acp-agent.js";
|
import { ACPAgentSession } from "./acp-agent.js";
|
||||||
|
import { asInternals } from "../../test-utils/class-mocks.js";
|
||||||
|
|
||||||
const smokeSelection = new Set(
|
const smokeSelection = new Set(
|
||||||
(process.env.ACP_SMOKE ?? "")
|
(process.env.ACP_SMOKE ?? "")
|
||||||
@@ -117,7 +118,7 @@ function createSmokeLogger(): SmokeLogger {
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
fatal: vi.fn(),
|
fatal: vi.fn(),
|
||||||
};
|
};
|
||||||
return logger as unknown as SmokeLogger;
|
return asInternals<SmokeLogger>(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrace(config: WrapperSmokeConfig): SmokeTrace {
|
function createTrace(config: WrapperSmokeConfig): SmokeTrace {
|
||||||
@@ -365,7 +366,7 @@ async function bootSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function captureFinalState(session: ACPAgentSession, trace: SmokeTrace): Promise<void> {
|
async function captureFinalState(session: ACPAgentSession, trace: SmokeTrace): Promise<void> {
|
||||||
const internals = session as unknown as SessionInternals;
|
const internals = asInternals<SessionInternals>(session);
|
||||||
trace.finalState = {
|
trace.finalState = {
|
||||||
runtime: await session.getRuntimeInfo(),
|
runtime: await session.getRuntimeInfo(),
|
||||||
availableModes: await session.getAvailableModes(),
|
availableModes: await session.getAvailableModes(),
|
||||||
@@ -376,7 +377,7 @@ async function captureFinalState(session: ACPAgentSession, trace: SmokeTrace): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getModelIds(session: ACPAgentSession): string[] {
|
function getModelIds(session: ACPAgentSession): string[] {
|
||||||
const internals = session as unknown as SessionInternals;
|
const internals = asInternals<SessionInternals>(session);
|
||||||
return internals.availableModels?.map((model) => model.modelId) ?? [];
|
return internals.availableModels?.map((model) => model.modelId) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +385,7 @@ function getSelectOption(
|
|||||||
session: ACPAgentSession,
|
session: ACPAgentSession,
|
||||||
category: string,
|
category: string,
|
||||||
): Extract<SessionConfigOption, { type: "select" }> | null {
|
): Extract<SessionConfigOption, { type: "select" }> | null {
|
||||||
const internals = session as unknown as SessionInternals;
|
const internals = asInternals<SessionInternals>(session);
|
||||||
return (
|
return (
|
||||||
internals.configOptions.find(
|
internals.configOptions.find(
|
||||||
(option): option is Extract<SessionConfigOption, { type: "select" }> =>
|
(option): option is Extract<SessionConfigOption, { type: "select" }> =>
|
||||||
@@ -515,7 +516,7 @@ for (const config of wrappers) {
|
|||||||
|
|
||||||
const modeAfterSuccess = await session.getCurrentMode();
|
const modeAfterSuccess = await session.getCurrentMode();
|
||||||
originalSessionId = session.id;
|
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();
|
await expect(session.setMode(mode.id)).rejects.toThrow();
|
||||||
expect(await session.getCurrentMode()).toBe(modeAfterSuccess);
|
expect(await session.getCurrentMode()).toBe(modeAfterSuccess);
|
||||||
trace.notes.push(
|
trace.notes.push(
|
||||||
@@ -523,7 +524,7 @@ for (const config of wrappers) {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (session && originalSessionId) {
|
if (session && originalSessionId) {
|
||||||
(session as unknown as { sessionId: string }).sessionId = originalSessionId;
|
asInternals<{ sessionId: string }>(session).sessionId = originalSessionId;
|
||||||
}
|
}
|
||||||
await captureFinalStateIfOpen(session, trace);
|
await captureFinalStateIfOpen(session, trace);
|
||||||
emitEvidence(`${config.id}:b`, trace);
|
emitEvidence(`${config.id}:b`, trace);
|
||||||
@@ -622,7 +623,7 @@ for (const config of wrappers) {
|
|||||||
expect(
|
expect(
|
||||||
trace.notifications.some((entry) => JSON.stringify(entry).includes("tool_call_update")),
|
trace.notifications.some((entry) => JSON.stringify(entry).includes("tool_call_update")),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect((session as unknown as SessionInternals).toolCalls.size).toBeGreaterThan(0);
|
expect(asInternals<SessionInternals>(session).toolCalls.size).toBeGreaterThan(0);
|
||||||
trace.criteria.toolSnapshot = {
|
trace.criteria.toolSnapshot = {
|
||||||
outcome: "PASS",
|
outcome: "PASS",
|
||||||
detail:
|
detail:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
import { asInternals } from "../../test-utils/class-mocks.js";
|
||||||
import { ClaudeAgentClient, readEventIdentifiers } from "./claude-agent.js";
|
import { ClaudeAgentClient, readEventIdentifiers } from "./claude-agent.js";
|
||||||
import { streamSession } from "./test-utils/session-stream-adapter.js";
|
import { streamSession } from "./test-utils/session-stream-adapter.js";
|
||||||
import type { AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
|
import type { AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
|
||||||
@@ -124,7 +125,7 @@ function createSpyLogger(): {
|
|||||||
loggerLike.child.mockReturnValue(loggerLike);
|
loggerLike.child.mockReturnValue(loggerLike);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logger: loggerLike as unknown as Logger,
|
logger: asInternals<Logger>(loggerLike),
|
||||||
calls,
|
calls,
|
||||||
debug,
|
debug,
|
||||||
info,
|
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 () => {
|
test("interruptActiveTurn only interrupts the active query without info logs", async () => {
|
||||||
const spy = createSpyLogger();
|
const spy = createSpyLogger();
|
||||||
const session = await createSessionWithLogger(spy.logger);
|
const session = await createSessionWithLogger(spy.logger);
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
query: {
|
query: {
|
||||||
interrupt: () => Promise<void>;
|
interrupt: () => Promise<void>;
|
||||||
return?: () => Promise<void>;
|
return?: () => Promise<void>;
|
||||||
@@ -270,7 +271,7 @@ test("interruptActiveTurn only interrupts the active query without info logs", a
|
|||||||
input: { end: () => void } | null;
|
input: { end: () => void } | null;
|
||||||
queryRestartNeeded: boolean;
|
queryRestartNeeded: boolean;
|
||||||
interruptActiveTurn: () => Promise<void>;
|
interruptActiveTurn: () => Promise<void>;
|
||||||
};
|
} = asInternals(session);
|
||||||
const interrupt = vi.fn(async () => undefined);
|
const interrupt = vi.fn(async () => undefined);
|
||||||
const queryReturn = vi.fn(async () => undefined);
|
const queryReturn = vi.fn(async () => undefined);
|
||||||
const end = vi.fn(() => undefined);
|
const end = vi.fn(() => undefined);
|
||||||
@@ -383,7 +384,7 @@ test("extracts identifiers from fixture-driven protocol shape variants", () => {
|
|||||||
for (const fixture of fixtures) {
|
for (const fixture of fixtures) {
|
||||||
expect(
|
expect(
|
||||||
readEventIdentifiers(
|
readEventIdentifiers(
|
||||||
fixture.message as unknown as Parameters<typeof readEventIdentifiers>[0],
|
asInternals<Parameters<typeof readEventIdentifiers>[0]>(fixture.message),
|
||||||
),
|
),
|
||||||
).toEqual(fixture.expected);
|
).toEqual(fixture.expected);
|
||||||
}
|
}
|
||||||
@@ -411,12 +412,12 @@ test("captures session IDs from fixture-driven init message variants", async ()
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
fixtures.map(async (fixture) => {
|
fixtures.map(async (fixture) => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
handleSystemMessage: (message: Record<string, unknown>) => {
|
handleSystemMessage: (message: Record<string, unknown>) => {
|
||||||
threadStartedSessionId: string | null;
|
threadStartedSessionId: string | null;
|
||||||
notice: AgentTimelineItem | null;
|
notice: AgentTimelineItem | null;
|
||||||
};
|
};
|
||||||
};
|
} = asInternals(session);
|
||||||
try {
|
try {
|
||||||
const started = internal.handleSystemMessage({
|
const started = internal.handleSystemMessage({
|
||||||
type: "system",
|
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 () => {
|
test("waits for complete JSON values before updating tool input from input_json_delta", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
|
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
|
||||||
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
|
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
|
||||||
toolUseIndexToId: Map<number, string>;
|
toolUseIndexToId: Map<number, string>;
|
||||||
toolUseInputBuffers: Map<string, string>;
|
toolUseInputBuffers: Map<string, string>;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
const toolUseId = "tool-input-delta";
|
const toolUseId = "tool-input-delta";
|
||||||
const index = 7;
|
const index = 7;
|
||||||
@@ -501,7 +502,7 @@ test("waits for complete JSON values before updating tool input from input_json_
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const fixture of deltaFixtures) {
|
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);
|
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 () => {
|
test("does not surface incomplete string values from input_json_delta", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
|
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
|
||||||
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
|
toolUseCache: Map<string, { input?: Record<string, unknown> }>;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
const toolUseId = "tool-input-preview";
|
const toolUseId = "tool-input-preview";
|
||||||
const index = 8;
|
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 () => {
|
test("maps tool_result content shapes into deterministic string output", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
buildToolOutput: (
|
buildToolOutput: (
|
||||||
block: Record<string, unknown>,
|
block: Record<string, unknown>,
|
||||||
entry: Record<string, unknown> | undefined,
|
entry: Record<string, unknown> | undefined,
|
||||||
) => Record<string, unknown> | undefined;
|
) => Record<string, unknown> | undefined;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
const toolEntry = {
|
const toolEntry = {
|
||||||
id: "tool-1",
|
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 () => {
|
test("Grep tool_result string content flows to a search detail with content", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
buildToolOutput: (
|
buildToolOutput: (
|
||||||
block: Record<string, unknown>,
|
block: Record<string, unknown>,
|
||||||
entry: Record<string, unknown> | undefined,
|
entry: Record<string, unknown> | undefined,
|
||||||
) => Record<string, unknown> | undefined;
|
) => Record<string, unknown> | undefined;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
const grepEntry = {
|
const grepEntry = {
|
||||||
id: "tool-grep-1",
|
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("bypassPermissions");
|
||||||
await session.setMode("plan");
|
await session.setMode("plan");
|
||||||
|
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
handlePermissionRequest: (
|
handlePermissionRequest: (
|
||||||
toolName: string,
|
toolName: string,
|
||||||
input: Record<string, unknown>,
|
input: Record<string, unknown>,
|
||||||
options: Record<string, unknown>,
|
options: Record<string, unknown>,
|
||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
const pendingResolution = internal.handlePermissionRequest(
|
const pendingResolution = internal.handlePermissionRequest(
|
||||||
"ExitPlanMode",
|
"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 () => {
|
test("reuses one autonomous run for unbound stream_event bursts with no foreground run", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal: {
|
||||||
turnState: "idle" | "foreground" | "autonomous";
|
turnState: "idle" | "foreground" | "autonomous";
|
||||||
nextTurnOrdinal: number;
|
nextTurnOrdinal: number;
|
||||||
routeSdkMessageFromPump: (message: Record<string, unknown>) => void;
|
routeSdkMessageFromPump: (message: Record<string, unknown>) => void;
|
||||||
autonomousTurn: { id: string } | null;
|
autonomousTurn: { id: string } | null;
|
||||||
};
|
} = asInternals(session);
|
||||||
|
|
||||||
internal.turnState = "idle";
|
internal.turnState = "idle";
|
||||||
internal.routeSdkMessageFromPump({
|
internal.routeSdkMessageFromPump({
|
||||||
@@ -1352,9 +1353,9 @@ test("does not use stream_event uuid as assistant message identity when message_
|
|||||||
|
|
||||||
expect(assistantText).toContain("HELLO WORLD");
|
expect(assistantText).toContain("HELLO WORLD");
|
||||||
|
|
||||||
const assembler = session as unknown as {
|
const assembler: {
|
||||||
timelineAssembler: { messages: Map<string, unknown> };
|
timelineAssembler: { messages: Map<string, unknown> };
|
||||||
};
|
} = asInternals(session);
|
||||||
expect(assembler.timelineAssembler.messages.size).toBe(0);
|
expect(assembler.timelineAssembler.messages.size).toBe(0);
|
||||||
|
|
||||||
await session.close();
|
await session.close();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
codexAppServerTurnInputFromPrompt,
|
codexAppServerTurnInputFromPrompt,
|
||||||
} from "./codex-app-server-agent.js";
|
} from "./codex-app-server-agent.js";
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
import { asInternals as castInternals, createStub } from "../../test-utils/class-mocks.js";
|
||||||
|
|
||||||
interface CollaborationModeRecord {
|
interface CollaborationModeRecord {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -71,7 +72,7 @@ function createSession(configOverrides: Partial<AgentSessionConfig> = {}): Codex
|
|||||||
() => {
|
() => {
|
||||||
throw new Error("Test session cannot spawn Codex app-server");
|
throw new Error("Test session cannot spawn Codex app-server");
|
||||||
},
|
},
|
||||||
) as unknown as CodexTestSession;
|
) as CodexTestSession;
|
||||||
session.connected = true;
|
session.connected = true;
|
||||||
session.currentThreadId = "test-thread";
|
session.currentThreadId = "test-thread";
|
||||||
session.activeForegroundTurnId = "test-turn";
|
session.activeForegroundTurnId = "test-turn";
|
||||||
@@ -79,7 +80,7 @@ function createSession(configOverrides: Partial<AgentSessionConfig> = {}): Codex
|
|||||||
}
|
}
|
||||||
|
|
||||||
function asInternals(session: CodexTestSession): CodexSessionTestAccess {
|
function asInternals(session: CodexTestSession): CodexSessionTestAccess {
|
||||||
return session as unknown as CodexSessionTestAccess;
|
return castInternals<CodexSessionTestAccess>(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Codex app-server provider", () => {
|
describe("Codex app-server provider", () => {
|
||||||
@@ -105,9 +106,9 @@ describe("Codex app-server provider", () => {
|
|||||||
{},
|
{},
|
||||||
true,
|
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");
|
const startCall = requests.find((req) => req.method === "thread/start");
|
||||||
expect(startCall).toBeDefined();
|
expect(startCall).toBeDefined();
|
||||||
@@ -134,9 +135,9 @@ describe("Codex app-server provider", () => {
|
|||||||
throw new Error("Test session cannot spawn Codex app-server");
|
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");
|
const startCall = requests.find((req) => req.method === "thread/start");
|
||||||
expect(startCall).toBeDefined();
|
expect(startCall).toBeDefined();
|
||||||
@@ -349,7 +350,7 @@ describe("Codex app-server provider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
session.activeForegroundTurnId = null;
|
session.activeForegroundTurnId = null;
|
||||||
session.client = { request } as unknown as CodexClientLike;
|
session.client = createStub<CodexClientLike>({ request });
|
||||||
|
|
||||||
await session.startTurn("Return JSON", {
|
await session.startTurn("Return JSON", {
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
@@ -405,7 +406,7 @@ describe("Codex app-server provider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
session.activeForegroundTurnId = null;
|
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");
|
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.activeForegroundTurnId = null;
|
||||||
session.client = { request } as unknown as CodexClientLike;
|
session.client = createStub<CodexClientLike>({ request });
|
||||||
|
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
session.subscribe((event) => events.push(event));
|
session.subscribe((event) => events.push(event));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
import { Session, type SessionOptions } from "./session.js";
|
import { Session, type SessionOptions } from "./session.js";
|
||||||
|
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||||
import type {
|
import type {
|
||||||
WorkspaceGitListener,
|
WorkspaceGitListener,
|
||||||
WorkspaceGitRuntimeSnapshot,
|
WorkspaceGitRuntimeSnapshot,
|
||||||
@@ -150,20 +151,20 @@ function createSessionForWorkspaceGitWatchTests(): {
|
|||||||
const session = new Session({
|
const session = new Session({
|
||||||
clientId: "test-client",
|
clientId: "test-client",
|
||||||
onMessage: (message) => emitted.push(message as { type: string; payload: unknown }),
|
onMessage: (message) => emitted.push(message as { type: string; payload: unknown }),
|
||||||
logger: logger as unknown as pino.Logger,
|
logger: createStub<pino.Logger>(logger),
|
||||||
downloadTokenStore: {} as SessionOptions["downloadTokenStore"],
|
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
|
||||||
pushTokenStore: {} as SessionOptions["pushTokenStore"],
|
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
|
||||||
paseoHome: "/tmp/paseo-test",
|
paseoHome: "/tmp/paseo-test",
|
||||||
agentManager: {
|
agentManager: createStub<SessionOptions["agentManager"]>({
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
listAgents: () => [],
|
listAgents: () => [],
|
||||||
getAgent: () => null,
|
getAgent: () => null,
|
||||||
} as unknown as SessionOptions["agentManager"],
|
}),
|
||||||
agentStorage: {
|
agentStorage: createStub<SessionOptions["agentStorage"]>({
|
||||||
list: async () => [],
|
list: async () => [],
|
||||||
get: async () => null,
|
get: async () => null,
|
||||||
} as unknown as SessionOptions["agentStorage"],
|
}),
|
||||||
projectRegistry: {
|
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
|
||||||
initialize: async () => {},
|
initialize: async () => {},
|
||||||
existsOnDisk: async () => true,
|
existsOnDisk: async () => true,
|
||||||
list: async () => Array.from(projects.values()),
|
list: async () => Array.from(projects.values()),
|
||||||
@@ -179,8 +180,8 @@ function createSessionForWorkspaceGitWatchTests(): {
|
|||||||
remove: async (projectId: string) => {
|
remove: async (projectId: string) => {
|
||||||
projects.delete(projectId);
|
projects.delete(projectId);
|
||||||
},
|
},
|
||||||
} as unknown as SessionOptions["projectRegistry"],
|
}),
|
||||||
workspaceRegistry: {
|
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
|
||||||
initialize: async () => {},
|
initialize: async () => {},
|
||||||
existsOnDisk: async () => true,
|
existsOnDisk: async () => true,
|
||||||
list: async () => Array.from(workspaces.values()),
|
list: async () => Array.from(workspaces.values()),
|
||||||
@@ -196,8 +197,8 @@ function createSessionForWorkspaceGitWatchTests(): {
|
|||||||
remove: async (workspaceId: string) => {
|
remove: async (workspaceId: string) => {
|
||||||
workspaces.delete(workspaceId);
|
workspaces.delete(workspaceId);
|
||||||
},
|
},
|
||||||
} as unknown as SessionOptions["workspaceRegistry"],
|
}),
|
||||||
checkoutDiffManager: {
|
checkoutDiffManager: createStub<SessionOptions["checkoutDiffManager"]>({
|
||||||
subscribe: async () => ({
|
subscribe: async () => ({
|
||||||
initial: { cwd: "/tmp", files: [], error: null },
|
initial: { cwd: "/tmp", files: [], error: null },
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
@@ -210,15 +211,15 @@ function createSessionForWorkspaceGitWatchTests(): {
|
|||||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||||
}),
|
}),
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
} as unknown as SessionOptions["checkoutDiffManager"],
|
}),
|
||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
mcpBaseUrl: null,
|
mcpBaseUrl: null,
|
||||||
stt: null,
|
stt: null,
|
||||||
tts: null,
|
tts: null,
|
||||||
terminalManager: null,
|
terminalManager: null,
|
||||||
} as unknown as SessionOptions);
|
});
|
||||||
|
|
||||||
(session as unknown as SessionInternals).listAgentPayloads = async () => [];
|
asInternals<SessionInternals>(session).listAgentPayloads = async () => [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
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 () => {
|
test("emits one workspace_update when the workspace git service emits a changed snapshot", async () => {
|
||||||
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
|
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
|
||||||
createSessionForWorkspaceGitWatchTests();
|
createSessionForWorkspaceGitWatchTests();
|
||||||
const sessionAny = session as unknown as SessionInternals;
|
const sessionAny = asInternals<SessionInternals>(session);
|
||||||
seedGitWorkspace({
|
seedGitWorkspace({
|
||||||
projects,
|
projects,
|
||||||
workspaces,
|
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 () => {
|
test("emits checkout_status_update to a client subscribed to the workspace git target", async () => {
|
||||||
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
|
const { session, emitted, projects, workspaces, workspaceGitService, subscriptions } =
|
||||||
createSessionForWorkspaceGitWatchTests();
|
createSessionForWorkspaceGitWatchTests();
|
||||||
const sessionAny = session as unknown as SessionInternals;
|
const sessionAny = asInternals<SessionInternals>(session);
|
||||||
seedGitWorkspace({
|
seedGitWorkspace({
|
||||||
projects,
|
projects,
|
||||||
workspaces,
|
workspaces,
|
||||||
@@ -405,7 +406,7 @@ describe("workspace git watch targets", () => {
|
|||||||
test("embeds PR status in checkout_status_update for GitHub-inclusive snapshot pushes", async () => {
|
test("embeds PR status in checkout_status_update for GitHub-inclusive snapshot pushes", async () => {
|
||||||
const { session, emitted, projects, workspaces, subscriptions } =
|
const { session, emitted, projects, workspaces, subscriptions } =
|
||||||
createSessionForWorkspaceGitWatchTests();
|
createSessionForWorkspaceGitWatchTests();
|
||||||
const sessionAny = session as unknown as SessionInternals;
|
const sessionAny = asInternals<SessionInternals>(session);
|
||||||
seedGitWorkspace({
|
seedGitWorkspace({
|
||||||
projects,
|
projects,
|
||||||
workspaces,
|
workspaces,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { expect, test, vi } from "vitest";
|
|||||||
import { Session, type SessionOptions } from "./session.js";
|
import { Session, type SessionOptions } from "./session.js";
|
||||||
import type { SessionOutboundMessage } from "../shared/messages.js";
|
import type { SessionOutboundMessage } from "../shared/messages.js";
|
||||||
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
|
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
|
||||||
|
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||||
import {
|
import {
|
||||||
createPersistedProjectRecord,
|
createPersistedProjectRecord,
|
||||||
createPersistedWorkspaceRecord,
|
createPersistedWorkspaceRecord,
|
||||||
@@ -87,11 +88,11 @@ function createHarness(input: {
|
|||||||
clientId: "test",
|
clientId: "test",
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
onMessage: (m) => emitted.push(m),
|
onMessage: (m) => emitted.push(m),
|
||||||
logger: logger as unknown as SessionOptions["logger"],
|
logger: createStub<SessionOptions["logger"]>(logger),
|
||||||
downloadTokenStore: {} as unknown as SessionOptions["downloadTokenStore"],
|
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
|
||||||
pushTokenStore: {} as unknown as SessionOptions["pushTokenStore"],
|
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
|
||||||
paseoHome: mkdtempSync(path.join(tmpdir(), "paseo-invariant-test-")),
|
paseoHome: mkdtempSync(path.join(tmpdir(), "paseo-invariant-test-")),
|
||||||
agentManager: {
|
agentManager: createStub<SessionOptions["agentManager"]>({
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
listAgents: () => [],
|
listAgents: () => [],
|
||||||
getAgent: () => null,
|
getAgent: () => null,
|
||||||
@@ -99,12 +100,12 @@ function createHarness(input: {
|
|||||||
archiveSnapshot: async () => ({}),
|
archiveSnapshot: async () => ({}),
|
||||||
clearAgentAttention: async () => {},
|
clearAgentAttention: async () => {},
|
||||||
notifyAgentState: () => {},
|
notifyAgentState: () => {},
|
||||||
} as unknown as SessionOptions["agentManager"],
|
}),
|
||||||
agentStorage: {
|
agentStorage: createStub<SessionOptions["agentStorage"]>({
|
||||||
list: async () => [],
|
list: async () => [],
|
||||||
get: async () => null,
|
get: async () => null,
|
||||||
} as unknown as SessionOptions["agentStorage"],
|
}),
|
||||||
projectRegistry: {
|
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
|
||||||
initialize: async () => {},
|
initialize: async () => {},
|
||||||
existsOnDisk: async () => true,
|
existsOnDisk: async () => true,
|
||||||
list: async () => Array.from(projects.values()),
|
list: async () => Array.from(projects.values()),
|
||||||
@@ -119,8 +120,8 @@ function createHarness(input: {
|
|||||||
remove: async (id: string) => {
|
remove: async (id: string) => {
|
||||||
projects.delete(id);
|
projects.delete(id);
|
||||||
},
|
},
|
||||||
} as unknown as SessionOptions["projectRegistry"],
|
}),
|
||||||
workspaceRegistry: {
|
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
|
||||||
initialize: async () => {},
|
initialize: async () => {},
|
||||||
existsOnDisk: async () => true,
|
existsOnDisk: async () => true,
|
||||||
list: async () => Array.from(workspaces.values()),
|
list: async () => Array.from(workspaces.values()),
|
||||||
@@ -135,11 +136,11 @@ function createHarness(input: {
|
|||||||
remove: async (id: string) => {
|
remove: async (id: string) => {
|
||||||
workspaces.delete(id);
|
workspaces.delete(id);
|
||||||
},
|
},
|
||||||
} as unknown as SessionOptions["workspaceRegistry"],
|
}),
|
||||||
chatService: {} as unknown as SessionOptions["chatService"],
|
chatService: createStub<SessionOptions["chatService"]>({}),
|
||||||
scheduleService: {} as unknown as SessionOptions["scheduleService"],
|
scheduleService: createStub<SessionOptions["scheduleService"]>({}),
|
||||||
loopService: {} as unknown as SessionOptions["loopService"],
|
loopService: createStub<SessionOptions["loopService"]>({}),
|
||||||
checkoutDiffManager: {
|
checkoutDiffManager: createStub<SessionOptions["checkoutDiffManager"]>({
|
||||||
subscribe: async () => ({
|
subscribe: async () => ({
|
||||||
initial: { cwd: "/tmp", files: [], error: null },
|
initial: { cwd: "/tmp", files: [], error: null },
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
@@ -152,12 +153,12 @@ function createHarness(input: {
|
|||||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||||
}),
|
}),
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
} as unknown as SessionOptions["checkoutDiffManager"],
|
}),
|
||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
daemonConfigStore: {
|
daemonConfigStore: createStub<SessionOptions["daemonConfigStore"]>({
|
||||||
get: () => ({ mcp: { injectIntoAgents: false }, providers: {} }),
|
get: () => ({ mcp: { injectIntoAgents: false }, providers: {} }),
|
||||||
onChange: () => () => {},
|
onChange: () => () => {},
|
||||||
} as unknown as SessionOptions["daemonConfigStore"],
|
}),
|
||||||
mcpBaseUrl: null,
|
mcpBaseUrl: null,
|
||||||
stt: null,
|
stt: null,
|
||||||
tts: null,
|
tts: null,
|
||||||
@@ -168,7 +169,7 @@ function createHarness(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openProject(session: Session, cwd: string, requestId = "req-1") {
|
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",
|
type: "open_project_request",
|
||||||
cwd,
|
cwd,
|
||||||
requestId,
|
requestId,
|
||||||
@@ -436,11 +437,9 @@ test("S12: findWorkspaceByDirectory does not return archived ancestor via prefix
|
|||||||
workspaces: [dirWorkspace("/parent", archivedAt)],
|
workspaces: [dirWorkspace("/parent", archivedAt)],
|
||||||
projects: [dirProject("/parent", archivedAt)],
|
projects: [dirProject("/parent", archivedAt)],
|
||||||
});
|
});
|
||||||
const found = await (
|
const found = await asInternals<{
|
||||||
h.session as unknown as {
|
findWorkspaceByDirectory(cwd: string): Promise<unknown>;
|
||||||
findWorkspaceByDirectory(cwd: string): Promise<unknown>;
|
}>(h.session).findWorkspaceByDirectory("/parent/child");
|
||||||
}
|
|
||||||
).findWorkspaceByDirectory("/parent/child");
|
|
||||||
expect(found).toBeNull();
|
expect(found).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, test, vi } from "vitest";
|
|||||||
import { Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import type { SessionOptions } from "./session.js";
|
import type { SessionOptions } from "./session.js";
|
||||||
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
||||||
|
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||||
|
|
||||||
interface SessionInternals {
|
interface SessionInternals {
|
||||||
archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>;
|
archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>;
|
||||||
@@ -90,51 +91,53 @@ describe("snapshot mutation ownership boundary", () => {
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const session = new Session({
|
const session = asInternals<SessionInternals>(
|
||||||
clientId: "test-client",
|
new Session({
|
||||||
onMessage,
|
clientId: "test-client",
|
||||||
logger: logger as unknown as SessionOptions["logger"],
|
onMessage,
|
||||||
downloadTokenStore: {} as unknown as SessionOptions["downloadTokenStore"],
|
logger: createStub<SessionOptions["logger"]>(logger),
|
||||||
pushTokenStore: {} as unknown as SessionOptions["pushTokenStore"],
|
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
|
||||||
paseoHome: "/tmp/paseo-test",
|
pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}),
|
||||||
agentManager: {
|
paseoHome: "/tmp/paseo-test",
|
||||||
subscribe: () => () => {},
|
agentManager: createStub<SessionOptions["agentManager"]>({
|
||||||
listAgents: () => [],
|
subscribe: () => () => {},
|
||||||
getAgent: () => null,
|
listAgents: () => [],
|
||||||
archiveSnapshot,
|
getAgent: () => null,
|
||||||
updateAgentMetadata,
|
archiveSnapshot,
|
||||||
} as unknown as SessionOptions["agentManager"],
|
updateAgentMetadata,
|
||||||
agentStorage: {
|
}),
|
||||||
list: async () => [],
|
agentStorage: createStub<SessionOptions["agentStorage"]>({
|
||||||
get: async () => storedRecord,
|
list: async () => [],
|
||||||
applySnapshot: directStorageWrite,
|
get: async () => storedRecord,
|
||||||
upsert: directStorageWrite,
|
applySnapshot: directStorageWrite,
|
||||||
} as unknown as SessionOptions["agentStorage"],
|
upsert: directStorageWrite,
|
||||||
projectRegistry: {
|
}),
|
||||||
initialize: async () => {},
|
projectRegistry: createStub<SessionOptions["projectRegistry"]>({
|
||||||
existsOnDisk: async () => true,
|
initialize: async () => {},
|
||||||
list: async () => [],
|
existsOnDisk: async () => true,
|
||||||
get: async () => null,
|
list: async () => [],
|
||||||
upsert: async () => {},
|
get: async () => null,
|
||||||
archive: async () => {},
|
upsert: async () => {},
|
||||||
remove: async () => {},
|
archive: async () => {},
|
||||||
} as unknown as SessionOptions["projectRegistry"],
|
remove: async () => {},
|
||||||
workspaceRegistry: {
|
}),
|
||||||
initialize: async () => {},
|
workspaceRegistry: createStub<SessionOptions["workspaceRegistry"]>({
|
||||||
existsOnDisk: async () => true,
|
initialize: async () => {},
|
||||||
list: async () => [],
|
existsOnDisk: async () => true,
|
||||||
get: async () => null,
|
list: async () => [],
|
||||||
upsert: async () => {},
|
get: async () => null,
|
||||||
archive: async () => {},
|
upsert: async () => {},
|
||||||
remove: async () => {},
|
archive: async () => {},
|
||||||
} as unknown as SessionOptions["workspaceRegistry"],
|
remove: async () => {},
|
||||||
createAgentMcpTransport: async () => {
|
}),
|
||||||
throw new Error("not used");
|
createAgentMcpTransport: async () => {
|
||||||
},
|
throw new Error("not used");
|
||||||
stt: null,
|
},
|
||||||
tts: null,
|
stt: null,
|
||||||
terminalManager: null,
|
tts: null,
|
||||||
}) as unknown as SessionInternals;
|
terminalManager: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const archiveResult = await session.archiveAgentForClose("agent-1");
|
const archiveResult = await session.archiveAgentForClose("agent-1");
|
||||||
expect(archiveSnapshot).toHaveBeenCalledTimes(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 { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js";
|
||||||
import type { SessionOptions } from "../session.js";
|
import type { SessionOptions } from "../session.js";
|
||||||
import type { SessionOutboundMessage } from "../../shared/messages.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"] {
|
export function asSessionLogger(stub: {
|
||||||
return stub as unknown as SessionOptions["logger"];
|
[K in keyof SessionOptions["logger"]]?: unknown;
|
||||||
|
}): SessionOptions["logger"] {
|
||||||
|
return createStub<SessionOptions["logger"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asAgentManager(stub: object): SessionOptions["agentManager"] {
|
export function asAgentManager(stub: {
|
||||||
return stub as unknown as SessionOptions["agentManager"];
|
[K in keyof SessionOptions["agentManager"]]?: unknown;
|
||||||
|
}): SessionOptions["agentManager"] {
|
||||||
|
return createStub<SessionOptions["agentManager"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asAgentStorage(stub: object): SessionOptions["agentStorage"] {
|
export function asAgentStorage(stub: {
|
||||||
return stub as unknown as SessionOptions["agentStorage"];
|
[K in keyof SessionOptions["agentStorage"]]?: unknown;
|
||||||
|
}): SessionOptions["agentStorage"] {
|
||||||
|
return createStub<SessionOptions["agentStorage"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asDownloadTokenStore(): SessionOptions["downloadTokenStore"] {
|
export function asDownloadTokenStore(): SessionOptions["downloadTokenStore"] {
|
||||||
return {} as unknown as SessionOptions["downloadTokenStore"];
|
return createStub<SessionOptions["downloadTokenStore"]>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asPushTokenStore(): SessionOptions["pushTokenStore"] {
|
export function asPushTokenStore(): SessionOptions["pushTokenStore"] {
|
||||||
return {} as unknown as SessionOptions["pushTokenStore"];
|
return createStub<SessionOptions["pushTokenStore"]>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asChatService(): SessionOptions["chatService"] {
|
export function asChatService(): SessionOptions["chatService"] {
|
||||||
return {} as unknown as SessionOptions["chatService"];
|
return createStub<SessionOptions["chatService"]>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asScheduleService(): SessionOptions["scheduleService"] {
|
export function asScheduleService(): SessionOptions["scheduleService"] {
|
||||||
return {} as unknown as SessionOptions["scheduleService"];
|
return createStub<SessionOptions["scheduleService"]>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asLoopService(): SessionOptions["loopService"] {
|
export function asLoopService(): SessionOptions["loopService"] {
|
||||||
return {} as unknown as SessionOptions["loopService"];
|
return createStub<SessionOptions["loopService"]>({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asCheckoutDiffManager(stub: object): SessionOptions["checkoutDiffManager"] {
|
export function asCheckoutDiffManager(stub: {
|
||||||
return stub as unknown as SessionOptions["checkoutDiffManager"];
|
[K in keyof SessionOptions["checkoutDiffManager"]]?: unknown;
|
||||||
|
}): SessionOptions["checkoutDiffManager"] {
|
||||||
|
return createStub<SessionOptions["checkoutDiffManager"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asDaemonConfigStore(stub: object): SessionOptions["daemonConfigStore"] {
|
export function asDaemonConfigStore(stub: {
|
||||||
return stub as unknown as SessionOptions["daemonConfigStore"];
|
[K in keyof SessionOptions["daemonConfigStore"]]?: unknown;
|
||||||
|
}): SessionOptions["daemonConfigStore"] {
|
||||||
|
return createStub<SessionOptions["daemonConfigStore"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asTerminalManager(stub: object): NonNullable<SessionOptions["terminalManager"]> {
|
export function asTerminalManager(stub: {
|
||||||
return stub as unknown as NonNullable<SessionOptions["terminalManager"]>;
|
[K in keyof NonNullable<SessionOptions["terminalManager"]>]?: unknown;
|
||||||
|
}): NonNullable<SessionOptions["terminalManager"]> {
|
||||||
|
return createStub<NonNullable<SessionOptions["terminalManager"]>>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asGitHubService(stub: object): NonNullable<SessionOptions["github"]> {
|
export function asGitHubService(stub: {
|
||||||
return stub as unknown as NonNullable<SessionOptions["github"]>;
|
[K in keyof NonNullable<SessionOptions["github"]>]?: unknown;
|
||||||
|
}): NonNullable<SessionOptions["github"]> {
|
||||||
|
return createStub<NonNullable<SessionOptions["github"]>>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asWorkspaceGitService(stub: object): SessionOptions["workspaceGitService"] {
|
export function asWorkspaceGitService(stub: {
|
||||||
return stub as unknown as SessionOptions["workspaceGitService"];
|
[K in keyof SessionOptions["workspaceGitService"]]?: unknown;
|
||||||
|
}): SessionOptions["workspaceGitService"] {
|
||||||
|
return createStub<SessionOptions["workspaceGitService"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asScriptRouteStore(stub: object): SessionOptions["scriptRouteStore"] {
|
export function asScriptRouteStore(stub: {
|
||||||
return stub as unknown as SessionOptions["scriptRouteStore"];
|
[K in keyof SessionOptions["scriptRouteStore"]]?: unknown;
|
||||||
|
}): SessionOptions["scriptRouteStore"] {
|
||||||
|
return createStub<SessionOptions["scriptRouteStore"]>(stub);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asWorkspaceScriptRuntimeStore(stub: object): SessionOptions["scriptRuntimeStore"] {
|
export function asWorkspaceScriptRuntimeStore(stub: {
|
||||||
return stub as unknown as SessionOptions["scriptRuntimeStore"];
|
[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 {
|
export { asInternals as asSessionInternals };
|
||||||
return session as unknown as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Type guard for SessionOutboundMessage — avoids casting unknown in test emit overrides
|
// Type guard for SessionOutboundMessage — avoids casting unknown in test emit overrides
|
||||||
@@ -134,7 +155,7 @@ export function createProviderSnapshotManagerStub(): {
|
|||||||
};
|
};
|
||||||
on.mockImplementation(() => stub);
|
on.mockImplementation(() => stub);
|
||||||
off.mockImplementation(() => stub);
|
off.mockImplementation(() => stub);
|
||||||
const manager = stub as unknown as ProviderSnapshotManager;
|
const manager = createStub<ProviderSnapshotManager>(stub);
|
||||||
return {
|
return {
|
||||||
manager,
|
manager,
|
||||||
getSnapshot,
|
getSnapshot,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { FileBackedChatService } from "./chat/chat-service.js";
|
|||||||
import type { LoopService } from "./loop-service.js";
|
import type { LoopService } from "./loop-service.js";
|
||||||
import type { ScheduleService } from "./schedule/service.js";
|
import type { ScheduleService } from "./schedule/service.js";
|
||||||
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||||
|
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||||
|
|
||||||
const wsModuleMock = vi.hoisted(() => {
|
const wsModuleMock = vi.hoisted(() => {
|
||||||
class MockWebSocketServer {
|
class MockWebSocketServer {
|
||||||
@@ -92,14 +93,14 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const server = new VoiceAssistantWebSocketServer(
|
const server = new VoiceAssistantWebSocketServer(
|
||||||
{} as unknown as HTTPServer,
|
createStub<HTTPServer>({}),
|
||||||
createLogger() as unknown as pino.Logger,
|
createStub<pino.Logger>(createLogger()),
|
||||||
"srv-test",
|
"srv-test",
|
||||||
agentManager as unknown as AgentManager,
|
createStub<AgentManager>(agentManager),
|
||||||
{} as unknown as AgentStorage,
|
createStub<AgentStorage>({}),
|
||||||
{} as unknown as DownloadTokenStore,
|
createStub<DownloadTokenStore>({}),
|
||||||
"/tmp/paseo-test",
|
"/tmp/paseo-test",
|
||||||
daemonConfigStore as unknown as DaemonConfigStore,
|
createStub<DaemonConfigStore>(daemonConfigStore),
|
||||||
null,
|
null,
|
||||||
{ allowedOrigins: new Set() },
|
{ allowedOrigins: new Set() },
|
||||||
undefined,
|
undefined,
|
||||||
@@ -113,10 +114,10 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{} as unknown as FileBackedChatService,
|
createStub<FileBackedChatService>({}),
|
||||||
{} as unknown as LoopService,
|
createStub<LoopService>({}),
|
||||||
{} as unknown as ScheduleService,
|
createStub<ScheduleService>({}),
|
||||||
{
|
createStub<CheckoutDiffManager>({
|
||||||
subscribe: vi.fn(),
|
subscribe: vi.fn(),
|
||||||
scheduleRefreshForCwd: vi.fn(),
|
scheduleRefreshForCwd: vi.fn(),
|
||||||
getMetrics: vi.fn(() => ({
|
getMetrics: vi.fn(() => ({
|
||||||
@@ -126,7 +127,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
|||||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||||
})),
|
})),
|
||||||
dispose: vi.fn(),
|
dispose: vi.fn(),
|
||||||
} as unknown as CheckoutDiffManager,
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { server, agentManager };
|
return { server, agentManager };
|
||||||
@@ -167,7 +168,7 @@ function connectClient(
|
|||||||
} | null,
|
} | null,
|
||||||
) {
|
) {
|
||||||
const ws = createOpenSocket();
|
const ws = createOpenSocket();
|
||||||
(server as unknown as WebSocketServerInternals).sessions.set(ws, {
|
asInternals<WebSocketServerInternals>(server).sessions.set(ws, {
|
||||||
session: createSessionWithActivity(activity),
|
session: createSessionWithActivity(activity),
|
||||||
clientId: "client-test",
|
clientId: "client-test",
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
@@ -207,7 +208,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
|
|||||||
getLastAssistantMessage,
|
getLastAssistantMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
|
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
|
||||||
agentId: "agent-1",
|
agentId: "agent-1",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
reason: "finished",
|
reason: "finished",
|
||||||
@@ -237,7 +238,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
|
|||||||
getLastAssistantMessage,
|
getLastAssistantMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
|
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
|
||||||
agentId: "agent-2",
|
agentId: "agent-2",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
reason: "finished",
|
reason: "finished",
|
||||||
@@ -263,7 +264,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
|
|||||||
lastActivityAt: new Date(nowMs - 300_000),
|
lastActivityAt: new Date(nowMs - 300_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
|
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
|
||||||
agentId: "agent-X",
|
agentId: "agent-X",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
reason: "finished",
|
reason: "finished",
|
||||||
@@ -278,7 +279,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
|
|||||||
const { server } = createServer();
|
const { server } = createServer();
|
||||||
const ws = connectClient(server, null);
|
const ws = connectClient(server, null);
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
|
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
|
||||||
agentId: "agent-no-heartbeat",
|
agentId: "agent-no-heartbeat",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
reason: "finished",
|
reason: "finished",
|
||||||
@@ -292,7 +293,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
|
|||||||
const { server } = createServer();
|
const { server } = createServer();
|
||||||
const ws = connectClient(server, null);
|
const ws = connectClient(server, null);
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).broadcastAgentAttention({
|
await asInternals<WebSocketServerInternals>(server).broadcastAgentAttention({
|
||||||
agentId: "agent-no-heartbeat",
|
agentId: "agent-no-heartbeat",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
reason: "error",
|
reason: "error",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { FileBackedChatService } from "./chat/chat-service.js";
|
|||||||
import type { LoopService } from "./loop-service.js";
|
import type { LoopService } from "./loop-service.js";
|
||||||
import type { ScheduleService } from "./schedule/service.js";
|
import type { ScheduleService } from "./schedule/service.js";
|
||||||
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||||
|
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||||
import {
|
import {
|
||||||
asUint8Array,
|
asUint8Array,
|
||||||
decodeTerminalStreamFrame,
|
decodeTerminalStreamFrame,
|
||||||
@@ -191,10 +192,10 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
|||||||
onChange: vi.fn(() => () => {}),
|
onChange: vi.fn(() => () => {}),
|
||||||
};
|
};
|
||||||
return new VoiceAssistantWebSocketServer(
|
return new VoiceAssistantWebSocketServer(
|
||||||
{} as unknown as HTTPServer,
|
createStub<HTTPServer>({}),
|
||||||
createLogger() as unknown as pino.Logger,
|
createStub<pino.Logger>(createLogger()),
|
||||||
"srv_test",
|
"srv_test",
|
||||||
{
|
createStub<AgentManager>({
|
||||||
setAgentAttentionCallback: vi.fn(),
|
setAgentAttentionCallback: vi.fn(),
|
||||||
getAgent: vi.fn(() => null),
|
getAgent: vi.fn(() => null),
|
||||||
getMetricsSnapshot: vi.fn(() => ({
|
getMetricsSnapshot: vi.fn(() => ({
|
||||||
@@ -204,11 +205,11 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
|||||||
pendingPermissionAgents: 0,
|
pendingPermissionAgents: 0,
|
||||||
erroredAgents: 0,
|
erroredAgents: 0,
|
||||||
})),
|
})),
|
||||||
} as unknown as AgentManager,
|
}),
|
||||||
{} as unknown as AgentStorage,
|
createStub<AgentStorage>({}),
|
||||||
{} as unknown as DownloadTokenStore,
|
createStub<DownloadTokenStore>({}),
|
||||||
"/tmp/paseo-test",
|
"/tmp/paseo-test",
|
||||||
daemonConfigStore as unknown as DaemonConfigStore,
|
createStub<DaemonConfigStore>(daemonConfigStore),
|
||||||
null,
|
null,
|
||||||
{ allowedOrigins: new Set() },
|
{ allowedOrigins: new Set() },
|
||||||
undefined,
|
undefined,
|
||||||
@@ -227,10 +228,10 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{} as unknown as FileBackedChatService,
|
createStub<FileBackedChatService>({}),
|
||||||
{} as unknown as LoopService,
|
createStub<LoopService>({}),
|
||||||
{} as unknown as ScheduleService,
|
createStub<ScheduleService>({}),
|
||||||
{
|
createStub<CheckoutDiffManager>({
|
||||||
subscribe: vi.fn(),
|
subscribe: vi.fn(),
|
||||||
scheduleRefreshForCwd: vi.fn(),
|
scheduleRefreshForCwd: vi.fn(),
|
||||||
getMetrics: vi.fn(() => ({
|
getMetrics: vi.fn(() => ({
|
||||||
@@ -240,7 +241,7 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
|||||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||||
})),
|
})),
|
||||||
dispose: vi.fn(),
|
dispose: vi.fn(),
|
||||||
} as unknown as CheckoutDiffManager,
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +367,7 @@ async function attachDirectAndHello(params: {
|
|||||||
socket: MockSocket;
|
socket: MockSocket;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}) {
|
}) {
|
||||||
await (params.server as unknown as WebSocketServerInternals).attachSocket(
|
await asInternals<WebSocketServerInternals>(params.server).attachSocket(
|
||||||
params.socket,
|
params.socket,
|
||||||
createDirectRequest(),
|
createDirectRequest(),
|
||||||
);
|
);
|
||||||
@@ -426,10 +427,7 @@ describe("relay external socket reconnect behavior", () => {
|
|||||||
const server = createServer();
|
const server = createServer();
|
||||||
const socket = new MockSocket();
|
const socket = new MockSocket();
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).attachSocket(
|
await asInternals<WebSocketServerInternals>(server).attachSocket(socket, createDirectRequest());
|
||||||
socket,
|
|
||||||
createDirectRequest(),
|
|
||||||
);
|
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"message",
|
"message",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -460,10 +458,7 @@ describe("relay external socket reconnect behavior", () => {
|
|||||||
closeReason = typeof reason === "string" ? reason : "";
|
closeReason = typeof reason === "string" ? reason : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
await (server as unknown as WebSocketServerInternals).attachSocket(
|
await asInternals<WebSocketServerInternals>(server).attachSocket(socket, createDirectRequest());
|
||||||
socket,
|
|
||||||
createDirectRequest(),
|
|
||||||
);
|
|
||||||
await vi.advanceTimersByTimeAsync(15_000);
|
await vi.advanceTimersByTimeAsync(15_000);
|
||||||
|
|
||||||
expect(closeCode).toBe(4001);
|
expect(closeCode).toBe(4001);
|
||||||
|
|||||||
Reference in New Issue
Block a user