Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts
T

246 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../src/types/index.ts";
import { defaultProviderProfileOptions, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, resolveConversationSessionStatus, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts";
test("R2 composer ignores stale workspace activeTraceId without verified active message", () => {
const workspace = workspaceRecord({ activeTraceId: "trc_stale", sessionStatus: "running" });
const composer = resolveComposerState({ workspace, messages: [], conversations: [], activeConversationId: "cnv_r2", chatPending: false });
assert.equal(composer.submitMode, "turn");
assert.equal(composer.route, "/v1/agent/chat");
assert.equal(composer.targetTraceId, null);
assert.equal(composer.sessionId, "ses_r2");
});
test("R2 composer steers only when an active agent message or status exists", () => {
const messages: ChatMessage[] = [agentMessage({ status: "running", traceId: "trc_running", conversationId: "cnv_r2", sessionId: "ses_r2" })];
const composer = resolveComposerState({ workspace: workspaceRecord({ activeTraceId: "trc_running", sessionStatus: "running" }), messages, activeConversationId: "cnv_r2", chatPending: true });
assert.equal(composer.submitMode, "steer");
assert.equal(composer.route, "/v1/agent/chat/steer");
assert.equal(composer.targetTraceId, "trc_running");
});
test("R2 composer cancel targets only the active conversation trace", () => {
const messages: ChatMessage[] = [
agentMessage({ id: "msg_other", status: "running", traceId: "trc_other", conversationId: "cnv_other", sessionId: "ses_other", threadId: "thr_other" }),
agentMessage({ id: "msg_active", status: "running", traceId: "trc_active", conversationId: "cnv_active", sessionId: "ses_active", threadId: "thr_active" })
];
assert.equal(resolveCancelableAgentMessage({
messages,
activeConversationId: "cnv_active",
targetTraceId: "trc_active",
targetSessionId: "ses_active",
targetThreadId: "thr_active"
})?.traceId, "trc_active");
assert.equal(resolveCancelableAgentMessage({
messages,
activeConversationId: "cnv_active",
targetTraceId: "trc_other",
targetSessionId: "ses_active",
targetThreadId: "thr_active"
}), null);
});
test("R2 composer never reuses a stale workspace session during routed selection", () => {
const workspace = workspaceRecord({ activeTraceId: null, sessionStatus: "idle" });
const withoutTarget = resolveComposerState({ workspace, messages: [], conversations: [], activeConversationId: "cnv_route", chatPending: false });
assert.equal(withoutTarget.disabled, true);
assert.equal(withoutTarget.disabledReason, "session_required");
assert.equal(withoutTarget.conversationId, "cnv_route");
assert.equal(withoutTarget.sessionId, null);
const withTarget = resolveComposerState({
workspace,
messages: [],
conversations: [{ conversationId: "cnv_route", sessionId: "ses_route", threadId: "thr_route" }],
activeConversationId: "cnv_route",
chatPending: false
});
assert.equal(withTarget.disabled, false);
assert.equal(withTarget.conversationId, "cnv_route");
assert.equal(withTarget.sessionId, "ses_route");
assert.equal(withTarget.threadId, "thr_route");
});
test("R2 active trace cleanup preserves workspace and records stale reason", () => {
const cleared = workspaceWithClearedActiveTrace(workspaceRecord({ activeTraceId: "trc_stale", sessionStatus: "running" }), "trc_stale", "steer-trace-not-found");
assert.equal(cleared?.activeTraceId, null);
assert.equal(cleared?.workspace?.activeTraceId, null);
assert.equal(cleared?.workspace?.staleActiveTraceId, "trc_stale");
assert.equal(cleared?.workspace?.staleActiveTraceReason, "steer-trace-not-found");
assert.equal(cleared?.workspace?.sessionStatus, "failed");
});
test("R2 workspace snapshots cannot override a newer explicit session selection", () => {
assert.equal(shouldApplyWorkspaceSnapshot({
requestEpoch: 1,
currentEpoch: 2,
currentConversationId: "cnv_user_selected",
workspace: workspaceRecord({ activeTraceId: null, sessionStatus: "idle" })
}), false);
assert.equal(shouldApplyWorkspaceSnapshot({
requestEpoch: 1,
currentEpoch: 2,
currentConversationId: "cnv_r2",
workspace: workspaceRecord({ activeTraceId: null, sessionStatus: "idle" })
}), true);
});
test("R2 session list shows loading until conversations are ready", () => {
assert.equal(shouldShowSessionListLoading({ loading: true, conversationsReady: false }), true);
assert.equal(shouldShowSessionListLoading({ loading: true, conversationsReady: true }), false);
assert.equal(shouldShowSessionListLoading({ loading: false, conversationsReady: false }), false);
});
test("R2 session tabs render only conversations supplied by the list API", () => {
const tabs = sortSessionTabs([], "cnv_selected_only");
assert.equal(tabs.length, 0);
});
test("R2 drafts keep recent unique values and normalize corrupt storage", () => {
const next = recordRecentDraft([{ text: "旧输入", ts: "2026-01-01T00:00:00.000Z" }], "新输入", "2026-01-02T00:00:00.000Z");
assert.deepEqual(next.map((item) => item.text), ["新输入", "旧输入"]);
const deduped = recordRecentDraft(next, "旧输入", "2026-01-03T00:00:00.000Z");
assert.deepEqual(deduped.map((item) => item.text), ["旧输入", "新输入"]);
assert.deepEqual(normalizeRecentDrafts([{ text: "ok", ts: "now" }, { text: 1 }]).map((item) => item.text), ["ok"]);
});
test("R2 session tabs expose label, model metadata source, count and active state", () => {
const tabs = sortSessionTabs([{ conversationId: "cnv_r2", sessionId: "ses_r2", lastTraceId: "trc_done", messageCount: 2, firstUserMessagePreview: "请执行一次长任务", status: "completed", updatedAt: "2026-01-02T00:00:00.000Z" }], "cnv_r2");
assert.equal(tabs.length, 1);
assert.equal(tabs[0]?.active, true);
assert.equal(tabs[0]?.label, "请执行一次长任务");
assert.match(tabs[0]?.subtitle ?? "", /trace trc_done/u);
});
test("R2 session tabs use last user message time instead of agent or trace activity", () => {
const tabs = sortSessionTabs([
{
conversationId: "cnv_old_user_recent_agent",
sessionId: "ses_old_user_recent_agent",
firstUserMessagePreview: "用户消息较早但 agent 刚刚更新",
updatedAt: "2026-01-01T00:00:00.000Z",
startedAt: "2026-01-01T00:00:00.000Z",
messages: [
userMessage({ conversationId: "cnv_old_user_recent_agent", createdAt: "2026-01-01T00:02:00.000Z" }),
agentMessage({ traceId: "trc_recent", conversationId: "cnv_old_user_recent_agent", updatedAt: "2026-01-04T00:01:00.000Z" })
]
},
{
conversationId: "cnv_newer_user",
sessionId: "ses_newer_user",
firstUserMessagePreview: "用户消息较新",
updatedAt: "2026-01-03T00:00:00.000Z",
messages: [userMessage({ conversationId: "cnv_newer_user", createdAt: "2026-01-03T00:00:00.000Z" })]
}
], null);
assert.equal(tabs[0]?.conversationId, "cnv_newer_user");
assert.equal(tabs.find((tab) => tab.conversationId === "cnv_old_user_recent_agent")?.updatedAt, "2026-01-01T00:02:00.000Z");
});
test("R2 running session tabs expose animation state without rewriting label", () => {
const tabs = sortSessionTabs([
{
conversationId: "cnv_running",
sessionId: "ses_running",
firstUserMessagePreview: "保持原始标题",
status: "busy",
messages: [
userMessage({ conversationId: "cnv_running", createdAt: "2026-01-04T00:00:00.000Z" }),
agentMessage({ conversationId: "cnv_running", status: "running", updatedAt: "2026-01-04T00:01:00.000Z" })
]
}
], null, { ses_running: { sessionId: "ses_running", status: "running" } });
assert.equal(tabs[0]?.label, "保持原始标题");
assert.equal(tabs[0]?.running, true);
});
test("R2 session tabs do not infer running state from workspace stubs", () => {
const conversations: ConversationRecord[] = [{
conversationId: "cnv_r2",
sessionId: "ses_r2",
firstUserMessagePreview: "保持原始标题",
status: "running",
messageCount: 2,
messages: [
userMessage({ conversationId: "cnv_r2", createdAt: "2026-01-04T00:00:00.000Z" }),
agentMessage({ conversationId: "cnv_r2", status: "running", updatedAt: "2026-01-04T00:01:00.000Z" })
]
}];
const tabs = sortSessionTabs(conversations, "cnv_r2");
assert.equal(tabs[0]?.status, "unknown");
assert.equal(tabs[0]?.running, false);
assert.equal(tabs[0]?.messageCount, 2);
assert.equal(tabs[0]?.label, "保持原始标题");
const authorityTabs = sortSessionTabs(conversations, "cnv_r2", { ses_r2: { sessionId: "ses_r2", status: "running" } });
assert.equal(authorityTabs[0]?.status, "running");
assert.equal(authorityTabs[0]?.running, true);
});
test("R2 session tabs keep trace metadata but status comes only from session authority", () => {
const conversation: ConversationRecord = {
conversationId: "cnv_stale_snapshot",
sessionId: "ses_stale_snapshot",
status: "idle",
snapshot: { sessionStatus: "running" },
messages: [agentMessage({ conversationId: "cnv_stale_snapshot", status: "running", traceId: "trc_stale" })]
};
assert.equal(resolveConversationSessionStatus(conversation), "running");
const tabs = sortSessionTabs([conversation], "cnv_stale_snapshot");
assert.equal(tabs[0]?.status, "unknown");
assert.equal(tabs[0]?.running, false);
assert.equal(tabs[0]?.lastTraceId, "trc_stale");
const authorityTabs = sortSessionTabs([conversation], "cnv_stale_snapshot", { ses_stale_snapshot: { sessionId: "ses_stale_snapshot", status: "idle" } });
assert.equal(authorityTabs[0]?.status, "idle");
assert.equal(authorityTabs[0]?.running, false);
});
test("R2 agent message titles drop transient running wording", () => {
assert.equal(normalizeWorkbenchMessageTitle("agent", "Code Agent 处理中"), "Code Agent");
assert.equal(normalizeWorkbenchMessageTitle("agent", "Code Agent running"), "Code Agent");
assert.equal(normalizeWorkbenchMessageTitle("agent", "Code Agent result"), "Code Agent result");
assert.equal(normalizeWorkbenchMessageTitle("user", "用户"), "用户");
});
test("R2 session tab labels normalize object previews and reject object placeholders", () => {
const tabs = sortSessionTabs([
{ conversationId: "cnv_object", sessionId: "ses_object", firstUserMessagePreview: { text: "对象预览标题" } as unknown as string, updatedAt: "2026-01-03T00:00:00.000Z" },
{ conversationId: "cnv_literal", sessionId: "ses_literal", firstUserMessagePreview: "[object Object]", updatedAt: "2026-01-02T00:00:00.000Z" }
], null);
assert.equal(tabs[0]?.label, "对象预览标题");
assert.equal(tabs[1]?.label, "Session ses_lite...");
assert.ok(tabs.every((tab) => !tab.label.includes("[object Object]")));
});
test("R2 provider options follow catalog and keep selected fallback", () => {
const catalog = providerProfileOptionsFromPayload({ profiles: [{ profile: "codex-api", configured: true }, { profile: "deepseek", configured: false }] }, "minimax-m3");
assert.deepEqual(catalog.map((item) => item.value), ["deepseek", "codex-api", "minimax-m3"]);
assert.equal(catalog.find((item) => item.value === "codex-api")?.configured, true);
assert.ok(defaultProviderProfileOptions("custom-profile").some((item) => item.value === "custom-profile"));
});
function workspaceRecord(input: { activeTraceId: string | null; sessionStatus: string | null }): WorkspaceRecord {
return {
workspaceId: "wsp_r2",
projectId: "prj_hwpod_workbench",
selectedConversationId: "cnv_r2",
selectedAgentSessionId: "ses_r2",
activeTraceId: input.activeTraceId,
workspace: {
selectedConversationId: "cnv_r2",
selectedAgentSessionId: "ses_r2",
activeTraceId: input.activeTraceId,
sessionStatus: input.sessionStatus
}
};
}
function agentMessage(patch: Partial<ChatMessage>): ChatMessage {
return { id: "msg_r2", role: "agent", title: "Code Agent", text: "running", status: "running", createdAt: "2026-01-01T00:00:00.000Z", ...patch };
}
function userMessage(patch: Partial<ChatMessage>): ChatMessage {
return { id: `msg_user_${patch.conversationId ?? "r2"}`, role: "user", title: "用户", text: "prompt", status: "sent", createdAt: "2026-01-01T00:00:00.000Z", ...patch };
}