mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
257 lines
7.5 KiB
TypeScript
257 lines
7.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { AppServerRequestClient } from "../../src/app-server/services/request-client";
|
|
import {
|
|
forkEphemeralThread,
|
|
listThreads,
|
|
startEphemeralThread,
|
|
startThread,
|
|
threadFromAppServerRecord,
|
|
} from "../../src/app-server/services/threads";
|
|
|
|
describe("app-server thread response adapters", () => {
|
|
it("preserves spawned subagent provenance in the domain thread", () => {
|
|
const thread = threadFromAppServerRecord({
|
|
id: "child",
|
|
preview: "Inspect",
|
|
name: null,
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
sessionId: "session",
|
|
parentThreadId: "parent",
|
|
source: {
|
|
subAgent: {
|
|
thread_spawn: {
|
|
parent_thread_id: "parent",
|
|
depth: 2,
|
|
agent_path: null,
|
|
agent_nickname: "Scout",
|
|
agent_role: "explorer",
|
|
},
|
|
},
|
|
},
|
|
agentNickname: "Scout",
|
|
agentRole: "explorer",
|
|
});
|
|
|
|
expect(thread.provenance).toEqual({
|
|
kind: "subagent",
|
|
subagentKind: "thread-spawn",
|
|
parentThreadId: "parent",
|
|
sessionId: "session",
|
|
depth: 2,
|
|
agentNickname: "Scout",
|
|
agentRole: "explorer",
|
|
});
|
|
});
|
|
|
|
it("forks read-only ephemeral side-chat threads", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({
|
|
thread: { id: "side", preview: "", name: null, createdAt: 1, updatedAt: 1 },
|
|
cwd: "/vault",
|
|
model: "gpt-5.5",
|
|
serviceTier: null,
|
|
approvalsReviewer: null,
|
|
reasoningEffort: null,
|
|
approvalPolicy: "never",
|
|
sandbox: { type: "readOnly", networkAccess: false },
|
|
activePermissionProfile: null,
|
|
}),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
const result = await forkEphemeralThread(client, "source", "/vault");
|
|
|
|
expect(client.request).toHaveBeenCalledWith("thread/fork", {
|
|
threadId: "source",
|
|
cwd: "/vault",
|
|
ephemeral: true,
|
|
sandbox: "read-only",
|
|
approvalPolicy: "never",
|
|
excludeTurns: true,
|
|
});
|
|
expect(result).toMatchObject({ sourceThreadId: "source", activation: { thread: { id: "side" }, cwd: "/vault" } });
|
|
});
|
|
|
|
it("starts panel-owned threads with the codex-panel service name", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await startThread(client, { cwd: "/vault" });
|
|
|
|
expect(client.request).toHaveBeenCalledWith("thread/start", {
|
|
cwd: "/vault",
|
|
serviceName: "codex-panel",
|
|
});
|
|
});
|
|
|
|
it("passes explicit service tier requests when starting panel-owned threads", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await startThread(client, { cwd: "/vault", serviceTier: "priority" });
|
|
|
|
expect(client.request).toHaveBeenCalledWith("thread/start", {
|
|
cwd: "/vault",
|
|
serviceName: "codex-panel",
|
|
serviceTier: "priority",
|
|
});
|
|
});
|
|
|
|
it("starts ephemeral helper threads without deprecated multi-agent mode params", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await startEphemeralThread(client, {
|
|
cwd: "/vault",
|
|
serviceName: "codex-panel-selection-rewrite",
|
|
developerInstructions: "Return structured output.",
|
|
});
|
|
|
|
expect(client.request).toHaveBeenCalledWith("thread/start", {
|
|
cwd: "/vault",
|
|
serviceName: "codex-panel-selection-rewrite",
|
|
developerInstructions: "Return structured output.",
|
|
ephemeral: true,
|
|
sandbox: "read-only",
|
|
approvalPolicy: "never",
|
|
environments: [],
|
|
});
|
|
});
|
|
|
|
it("maps listed threads to domain threads with archive state", async () => {
|
|
const clientListThreads = vi.fn().mockResolvedValue({
|
|
data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20 }],
|
|
});
|
|
const client = {
|
|
request: clientListThreads,
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await expect(listThreads(client, "/vault", { archived: true })).resolves.toEqual([
|
|
{
|
|
id: "thread-1",
|
|
preview: "Preview",
|
|
name: null,
|
|
archived: true,
|
|
createdAt: 10,
|
|
updatedAt: 20,
|
|
provenance: { kind: "interactive" },
|
|
},
|
|
]);
|
|
expect(clientListThreads).toHaveBeenCalledWith("thread/list", {
|
|
cwd: "/vault",
|
|
archived: true,
|
|
limit: 100,
|
|
sortKey: "recency_at",
|
|
sortDirection: "desc",
|
|
});
|
|
});
|
|
|
|
it("preserves app-server recency timestamps when available", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({
|
|
data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20, recencyAt: 30 }],
|
|
}),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await expect(listThreads(client, "/vault")).resolves.toEqual([
|
|
{
|
|
id: "thread-1",
|
|
preview: "Preview",
|
|
name: null,
|
|
archived: false,
|
|
createdAt: 10,
|
|
updatedAt: 20,
|
|
recencyAt: 30,
|
|
provenance: { kind: "interactive" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("preserves nullable app-server recency timestamps", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({
|
|
data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20, recencyAt: null }],
|
|
}),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await expect(listThreads(client, "/vault")).resolves.toEqual([
|
|
{
|
|
id: "thread-1",
|
|
preview: "Preview",
|
|
name: null,
|
|
archived: false,
|
|
createdAt: 10,
|
|
updatedAt: 20,
|
|
recencyAt: null,
|
|
provenance: { kind: "interactive" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("follows thread list pagination until the final page", async () => {
|
|
const clientListThreads = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
data: [{ id: "thread-1", preview: "First", name: null, createdAt: 10, updatedAt: 20 }],
|
|
nextCursor: "next",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
data: [{ id: "thread-2", preview: "Second", name: null, createdAt: 30, updatedAt: 40 }],
|
|
nextCursor: null,
|
|
});
|
|
const client = {
|
|
request: clientListThreads,
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await expect(listThreads(client, "/vault")).resolves.toEqual([
|
|
{
|
|
id: "thread-1",
|
|
preview: "First",
|
|
name: null,
|
|
archived: false,
|
|
createdAt: 10,
|
|
updatedAt: 20,
|
|
provenance: { kind: "interactive" },
|
|
},
|
|
{
|
|
id: "thread-2",
|
|
preview: "Second",
|
|
name: null,
|
|
archived: false,
|
|
createdAt: 30,
|
|
updatedAt: 40,
|
|
provenance: { kind: "interactive" },
|
|
},
|
|
]);
|
|
expect(clientListThreads).toHaveBeenNthCalledWith(1, "thread/list", {
|
|
cwd: "/vault",
|
|
archived: false,
|
|
limit: 100,
|
|
sortKey: "recency_at",
|
|
sortDirection: "desc",
|
|
});
|
|
expect(clientListThreads).toHaveBeenNthCalledWith(2, "thread/list", {
|
|
cwd: "/vault",
|
|
cursor: "next",
|
|
archived: false,
|
|
limit: 100,
|
|
sortKey: "recency_at",
|
|
sortDirection: "desc",
|
|
});
|
|
});
|
|
|
|
it("rejects repeated thread list cursors", async () => {
|
|
const client = {
|
|
request: vi.fn().mockResolvedValue({
|
|
data: [],
|
|
nextCursor: "same",
|
|
}),
|
|
} as unknown as AppServerRequestClient;
|
|
|
|
await expect(listThreads(client, "/vault")).rejects.toThrow("repeated thread list cursor");
|
|
});
|
|
});
|