fix: support conversational mode draft updates
This commit is contained in:
@@ -72,6 +72,11 @@ class MockElement extends MockEventTarget {
|
||||
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);
|
||||
@@ -79,8 +84,31 @@ class MockElement extends MockEventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
closest(selector) {
|
||||
if (selector.startsWith("#") && this.id === selector.slice(1)) return this;
|
||||
return this.parentElement?.closest?.(selector) || null;
|
||||
}
|
||||
focus() {}
|
||||
querySelector() { return null; }
|
||||
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);
|
||||
@@ -133,7 +161,9 @@ function createHarness(events) {
|
||||
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] : []
|
||||
selector === "#assistantMessages [data-message-role]"
|
||||
? [...messages.children]
|
||||
: messages.querySelectorAll(selector)
|
||||
);
|
||||
|
||||
const storage = new Map();
|
||||
@@ -181,6 +211,15 @@ function createHarness(events) {
|
||||
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);
|
||||
},
|
||||
};
|
||||
@@ -234,3 +273,177 @@ test("a completed NDJSON answer remains the authoritative visible response", asy
|
||||
assert.equal(harness.completed.length, 1);
|
||||
assert.equal(harness.completed[0].error, null);
|
||||
});
|
||||
|
||||
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("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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user