refactor: share chat abort test helpers
This commit is contained in:
parent
4a00cefe63
commit
8de94abfbc
@ -1,68 +1,24 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createActiveRun,
|
||||||
|
createChatAbortContext,
|
||||||
|
invokeChatAbortHandler,
|
||||||
|
} from "./chat.abort.test-helpers.js";
|
||||||
import { chatHandlers } from "./chat.js";
|
import { chatHandlers } from "./chat.js";
|
||||||
|
|
||||||
function createActiveRun(sessionKey: string, owner?: { connId?: string; deviceId?: string }) {
|
|
||||||
const now = Date.now();
|
|
||||||
return {
|
|
||||||
controller: new AbortController(),
|
|
||||||
sessionId: `${sessionKey}-session`,
|
|
||||||
sessionKey,
|
|
||||||
startedAtMs: now,
|
|
||||||
expiresAtMs: now + 30_000,
|
|
||||||
ownerConnId: owner?.connId,
|
|
||||||
ownerDeviceId: owner?.deviceId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createContext(overrides: Record<string, unknown> = {}) {
|
|
||||||
return {
|
|
||||||
chatAbortControllers: new Map(),
|
|
||||||
chatRunBuffers: new Map(),
|
|
||||||
chatDeltaSentAt: new Map(),
|
|
||||||
chatAbortedRuns: new Map<string, number>(),
|
|
||||||
removeChatRun: vi
|
|
||||||
.fn()
|
|
||||||
.mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })),
|
|
||||||
agentRunSeq: new Map<string, number>(),
|
|
||||||
broadcast: vi.fn(),
|
|
||||||
nodeSendToSession: vi.fn(),
|
|
||||||
logGateway: { warn: vi.fn() },
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invokeChatAbort(params: {
|
|
||||||
context: ReturnType<typeof createContext>;
|
|
||||||
request: { sessionKey: string; runId?: string };
|
|
||||||
client?: {
|
|
||||||
connId?: string;
|
|
||||||
connect?: {
|
|
||||||
device?: { id?: string };
|
|
||||||
scopes?: string[];
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
}) {
|
|
||||||
const respond = vi.fn();
|
|
||||||
await chatHandlers["chat.abort"]({
|
|
||||||
params: params.request,
|
|
||||||
respond: respond as never,
|
|
||||||
context: params.context as never,
|
|
||||||
req: {} as never,
|
|
||||||
client: (params.client ?? null) as never,
|
|
||||||
isWebchatConnect: () => false,
|
|
||||||
});
|
|
||||||
return respond;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("chat.abort authorization", () => {
|
describe("chat.abort authorization", () => {
|
||||||
it("rejects explicit run aborts from other clients", async () => {
|
it("rejects explicit run aborts from other clients", async () => {
|
||||||
const context = createContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([
|
chatAbortControllers: new Map([
|
||||||
["run-1", createActiveRun("main", { connId: "conn-owner", deviceId: "dev-owner" })],
|
[
|
||||||
|
"run-1",
|
||||||
|
createActiveRun("main", { owner: { connId: "conn-owner", deviceId: "dev-owner" } }),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const respond = await invokeChatAbort({
|
const respond = await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
context,
|
context,
|
||||||
request: { sessionKey: "main", runId: "run-1" },
|
request: { sessionKey: "main", runId: "run-1" },
|
||||||
client: {
|
client: {
|
||||||
@ -79,13 +35,14 @@ describe("chat.abort authorization", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows the same paired device to abort after reconnecting", async () => {
|
it("allows the same paired device to abort after reconnecting", async () => {
|
||||||
const context = createContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([
|
chatAbortControllers: new Map([
|
||||||
["run-1", createActiveRun("main", { connId: "conn-old", deviceId: "dev-1" })],
|
["run-1", createActiveRun("main", { owner: { connId: "conn-old", deviceId: "dev-1" } })],
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const respond = await invokeChatAbort({
|
const respond = await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
context,
|
context,
|
||||||
request: { sessionKey: "main", runId: "run-1" },
|
request: { sessionKey: "main", runId: "run-1" },
|
||||||
client: {
|
client: {
|
||||||
@ -101,14 +58,15 @@ describe("chat.abort authorization", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("only aborts session-scoped runs owned by the requester", async () => {
|
it("only aborts session-scoped runs owned by the requester", async () => {
|
||||||
const context = createContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([
|
chatAbortControllers: new Map([
|
||||||
["run-mine", createActiveRun("main", { deviceId: "dev-1" })],
|
["run-mine", createActiveRun("main", { owner: { deviceId: "dev-1" } })],
|
||||||
["run-other", createActiveRun("main", { deviceId: "dev-2" })],
|
["run-other", createActiveRun("main", { owner: { deviceId: "dev-2" } })],
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const respond = await invokeChatAbort({
|
const respond = await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
context,
|
context,
|
||||||
request: { sessionKey: "main" },
|
request: { sessionKey: "main" },
|
||||||
client: {
|
client: {
|
||||||
@ -125,13 +83,17 @@ describe("chat.abort authorization", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows operator.admin clients to bypass owner checks", async () => {
|
it("allows operator.admin clients to bypass owner checks", async () => {
|
||||||
const context = createContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([
|
chatAbortControllers: new Map([
|
||||||
["run-1", createActiveRun("main", { connId: "conn-owner", deviceId: "dev-owner" })],
|
[
|
||||||
|
"run-1",
|
||||||
|
createActiveRun("main", { owner: { connId: "conn-owner", deviceId: "dev-owner" } }),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const respond = await invokeChatAbort({
|
const respond = await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
context,
|
context,
|
||||||
request: { sessionKey: "main", runId: "run-1" },
|
request: { sessionKey: "main", runId: "run-1" },
|
||||||
client: {
|
client: {
|
||||||
|
|||||||
@ -3,6 +3,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
createActiveRun,
|
||||||
|
createChatAbortContext,
|
||||||
|
invokeChatAbortHandler,
|
||||||
|
} from "./chat.abort.test-helpers.js";
|
||||||
|
|
||||||
type TranscriptLine = {
|
type TranscriptLine = {
|
||||||
message?: Record<string, unknown>;
|
message?: Record<string, unknown>;
|
||||||
@ -31,17 +36,6 @@ vi.mock("../session-utils.js", async (importOriginal) => {
|
|||||||
|
|
||||||
const { chatHandlers } = await import("./chat.js");
|
const { chatHandlers } = await import("./chat.js");
|
||||||
|
|
||||||
function createActiveRun(sessionKey: string, sessionId: string) {
|
|
||||||
const now = Date.now();
|
|
||||||
return {
|
|
||||||
controller: new AbortController(),
|
|
||||||
sessionId,
|
|
||||||
sessionKey,
|
|
||||||
startedAtMs: now,
|
|
||||||
expiresAtMs: now + 30_000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeTranscriptHeader(transcriptPath: string, sessionId: string) {
|
async function writeTranscriptHeader(transcriptPath: string, sessionId: string) {
|
||||||
const header = {
|
const header = {
|
||||||
type: "session",
|
type: "session",
|
||||||
@ -81,49 +75,6 @@ async function createTranscriptFixture(prefix: string) {
|
|||||||
return { transcriptPath, sessionId };
|
return { transcriptPath, sessionId };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createChatAbortContext(overrides: Record<string, unknown> = {}): {
|
|
||||||
chatAbortControllers: Map<string, ReturnType<typeof createActiveRun>>;
|
|
||||||
chatRunBuffers: Map<string, string>;
|
|
||||||
chatDeltaSentAt: Map<string, number>;
|
|
||||||
chatAbortedRuns: Map<string, number>;
|
|
||||||
removeChatRun: ReturnType<typeof vi.fn>;
|
|
||||||
agentRunSeq: Map<string, number>;
|
|
||||||
broadcast: ReturnType<typeof vi.fn>;
|
|
||||||
nodeSendToSession: ReturnType<typeof vi.fn>;
|
|
||||||
logGateway: { warn: ReturnType<typeof vi.fn> };
|
|
||||||
dedupe?: { get: ReturnType<typeof vi.fn> };
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
chatAbortControllers: new Map(),
|
|
||||||
chatRunBuffers: new Map(),
|
|
||||||
chatDeltaSentAt: new Map(),
|
|
||||||
chatAbortedRuns: new Map<string, number>(),
|
|
||||||
removeChatRun: vi
|
|
||||||
.fn()
|
|
||||||
.mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })),
|
|
||||||
agentRunSeq: new Map<string, number>(),
|
|
||||||
broadcast: vi.fn(),
|
|
||||||
nodeSendToSession: vi.fn(),
|
|
||||||
logGateway: { warn: vi.fn() },
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invokeChatAbort(
|
|
||||||
context: ReturnType<typeof createChatAbortContext>,
|
|
||||||
params: { sessionKey: string; runId?: string },
|
|
||||||
respond: ReturnType<typeof vi.fn>,
|
|
||||||
) {
|
|
||||||
await chatHandlers["chat.abort"]({
|
|
||||||
params,
|
|
||||||
respond: respond as never,
|
|
||||||
context: context as never,
|
|
||||||
req: {} as never,
|
|
||||||
client: null,
|
|
||||||
isWebchatConnect: () => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@ -134,7 +85,7 @@ describe("chat abort transcript persistence", () => {
|
|||||||
const runId = "idem-abort-run-1";
|
const runId = "idem-abort-run-1";
|
||||||
const respond = vi.fn();
|
const respond = vi.fn();
|
||||||
const context = createChatAbortContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([[runId, createActiveRun("main", sessionId)]]),
|
chatAbortControllers: new Map([[runId, createActiveRun("main", { sessionId })]]),
|
||||||
chatRunBuffers: new Map([[runId, "Partial from run abort"]]),
|
chatRunBuffers: new Map([[runId, "Partial from run abort"]]),
|
||||||
chatDeltaSentAt: new Map([[runId, Date.now()]]),
|
chatDeltaSentAt: new Map([[runId, Date.now()]]),
|
||||||
removeChatRun: vi
|
removeChatRun: vi
|
||||||
@ -149,17 +100,27 @@ describe("chat abort transcript persistence", () => {
|
|||||||
logGateway: { warn: vi.fn() },
|
logGateway: { warn: vi.fn() },
|
||||||
});
|
});
|
||||||
|
|
||||||
await invokeChatAbort(context, { sessionKey: "main", runId }, respond);
|
await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
|
context,
|
||||||
|
request: { sessionKey: "main", runId },
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
const [ok1, payload1] = respond.mock.calls.at(-1) ?? [];
|
const [ok1, payload1] = respond.mock.calls.at(-1) ?? [];
|
||||||
expect(ok1).toBe(true);
|
expect(ok1).toBe(true);
|
||||||
expect(payload1).toMatchObject({ aborted: true, runIds: [runId] });
|
expect(payload1).toMatchObject({ aborted: true, runIds: [runId] });
|
||||||
|
|
||||||
context.chatAbortControllers.set(runId, createActiveRun("main", sessionId));
|
context.chatAbortControllers.set(runId, createActiveRun("main", { sessionId }));
|
||||||
context.chatRunBuffers.set(runId, "Partial from run abort");
|
context.chatRunBuffers.set(runId, "Partial from run abort");
|
||||||
context.chatDeltaSentAt.set(runId, Date.now());
|
context.chatDeltaSentAt.set(runId, Date.now());
|
||||||
|
|
||||||
await invokeChatAbort(context, { sessionKey: "main", runId }, respond);
|
await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
|
context,
|
||||||
|
request: { sessionKey: "main", runId },
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
const lines = await readTranscriptLines(transcriptPath);
|
const lines = await readTranscriptLines(transcriptPath);
|
||||||
const persisted = lines
|
const persisted = lines
|
||||||
@ -188,8 +149,8 @@ describe("chat abort transcript persistence", () => {
|
|||||||
const respond = vi.fn();
|
const respond = vi.fn();
|
||||||
const context = createChatAbortContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([
|
chatAbortControllers: new Map([
|
||||||
["run-a", createActiveRun("main", sessionId)],
|
["run-a", createActiveRun("main", { sessionId })],
|
||||||
["run-b", createActiveRun("main", sessionId)],
|
["run-b", createActiveRun("main", { sessionId })],
|
||||||
]),
|
]),
|
||||||
chatRunBuffers: new Map([
|
chatRunBuffers: new Map([
|
||||||
["run-a", "Session abort partial"],
|
["run-a", "Session abort partial"],
|
||||||
@ -201,7 +162,12 @@ describe("chat abort transcript persistence", () => {
|
|||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
await invokeChatAbort(context, { sessionKey: "main" }, respond);
|
await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
|
context,
|
||||||
|
request: { sessionKey: "main" },
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
@ -280,12 +246,17 @@ describe("chat abort transcript persistence", () => {
|
|||||||
const runId = "idem-abort-run-blank";
|
const runId = "idem-abort-run-blank";
|
||||||
const respond = vi.fn();
|
const respond = vi.fn();
|
||||||
const context = createChatAbortContext({
|
const context = createChatAbortContext({
|
||||||
chatAbortControllers: new Map([[runId, createActiveRun("main", sessionId)]]),
|
chatAbortControllers: new Map([[runId, createActiveRun("main", { sessionId })]]),
|
||||||
chatRunBuffers: new Map([[runId, " \n\t "]]),
|
chatRunBuffers: new Map([[runId, " \n\t "]]),
|
||||||
chatDeltaSentAt: new Map([[runId, Date.now()]]),
|
chatDeltaSentAt: new Map([[runId, Date.now()]]),
|
||||||
});
|
});
|
||||||
|
|
||||||
await invokeChatAbort(context, { sessionKey: "main", runId }, respond);
|
await invokeChatAbortHandler({
|
||||||
|
handler: chatHandlers["chat.abort"],
|
||||||
|
context,
|
||||||
|
request: { sessionKey: "main", runId },
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
|
|||||||
69
src/gateway/server-methods/chat.abort.test-helpers.ts
Normal file
69
src/gateway/server-methods/chat.abort.test-helpers.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
export function createActiveRun(
|
||||||
|
sessionKey: string,
|
||||||
|
params: {
|
||||||
|
sessionId?: string;
|
||||||
|
owner?: { connId?: string; deviceId?: string };
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const now = Date.now();
|
||||||
|
return {
|
||||||
|
controller: new AbortController(),
|
||||||
|
sessionId: params.sessionId ?? `${sessionKey}-session`,
|
||||||
|
sessionKey,
|
||||||
|
startedAtMs: now,
|
||||||
|
expiresAtMs: now + 30_000,
|
||||||
|
ownerConnId: params.owner?.connId,
|
||||||
|
ownerDeviceId: params.owner?.deviceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChatAbortContext(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
chatAbortControllers: new Map(),
|
||||||
|
chatRunBuffers: new Map(),
|
||||||
|
chatDeltaSentAt: new Map(),
|
||||||
|
chatAbortedRuns: new Map<string, number>(),
|
||||||
|
removeChatRun: vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })),
|
||||||
|
agentRunSeq: new Map<string, number>(),
|
||||||
|
broadcast: vi.fn(),
|
||||||
|
nodeSendToSession: vi.fn(),
|
||||||
|
logGateway: { warn: vi.fn() },
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invokeChatAbortHandler(params: {
|
||||||
|
handler: (args: {
|
||||||
|
params: { sessionKey: string; runId?: string };
|
||||||
|
respond: never;
|
||||||
|
context: never;
|
||||||
|
req: never;
|
||||||
|
client: never;
|
||||||
|
isWebchatConnect: () => boolean;
|
||||||
|
}) => Promise<void>;
|
||||||
|
context: ReturnType<typeof createChatAbortContext>;
|
||||||
|
request: { sessionKey: string; runId?: string };
|
||||||
|
client?: {
|
||||||
|
connId?: string;
|
||||||
|
connect?: {
|
||||||
|
device?: { id?: string };
|
||||||
|
scopes?: string[];
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
respond?: ReturnType<typeof vi.fn>;
|
||||||
|
}) {
|
||||||
|
const respond = params.respond ?? vi.fn();
|
||||||
|
await params.handler({
|
||||||
|
params: params.request,
|
||||||
|
respond: respond as never,
|
||||||
|
context: params.context as never,
|
||||||
|
req: {} as never,
|
||||||
|
client: (params.client ?? null) as never,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
});
|
||||||
|
return respond;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user