Files
2026-07-14 03:55:47 +02:00

342 lines
21 KiB
TypeScript

// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010403 API契约 draft-2026-06-18-r1.
// Responsibility: Regression tests for Workbench route parsing, server-state projection, and session tab status authority.
import assert from "node:assert/strict";
import test from "node:test";
import type { WorkbenchSessionRecord } from "../src/types/index.ts";
import { initialWorkbenchSessionIdFromLocation } from "../src/stores/workbench-projection.ts";
import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority } from "../src/stores/workbench-server-state.ts";
import { sessionToSessionTab, stableSessionList } from "../src/stores/workbench-session.ts";
test("Workbench route parsing accepts only session routes", () => {
assert.equal(initialWorkbenchSessionIdFromLocation({ pathname: "/workbench/sessions/ses_route" }), "ses_route");
assert.equal(initialWorkbenchSessionIdFromLocation({ pathname: "/workbench/sessions/cnv_route" }), null);
assert.equal(initialWorkbenchSessionIdFromLocation({ pathname: "/workspace/sessions/ses_route" }), null);
assert.equal(initialWorkbenchSessionIdFromLocation({ pathname: "/workbench" }), null);
});
test("Workbench server-state is keyed only by session and trace", () => {
const session: WorkbenchSessionRecord = {
sessionId: "ses_route",
threadId: "thr_route",
messages: [{ id: "msg_route", messageId: "msg_route", role: "user", title: "用户", text: "route prompt", status: "sent", createdAt: "2026-06-18T00:00:00.000Z", sessionId: "ses_route" }]
};
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session });
state = reduceWorkbenchServerState(state, { type: "trace.snapshot", traceId: "trc_route", trace: { traceId: "trc_route", status: "completed", events: [{ seq: 1, type: "done" }] } });
assert.equal(selectActiveSession(state, "ses_route")?.sessionId, "ses_route");
assert.deepEqual(selectActiveMessages(state, "ses_route").map((message) => message.id), ["msg_route"]);
assert.equal(selectTraceAuthorityById(state).trc_route?.traceId, "trc_route");
});
test("Workbench session list accepts server projection while keeping selected messages", () => {
const selected: WorkbenchSessionRecord = {
sessionId: "ses_live",
threadId: "thr_live",
status: "running",
lastTraceId: "trc_live",
messageCount: 2,
messages: [
{ id: "msg_live_user", messageId: "msg_live_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-18T00:00:00.000Z", sessionId: "ses_live", traceId: "trc_live" },
{ id: "msg_live_agent", messageId: "msg_live_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-18T00:00:01.000Z", sessionId: "ses_live", traceId: "trc_live" }
]
};
const serverSummary: WorkbenchSessionRecord = {
sessionId: "ses_live",
threadId: "thr_live",
status: "completed",
lastTraceId: "trc_live",
messageCount: 2,
updatedAt: "2026-06-18T00:00:03.000Z"
};
const next = stableSessionList([selected], [serverSummary], "ses_live", selected);
assert.equal(next[0]?.sessionId, "ses_live");
assert.equal(next[0]?.status, "completed");
assert.equal(next[0]?.updatedAt, "2026-06-18T00:00:03.000Z");
assert.equal(next[0]?.messages?.length, 2);
});
test("Workbench session tab prefers server status authority over the latest message fallback", () => {
const session: WorkbenchSessionRecord = {
sessionId: "ses_tab_projection",
threadId: "thr_tab_projection",
status: "running",
lastTraceId: "trc_tab_projection",
messageCount: 2,
messages: [
{ id: "msg_tab_user", messageId: "msg_tab_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-18T00:00:00.000Z", sessionId: "ses_tab_projection", traceId: "trc_tab_projection" },
{ id: "msg_tab_agent", messageId: "msg_tab_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-18T00:00:01.000Z", sessionId: "ses_tab_projection", traceId: "trc_tab_projection" }
]
};
const rawTab = sessionToSessionTab(session, "ses_tab_projection");
const tab = sessionToSessionTab(session, "ses_tab_projection", { ses_tab_projection: { sessionId: "ses_tab_projection", status: "running", lastTraceId: "trc_tab_projection" } });
assert.equal(rawTab.status, "completed");
assert.equal(rawTab.running, false);
assert.equal(tab.status, "running");
assert.equal(tab.running, true);
});
test("Workbench server-state session detail updates session status authority", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.status",
session: { sessionId: "ses_backfill", status: "running" }
});
state = reduceWorkbenchServerState(state, {
type: "session.detail",
session: {
sessionId: "ses_backfill",
threadId: "thr_backfill",
status: "completed",
lastTraceId: "trc_backfill",
updatedAt: "2026-06-18T00:00:03.000Z"
}
});
const authority = selectSessionStatusAuthority(state).ses_backfill;
assert.equal(authority?.status, "completed");
assert.equal(authority?.lastTraceId, "trc_backfill");
});
test("Workbench turn status action seals completed authority for the same trace", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_status_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_status_seal", traceId: "trc_status_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_status_seal", traceId: "trc_status_seal" }
]
});
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_status_seal", sessionId: "ses_status_seal", status: "completed", running: false, terminal: true }
});
let authority = selectSessionStatusAuthority(state).ses_status_seal;
assert.equal(authority?.status, "completed");
assert.equal(authority?.lastTraceId, "trc_status_seal");
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_status_next", sessionId: "ses_status_seal", status: "running", running: true, terminal: false }
});
authority = selectSessionStatusAuthority(state).ses_status_seal;
assert.equal(authority?.status, "running");
assert.equal(authority?.lastTraceId, "trc_status_next");
});
test("Workbench turn status action preserves terminal authority over stale running refresh", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_turn_seal", status: "running", running: true, terminal: false, sessionId: "ses_turn_seal" }
});
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_turn_seal", status: "completed", running: false, terminal: true, sessionId: "ses_turn_seal", loadedAt: "2026-06-24T00:00:02.000Z" }
});
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_turn_seal", status: "running", running: true, terminal: false, sessionId: "ses_turn_seal", loadedAt: "2026-06-24T00:00:01.000Z" }
});
const turn = selectTurnStatusAuthority(state).trc_turn_seal;
assert.equal(turn?.status, "completed");
assert.equal(turn?.running, false);
assert.equal(turn?.terminal, true);
assert.equal(turn?.loadedAt, "2026-06-24T00:00:02.000Z");
});
test("Workbench realtime agent snapshots cannot create duplicate assistant messages", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_steer",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_steer", traceId: "trc_steer_target" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_steer", traceId: "trc_steer_target" }
]
});
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_steer",
message: { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "done", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_steer", traceId: "trc_steer_target" }
});
assert.deepEqual(selectActiveMessages(state, "ses_steer").map((message) => `${message.role}:${message.messageId}:${message.text}`), ["user:msg_user:ping", "agent:msg_agent:done"]);
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_steer",
message: { id: "msg_duplicate_agent", messageId: "msg_duplicate_agent", role: "agent", title: "Code Agent", text: "projection-resume:non-terminal-backoff", status: "completed", createdAt: "2026-06-24T00:00:02.000Z", sessionId: "ses_steer", traceId: "trc_duplicate" }
});
assert.deepEqual(selectActiveMessages(state, "ses_steer").map((message) => message.messageId), ["msg_user", "msg_agent"]);
});
test("Workbench realtime agent snapshots can seed an empty session projection", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_empty_snapshot",
message: { id: "msg_agent_seed", messageId: "msg_agent_seed", role: "agent", title: "Code Agent", text: "Failed to fetch", status: "failed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_empty_snapshot", traceId: "trc_empty_snapshot" }
});
assert.deepEqual(selectActiveMessages(state, "ses_empty_snapshot").map((message) => `${message.role}:${message.messageId}:${message.text}`), ["agent:msg_agent_seed:Failed to fetch"]);
});
test("Workbench session messages keep sealed terminal timing on bulk refresh", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:12.000Z", durationMs: 12_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:12.000Z", durationMs: 12_000 }
]
});
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:13.000Z", durationMs: 13_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:13.000Z", durationMs: 13_000 }
]
});
const agent = selectActiveMessages(state, "ses_terminal_seal").find((message) => message.role === "agent");
assert.equal(agent?.durationMs, 12_000);
assert.equal(agent?.timing?.durationMs, 12_000);
assert.equal(agent?.finishedAt, "2026-06-24T00:00:12.000Z");
});
test("Workbench sealed completed final body survives stale running session messages", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.status",
session: { sessionId: "ses_final_seal", status: "running", lastTraceId: "trc_final_seal" }
});
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_final_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "running", timing: { startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: null }
]
});
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_final_seal",
message: { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "final answer", finalResponse: { text: "final answer", sealed: true }, status: "completed", createdAt: "2026-06-24T00:00:01.000Z", updatedAt: "2026-06-24T00:00:06.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "terminal", timing: { startedAt: "2026-06-24T00:00:01.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 5_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 5_000 }
});
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_final_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "running", timing: { startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:04.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:04.000Z", durationMs: null }
]
});
const agent = selectActiveMessages(state, "ses_final_seal").find((message) => message.role === "agent");
assert.equal(agent?.status, "completed");
assert.equal(agent?.text, "final answer");
assert.equal((agent?.finalResponse as { text?: string } | undefined)?.text, "final answer");
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "running");
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_final_seal", sessionId: "ses_final_seal", status: "completed", running: false, terminal: true, updatedAt: "2026-06-24T00:00:06.000Z" }
});
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "completed");
state = reduceWorkbenchServerState(state, {
type: "session.list",
sessions: [{ sessionId: "ses_final_seal", threadId: "thr_final_seal", status: "running", lastTraceId: "trc_final_seal", updatedAt: "2026-06-24T00:00:07.000Z" }]
});
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "completed");
assert.equal(selectActiveSession(state, "ses_final_seal")?.status, "completed");
});
test("Workbench session messages preserve canonical terminal zero timing", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_zero_repair",
messages: [
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_zero_repair", traceId: "trc_terminal_zero_repair", timing: { startedAt: "2026-06-24T00:00:00.000Z", lastEventAt: "2026-06-24T00:00:05.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", lastEventAt: "2026-06-24T00:00:05.000Z", durationMs: null }
]
});
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_terminal_zero_repair",
message: { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:00.000Z", updatedAt: "2026-06-24T00:00:06.000Z", sessionId: "ses_terminal_zero_repair", traceId: "trc_terminal_zero_repair", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:00.000Z", durationMs: 0, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:00.000Z", durationMs: 0 }
});
const agent = selectActiveMessages(state, "ses_terminal_zero_repair").find((message) => message.role === "agent");
assert.equal(agent?.status, "completed");
assert.equal(agent?.durationMs, 0);
assert.equal(agent?.timing?.durationMs, 0);
});
test("Workbench session messages can replace sealed terminal zero with positive timing", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_zero_positive",
messages: [
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_zero_positive", traceId: "trc_terminal_zero_positive", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:00.000Z", durationMs: 0, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:00.000Z", durationMs: 0 }
]
});
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_zero_positive",
messages: [
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_zero_positive", traceId: "trc_terminal_zero_positive", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 6_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 6_000 }
]
});
const agent = selectActiveMessages(state, "ses_terminal_zero_positive").find((message) => message.role === "agent");
assert.equal(agent?.durationMs, 6_000);
assert.equal(agent?.timing?.durationMs, 6_000);
});
test("Workbench server-state keeps running card startedAt stable across early snapshots", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_running_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_running_seal", traceId: "trc_running_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_running_seal", traceId: "trc_running_seal", traceAutoLifecycle: "running", timing: { startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:01.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:01.000Z", durationMs: null }
]
});
state = reduceWorkbenchServerState(state, {
type: "message.snapshot",
sessionId: "ses_running_seal",
message: { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:02.000Z", sessionId: "ses_running_seal", traceId: "trc_running_seal", timing: { startedAt: "2026-06-24T00:00:02.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: 0, valuesRedacted: true }, startedAt: "2026-06-24T00:00:02.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: 0 }
});
const agent = selectActiveMessages(state, "ses_running_seal").find((message) => message.role === "agent");
assert.equal(agent?.startedAt, "2026-06-24T00:00:01.000Z");
assert.equal(agent?.timing?.startedAt, "2026-06-24T00:00:01.000Z");
assert.equal(agent?.durationMs, null);
assert.equal(agent?.timing?.durationMs, null);
assert.equal(agent?.lastEventAt, "2026-06-24T00:00:02.000Z");
});