Files
Cognitive_Agent/tests/unit/assistant_stream_runtime.test.cjs
2026-09-11 17:56:56 +08:00

771 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const ROOT = path.resolve(__dirname, "../..");
const ASSISTANT_SOURCE = fs.readFileSync(
path.join(ROOT, "apps/web-console/assistant-chat.js"),
"utf8",
);
class MockClassList {
constructor() {
this.values = new Set();
}
add(...names) { names.forEach((name) => this.values.add(name)); }
remove(...names) { names.forEach((name) => this.values.delete(name)); }
contains(name) { return this.values.has(name); }
toggle(name, force) {
const enabled = force === undefined ? !this.values.has(name) : Boolean(force);
if (enabled) this.values.add(name);
else this.values.delete(name);
return enabled;
}
}
class MockEventTarget {
constructor() {
this.listeners = new Map();
}
addEventListener(type, listener) {
const listeners = this.listeners.get(type) || [];
listeners.push(listener);
this.listeners.set(type, listeners);
}
removeEventListener(type, listener) {
const listeners = this.listeners.get(type) || [];
this.listeners.set(type, listeners.filter((candidate) => candidate !== listener));
}
dispatchEvent(event) {
if (!event?.type) throw new TypeError("event.type is required");
if (!event.target) event.target = this;
for (const listener of this.listeners.get(event.type) || []) listener.call(this, event);
return event.defaultPrevented !== true;
}
}
class MockElement extends MockEventTarget {
constructor(tagName = "div") {
super();
this.attributes = new Map();
this.children = [];
this.classList = new MockClassList();
this.dataset = {};
this.disabled = false;
this.hidden = false;
this.id = "";
this.innerHTML = "";
this.parentElement = null;
this.scrollHeight = 100;
this.scrollTop = 100;
this.clientHeight = 100;
this.style = {};
this.tagName = tagName.toUpperCase();
this.textContent = "";
this.title = "";
this.value = "";
}
get className() { return [...this.classList.values].join(" "); }
set className(value) {
this.classList.values = new Set(String(value).split(/\s+/).filter(Boolean));
}
append(...nodes) {
nodes.forEach((node) => {
this.children.push(node);
if (node && typeof node === "object") node.parentElement = this;
});
}
closest(selector) {
if (selector.startsWith("#") && this.id === selector.slice(1)) return this;
return this.parentElement?.closest?.(selector) || null;
}
focus() {}
matches(selector) {
if (selector === "button") return this.tagName === "BUTTON";
if (selector.startsWith(".")) return this.classList.contains(selector.slice(1));
const dataSelector = selector.match(/^\[data-([a-z-]+)(?:="([^"]*)")?\]$/);
if (!dataSelector) return false;
const key = dataSelector[1].replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
if (!(key in this.dataset)) return false;
return dataSelector[2] === undefined || this.dataset[key] === dataSelector[2];
}
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
querySelectorAll(selector) {
const matches = [];
const visit = (node) => {
if (!(node instanceof MockElement)) return;
if (node.matches(selector)) matches.push(node);
node.children.forEach(visit);
};
this.children.forEach(visit);
return matches;
}
replaceChildren(...nodes) {
this.children = [];
this.append(...nodes);
}
setAttribute(name, value) { this.attributes.set(name, String(value)); }
toggleAttribute(name, force) { this[name] = Boolean(force); }
}
function ndjsonResponse(events) {
const chunks = events.map((event) => Buffer.from(`${JSON.stringify(event)}\n`, "utf8"));
let index = 0;
return {
ok: true,
status: 200,
body: {
getReader() {
return {
async cancel() {},
async read() {
if (index >= chunks.length) return { done: true, value: undefined };
return { done: false, value: chunks[index++] };
},
releaseLock() {},
};
},
},
};
}
function createHarness(events) {
const document = new MockEventTarget();
const windowEvents = new MockEventTarget();
const body = new MockElement("body");
const input = new MockElement("textarea");
const messages = new MockElement("div");
const runButton = new MockElement("button");
const transcript = new MockElement("section");
input.id = "utterance";
messages.id = "assistantMessages";
runButton.id = "runButton";
transcript.id = "assistantTranscript";
const selectors = new Map([
["#utterance", input],
["#assistantMessages", messages],
["#runButton", runButton],
["#assistantTranscript", transcript],
]);
document.body = body;
document.createElement = (tagName) => new MockElement(tagName);
document.createTextNode = (text) => ({ nodeType: 3, textContent: String(text) });
document.querySelector = (selector) => selectors.get(selector) || null;
document.querySelectorAll = (selector) => (
selector === "#assistantMessages [data-message-role]"
? [...messages.children]
: messages.querySelectorAll(selector)
);
const storage = new Map();
const requestCalls = [];
const completed = [];
const refreshedModeProfiles = [];
windowEvents.addEventListener("cognitive-agent:request-complete", (event) => {
completed.push(event.detail);
});
const sandbox = {
AbortController,
Buffer,
CSS: { escape: (value) => String(value) },
CustomEvent: class CustomEvent {
constructor(type, options = {}) { this.type = type; this.detail = options.detail; }
},
Element: MockElement,
Event: class Event {
constructor(type, options = {}) { this.type = type; this.bubbles = options.bubbles === true; }
},
HTMLFormElement: class HTMLFormElement extends MockElement {},
JSON,
Math,
Promise,
TextDecoder,
Uint8Array,
clearTimeout,
console,
crypto: { randomUUID: () => `runtime-${requestCalls.length}-${messages.children.length}` },
document,
localStorage: { getItem: () => null, removeItem() {}, setItem() {} },
requestAnimationFrame: (callback) => callback(),
sessionStorage: {
getItem: (key) => storage.get(key) || null,
removeItem: (key) => storage.delete(key),
setItem: (key, value) => storage.set(key, String(value)),
},
setTimeout,
__N7WEB_ASSISTANT_TEST__: true,
};
sandbox.window = sandbox;
sandbox.addEventListener = windowEvents.addEventListener.bind(windowEvents);
sandbox.removeEventListener = windowEvents.removeEventListener.bind(windowEvents);
sandbox.dispatchEvent = windowEvents.dispatchEvent.bind(windowEvents);
sandbox.CognitiveAgentConsole = {
getExecutionMode: () => "dry_run",
async refreshModeProfiles(modeProfileId) {
refreshedModeProfiles.push(modeProfileId);
},
};
sandbox.CognitiveAgentNetwork = {
async request(url, options) {
requestCalls.push({ url, options });
if (url === "/api/mode-profiles" || url.startsWith("/api/mode-profiles/")) {
return {
ok: true,
status: 200,
async json() {
return { mode_profile: { id: "mode-saved", name: "午休模式" } };
},
};
}
return ndjsonResponse(events);
},
};
vm.createContext(sandbox);
vm.runInContext(ASSISTANT_SOURCE, sandbox, { filename: "assistant-chat.js" });
return {
completed,
input,
messages,
refreshedModeProfiles,
requestCalls,
sandbox,
};
}
test("a late NDJSON model error is rendered after an HTTP 200 stream", async () => {
const publicMessage = "小迪暂时没有响应,请稍后再试。";
const harness = createHarness([
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "work.started", display_text: "小迪正在理解并处理……" },
{ type: "work.progress", display_text: "小迪正在重新连接……" },
{ type: "error", error: "assistant_model_failed", message: publicMessage },
]);
harness.input.value = "解释一下 CAN FD";
await harness.sandbox.CognitiveAgentAssistant.submit();
assert.equal(harness.requestCalls.length, 1);
assert.equal(harness.requestCalls[0].url, "/api/assistant/stream");
assert.equal(harness.requestCalls[0].options.method, "POST");
assert.equal(harness.requestCalls[0].options.headers.Accept, "application/x-ndjson");
assert.equal(harness.messages.children.length, 2);
const assistantArticle = harness.messages.children[1];
assert.equal(assistantArticle.dataset.messageRole, "assistant");
assert.equal(assistantArticle.dataset.messageContent, publicMessage);
const actionStatus = assistantArticle.children.at(-1).children.at(-1);
assert.equal(actionStatus.className, "assistant-action-status failed");
assert.equal(actionStatus.children.at(-1).textContent, "本次请求未完成");
assert.equal(harness.completed.length, 1);
assert.equal(harness.completed[0].error.code, "assistant_model_failed");
assert.equal(harness.completed[0].error.message, publicMessage);
assert.equal(harness.completed[0].result, null);
});
test("a completed NDJSON answer remains the authoritative visible response", async () => {
const harness = createHarness([
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.delta", delta: "流式草稿" },
{ type: "work.completed", success: true, display_text: "小迪已完成" },
{ type: "answer.completed", route: "agent", answer: "最终回答" },
]);
harness.input.value = "普通问题";
await harness.sandbox.CognitiveAgentAssistant.submit();
const assistantArticle = harness.messages.children[1];
assert.equal(assistantArticle.dataset.messageContent, "最终回答");
assert.equal(harness.completed.length, 1);
assert.equal(harness.completed[0].error, null);
});
test("assistant bridges request start and validated local controls to the console", async () => {
const payload = {
request_id: "req-local-mixed",
utterance: "空调温度设置到23度,打开闪烁灯",
execution_mode: "dry_run",
provider_used: "local_rules+infrared_local_rules",
validated_control_commands: [
{
operation: {
id: "climate-temperature-1",
capability: "climate.set_temperature",
target: { zone: "driver" },
parameters: { temperature_c: 23 },
parse_source: "local_rule",
},
channel: "CAN1",
actual_command: "ac temp driver 23",
frames: [{
label: "CAN1 帧",
value: "0x1DE · 00 00 00 00 17 00 00 00",
}],
},
{
operation: {
id: "reading-light-1",
capability: "interior_light.set_reading_power",
target: { instance: "all" },
parameters: { state: "off" },
parse_source: "local_rule",
},
channel: "CAN1",
actual_command: "light reading all off",
frames: [],
},
{
operation: {
id: "roof-shade-1",
capability: "roof.set_shade_motion",
target: { instance: "shade" },
parameters: { motion: "close" },
parse_source: "local_rule",
},
channel: "CAN1",
actual_command: "shade close",
frames: [],
},
],
trace_events: [{ stage: "compile", duration_ms: 2 }],
};
const harness = createHarness([
{ type: "validated_control_commands", payload },
{ type: "answer.completed", route: "vehicle_control", answer: "模拟编排已完成。" },
]);
const starts = [];
const validated = [];
harness.sandbox.addEventListener("cognitive-agent:control-request-start", (event) => {
starts.push(event.detail);
});
harness.sandbox.addEventListener("cognitive-agent:validated-can-commands", (event) => {
validated.push(event.detail);
});
harness.input.value = payload.utterance;
harness.input.dataset.speechUtteranceId = "speech-local-mixed";
harness.input.dataset.speechValue = payload.utterance;
await harness.sandbox.CognitiveAgentAssistant.submit();
assert.deepEqual(JSON.parse(JSON.stringify(starts)), [{
utterance: payload.utterance,
speech_utterance_id: "speech-local-mixed",
speech_value: payload.utterance,
}]);
assert.deepEqual(JSON.parse(JSON.stringify(validated)), [payload]);
assert.equal(harness.requestCalls.length, 1);
assert.equal(JSON.parse(harness.requestCalls[0].options.body).execution_mode, "dry_run");
const assistantArticle = harness.messages.children[1];
const cards = assistantArticle.querySelectorAll(".assistant-dispatched-controls");
assert.equal(cards.length, 1);
assert.equal(cards[0].open, true);
assert.equal(
cards[0].querySelector(".assistant-dispatched-controls-count").textContent,
"3",
);
const rows = cards[0].querySelector(".assistant-dispatched-controls-list").children;
assert.deepEqual(
rows.map((row) => row.querySelector(".assistant-dispatched-control-label").textContent),
["空调温度", "阅读灯", "遮阳帘"],
);
assert.deepEqual(
rows.map((row) => row.querySelector(".assistant-dispatched-control-value").textContent),
["23°C", "关闭", "关闭"],
);
assert.match(rows[0].title, /实际控制指令:ac temp driver 23/);
});
test("unsupported-control suggestions render once and click through as a new command", async () => {
const suggestions = [
{ id: "climate-on", label: "打开空调", command: "打开空调", domain: "climate" },
{ id: "shade-close", label: "关闭遮阳帘", command: "关闭遮阳帘", domain: "roof" },
{ id: "climate-copy", label: "重复项", command: " 打开 空调 ", domain: "climate" },
];
const events = [
{
type: "control.suggestions",
message_id: "msg-unsupported",
suggestions,
},
{
type: "answer.completed",
route: "unsupported_control",
engine: "n7web-capability-registry",
answer: "当前车端暂不支持该功能控制哦,您可以尝试以下操作。",
suggestions,
},
];
const harness = createHarness(events);
harness.input.value = "打开后备箱";
await harness.sandbox.CognitiveAgentAssistant.submit();
const suggestionGroups = harness.messages.querySelectorAll(".assistant-control-suggestions");
assert.equal(suggestionGroups.length, 1);
const buttons = suggestionGroups[0].querySelectorAll("button");
assert.equal(buttons.length, 2);
assert.equal(buttons[0].type, "button");
assert.equal(buttons[0].dataset.suggestionCommand, "打开空调");
assert.equal(buttons[0].attributes.get("aria-label"), "执行控制指令:打开空调");
assert.equal(buttons[0].attributes.get("aria-busy"), "false");
buttons[0].dispatchEvent({ type: "click" });
buttons[0].dispatchEvent({ type: "click" });
assert.equal(buttons.every((button) => button.disabled), true);
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
const streamCalls = harness.requestCalls.filter(
(call) => call.url === "/api/assistant/stream",
);
assert.equal(streamCalls.length, 2);
assert.equal(JSON.parse(streamCalls[1].options.body).message, "打开空调");
assert.equal(harness.messages.children[2].dataset.messageContent, "打开空调");
assert.equal(buttons.every((button) => !button.disabled), true);
assert.equal(buttons[0].attributes.get("aria-busy"), "false");
});
test("answer fallback accepts recommended_commands and caps unsafe suggestion payloads", async () => {
const harness = createHarness([
{
type: "answer.completed",
route: "unsupported_control",
answer: "当前车端暂不支持该功能控制哦。",
recommended_commands: [
"打开空调",
{ id: "seat", label: "调节座椅", command: "副驾座椅调到80%", domain: "seat" },
{ id: "empty", label: "空指令", command: "" },
{ id: "window", label: "关闭车窗", command: "关闭所有车窗", domain: "window" },
{ id: "light", label: "打开氛围灯", command: "打开氛围灯", domain: "lighting" },
{ id: "extra", label: "不应显示", command: "空调调到25度", domain: "climate" },
],
},
]);
harness.input.value = "打开后备箱";
await harness.sandbox.CognitiveAgentAssistant.submit();
const group = harness.messages.querySelector(".assistant-control-suggestions");
assert.ok(group);
const buttons = group.querySelectorAll("button");
assert.equal(buttons.length, 4);
assert.deepEqual(
buttons.map((button) => button.dataset.suggestionCommand),
["打开空调", "副驾座椅调到80%", "关闭所有车窗", "打开氛围灯"],
);
buttons.forEach((button) => {
assert.equal(button.type, "button");
assert.match(button.attributes.get("aria-label"), /^执行控制指令:/);
});
});
test("a natural-language saved-mode update refreshes the mode gallery", async () => {
const harness = createHarness([
{ type: "message.accepted", route: "mode_profile_management", engine: "n7web-mode-registry" },
{
type: "action.completed",
success: true,
payload: {
status: "succeeded",
execution_mode: "dry_run",
orchestration_route: "mode_profile_update",
mode_profile: { id: "mode-reading", name: "阅读模式" },
},
},
{ type: "answer.completed", route: "mode_profile_management", answer: "已更新阅读模式。" },
]);
harness.input.value = "把阅读模式中空调温度调整到26度";
await harness.sandbox.CognitiveAgentAssistant.submit();
assert.deepEqual(harness.refreshedModeProfiles, ["mode-reading"]);
const state = harness.messages.querySelectorAll(".assistant-mode-created");
assert.equal(state.length, 1);
assert.equal(state[0].children[1].children[0].textContent, "已更新「我的模式」");
});
test("a natural-language saved-mode deletion refreshes the mode gallery", async () => {
const harness = createHarness([
{ type: "message.accepted", route: "mode_profile_management", engine: "n7web-mode-registry" },
{
type: "action.completed",
success: true,
payload: {
status: "succeeded",
execution_mode: "dry_run",
orchestration_route: "mode_profile_delete",
deleted_mode_profile: { id: "mode-reading", name: "阅读模式" },
},
},
{ type: "answer.completed", route: "mode_profile_management", answer: "已删除阅读模式。" },
]);
harness.input.value = "删除阅读模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
assert.deepEqual(harness.refreshedModeProfiles, [undefined]);
assert.equal(harness.messages.querySelectorAll(".assistant-mode-created").length, 0);
});
function modeDraftEvents(sourceRequestId, temperatureC) {
return [
{ type: "message.accepted", route: "vehicle_control", engine: "n7web-mode-builder" },
{
type: "action.completed",
success: true,
payload: {
status: "succeeded",
execution_mode: "dry_run",
orchestration_route: "mode_profile_create_preview",
mode_profile_draft: {
name: "午休模式",
instruction: `空调调整到${temperatureC}度`,
operation_count: 1,
source_request_id: sourceRequestId,
vehicle_id: "demo-vehicle",
},
actions: [{
domain: "climate",
capability: "climate.set_temperature",
target: { zone: "all" },
parameters: { temperature_c: temperatureC },
}],
},
},
{ type: "answer.completed", route: "vehicle_control", answer: "模式草稿已生成" },
];
}
test("a follow-up turn carries only the active authoritative draft reference", async () => {
const events = modeDraftEvents("req-draft-one", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已收到调整请求" },
);
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls[1].options.body);
assert.equal(request.mode_profile_draft_context.name, "午休模式");
assert.equal(request.mode_profile_draft_context.source_request_id, "req-draft-one");
assert.equal(request.mode_profile_draft_context.vehicle_id, "demo-vehicle");
assert.equal(Object.hasOwn(request.mode_profile_draft_context, "actions"), false);
});
test("the change-into predicate carries only a same-name opaque draft reference", async () => {
const events = modeDraftEvents("req-draft-change-into", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已处理" },
);
harness.input.value = "把午休模式的空调温度变成25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "午休模式的空调温度变成25度了吗?";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "把露营模式的空调温度变成25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const sameMode = JSON.parse(harness.requestCalls[1].options.body);
const question = JSON.parse(harness.requestCalls[2].options.body);
const anotherMode = JSON.parse(harness.requestCalls[3].options.body);
assert.deepEqual(
Object.keys(sameMode.mode_profile_draft_context).sort(),
["name", "source_request_id", "vehicle_id"],
);
assert.equal(sameMode.mode_profile_draft_context.name, "午休模式");
assert.equal(sameMode.mode_profile_draft_context.source_request_id, "req-draft-change-into");
assert.equal(sameMode.mode_profile_draft_context.vehicle_id, "demo-vehicle");
assert.equal(Object.hasOwn(question, "mode_profile_draft_context"), false);
assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false);
});
test("shade position rows explain the target opening at semantic boundaries", async () => {
const positions = [0, 30, 50, 100];
const events = [
{ type: "message.accepted", route: "vehicle_control", engine: "n7web-mode-builder" },
{
type: "action.completed",
success: true,
payload: {
status: "succeeded",
execution_mode: "dry_run",
orchestration_route: "mode_profile_create_preview",
mode_profile_draft: {
name: "遮阳测试模式",
instruction: "遮阳帘目标开度测试",
operation_count: positions.length,
source_request_id: "req-draft-shade-positions",
vehicle_id: "demo-vehicle",
},
actions: positions.map((position) => ({
domain: "roof",
capability: "roof.set_shade_position",
target: { zone: "vehicle" },
parameters: { position_pct: position },
})),
},
},
{ type: "answer.completed", route: "vehicle_control", answer: "模式草稿已生成" },
];
const harness = createHarness(events);
harness.input.value = "构建一个遮阳测试模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
const rows = harness.messages.querySelectorAll(".assistant-mode-draft-row");
assert.equal(rows.length, 4);
assert.deepEqual(
rows.map((row) => ({
label: row.children[1].textContent,
value: row.children[2].textContent,
})),
[
{ label: "遮阳帘", value: "完全关闭(目标开度 0%)" },
{ label: "遮阳帘", value: "目标开度 30%" },
{ label: "遮阳帘", value: "半开(目标开度 50%)" },
{ label: "遮阳帘", value: "完全打开(目标开度 100%)" },
],
);
});
test("ordinary questions and updates for another mode do not carry the draft reference", async () => {
const events = modeDraftEvents("req-draft-scoped", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "普通回答" },
);
harness.input.value = "空调制冷原理是什么";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "把露营模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const ordinary = JSON.parse(harness.requestCalls[1].options.body);
const anotherMode = JSON.parse(harness.requestCalls[2].options.body);
assert.equal(Object.hasOwn(ordinary, "mode_profile_draft_context"), false);
assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false);
});
test("device action predicates carry context while mode questions stay ordinary", async () => {
const events = modeDraftEvents("req-draft-actions", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已处理" },
);
harness.input.value = "请帮我把午休模式里的遮阳帘关闭";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "把午休模式的车窗全关";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "午休模式的空调设置有哪些?";
await harness.sandbox.CognitiveAgentAssistant.submit();
const deviceAction = JSON.parse(harness.requestCalls[1].options.body);
const secondDeviceAction = JSON.parse(harness.requestCalls[2].options.body);
const ordinaryQuestion = JSON.parse(harness.requestCalls[3].options.body);
assert.equal(deviceAction.mode_profile_draft_context.source_request_id, "req-draft-actions");
assert.equal(secondDeviceAction.mode_profile_draft_context.source_request_id, "req-draft-actions");
assert.equal(Object.hasOwn(ordinaryQuestion, "mode_profile_draft_context"), false);
});
test("a replacement draft disables the stale same-name card", async () => {
const events = modeDraftEvents("req-draft-old", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, ...modeDraftEvents("req-draft-new", 25));
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const oldCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-old"]',
);
const newCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-new"]',
);
assert.ok(oldCard);
assert.ok(newCard);
assert.equal(oldCard.classList.contains("is-superseded"), true);
oldCard.querySelectorAll("button").forEach((button) => assert.equal(button.disabled, true));
assert.equal(
oldCard.children.at(-1).textContent,
"已由下方最新草稿替换",
);
});
test("saving the replacement card uses its latest request and clears follow-up context", async () => {
const events = modeDraftEvents("req-draft-before-save", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, ...modeDraftEvents("req-draft-after-edit", 25));
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const latestCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-after-edit"]',
);
latestCard.querySelectorAll("button")[0].dispatchEvent({ type: "click" });
await new Promise((resolve) => setTimeout(resolve, 0));
const saveCall = harness.requestCalls.find((call) => call.url === "/api/mode-profiles");
assert.ok(saveCall);
assert.equal(JSON.parse(saveCall.options.body).source_request_id, "req-draft-after-edit");
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已保存后的回答" },
);
harness.input.value = "把午休模式的空调温度调整到26度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const finalStreamCall = harness.requestCalls.filter(
(call) => call.url === "/api/assistant/stream",
).at(-1);
assert.equal(
Object.hasOwn(JSON.parse(finalStreamCall.options.body), "mode_profile_draft_context"),
false,
);
});
test("starting a new conversation clears the active draft reference", async () => {
const events = modeDraftEvents("req-draft-reset", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
const reset = new MockElement("button");
reset.id = "newConversation";
harness.sandbox.document.dispatchEvent({ type: "click", target: reset });
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "新的对话" },
);
harness.input.value = "普通问题";
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls[1].options.body);
assert.equal(Object.hasOwn(request, "mode_profile_draft_context"), false);
});