murashit_codex-panel/tests/domain/threads/reference.test.ts

179 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Thread } from "../../../src/generated/app-server/v2/Thread";
import type { Turn } from "../../../src/generated/app-server/v2/Turn";
import {
referencedThreadDisplayFromPrompt,
referencedThreadInput,
referencedThreadPrompt,
referencedThreadTurns,
} from "../../../src/domain/threads/reference";
function thread(overrides: Partial<Thread> = {}): Thread {
return {
id: "019abcde-0000-7000-8000-000000000001",
sessionId: "session-1",
forkedFromId: null,
preview: "Preview",
ephemeral: false,
modelProvider: "openai",
createdAt: 1,
updatedAt: 1,
status: "idle",
path: null,
cwd: "/vault",
cliVersion: "0.130.0",
source: "appServer",
threadSource: null,
agentNickname: null,
agentRole: null,
gitInfo: null,
name: "参照元",
turns: [],
...overrides,
} as Thread;
}
function turn(id: string, startedAt: number, items: Turn["items"]): Turn {
return {
id,
items,
itemsView: "full",
status: "completed",
error: null,
startedAt,
completedAt: startedAt + 1,
durationMs: 1,
};
}
describe("thread reference context", () => {
it("extracts user messages and final Codex responses in chronological order", () => {
const turns = referencedThreadTurns([
turn("turn-2", 2, [
{ type: "userMessage", id: "u2", clientId: null, content: [{ type: "text", text: "次の依頼", text_elements: [] }] },
{
type: "commandExecution",
id: "cmd",
command: "npm test",
cwd: "/vault",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ignored",
exitCode: 0,
durationMs: 1,
},
{ type: "agentMessage", id: "a2", text: "次の回答", phase: "final_answer", memoryCitation: null },
]),
turn("turn-1", 1, [
{ type: "userMessage", id: "u1", clientId: null, content: [{ type: "text", text: "最初の依頼", text_elements: [] }] },
{ type: "agentMessage", id: "draft", text: "途中経過", phase: null, memoryCitation: null },
{ type: "agentMessage", id: "a1", text: "最終回答", phase: "final_answer", memoryCitation: null },
]),
]);
expect(turns).toEqual([
{ userText: "最初の依頼", assistantText: "最終回答" },
{ userText: "次の依頼", assistantText: "次の回答" },
]);
});
it("builds an untruncated reference prompt with the 20 turn limit noted", () => {
const longText = "x".repeat(5000);
const prompt = referencedThreadPrompt(thread(), [{ userText: longText, assistantText: "回答" }], "この続きです");
expect(prompt).toContain("[Codex Panel referenced thread v1]");
expect(prompt).toContain('"version":1');
expect(prompt).toContain('"includedTurns":1');
expect(prompt).toContain('"turnLimit":20');
expect(prompt).toContain(longText);
expect(prompt).toContain("Current user request:\nこの続きです");
});
it("extracts display text and metadata from a reference prompt", () => {
const prompt = referencedThreadPrompt(thread(), [{ userText: "元の依頼", assistantText: "回答" }], "この続きです");
expect(referencedThreadDisplayFromPrompt(prompt)).toEqual({
text: "この続きです",
reference: {
threadId: "019abcde-0000-7000-8000-000000000001",
title: "参照元",
includedTurns: 1,
turnLimit: 20,
},
});
});
it("does not parse the old line-based reference prompt format", () => {
expect(
referencedThreadDisplayFromPrompt(
[
"[Codex Panel referenced thread]",
"Title: 参照元",
"Thread ID: thread-ref",
"Included turns: 1/20",
"",
"Reference thread history:",
"",
"Turn 1:",
"User:",
"元の依頼",
"",
"[/Codex Panel referenced thread]",
"",
"Current user request:",
"この続きです",
].join("\n"),
),
).toBeNull();
});
it("rejects malformed or unsupported reference envelopes", () => {
expect(
referencedThreadDisplayFromPrompt(
[
"[Codex Panel referenced thread v1]",
"{not-json}",
"",
"Reference thread history:",
"",
"[/Codex Panel referenced thread]",
"",
"Current user request:",
"この続きです",
].join("\n"),
),
).toBeNull();
expect(
referencedThreadDisplayFromPrompt(
[
"[Codex Panel referenced thread v1]",
'{"version":2,"threadId":"thread-ref","title":"参照元","includedTurns":1,"turnLimit":20}',
"",
"Reference thread history:",
"",
"[/Codex Panel referenced thread]",
"",
"Current user request:",
"この続きです",
].join("\n"),
),
).toBeNull();
});
it("builds slash command input while preserving non-text attachments", () => {
const source = thread();
const input = referencedThreadInput(source, [{ userText: "元の依頼", assistantText: "回答" }], "この続きです", [
{ type: "text", text: "この続きです", text_elements: [] },
{ type: "mention", name: "Note", path: "Note.md" },
]);
expect(input.status).toBe("Referencing 019abcde (1/20 turns).");
expect(input.referencedThread).toMatchObject({ threadId: source.id, title: "参照元", includedTurns: 1 });
expect(input.input[0]).toMatchObject({ type: "text" });
expect(input.input[1]).toEqual({ type: "mention", name: "Note", path: "Note.md" });
});
});