merge: sync feature/N7Web into feature/N7Web-Tech

# Conflicts:
#	apps/web-console/app.js
#	apps/web-console/assistant-chat.js
#	apps/web-console/index.html
#	apps/web-console/service-worker.js
#	src/cognitive_agent/server.py
#	tests/e2e/test_seat_web_api.py
#	tests/unit/test_assistant_frontend_contract.py
#	tests/unit/test_energy_monitor_frontend_contract.py
#	tests/unit/test_frontend_theme_contract.py
#	tests/unit/test_mode_scene_catalog.py
This commit is contained in:
Miayui
2026-09-16 15:03:54 +08:00
68 changed files with 7292 additions and 123 deletions
+178
View File
@@ -0,0 +1,178 @@
const assert = require("node:assert/strict");
const path = require("node:path");
const { chromium } = require("playwright");
const [baseURL, evidenceDir] = process.argv.slice(2);
const desktop = { width: 1600, height: 1000 };
async function main() {
const browser = await chromium.launch({
headless: true,
...(process.env.N7WEB_BROWSER_CHANNEL
? { channel: process.env.N7WEB_BROWSER_CHANNEL }
: process.platform === "win32" ? { channel: "msedge" } : {}),
});
const context = await browser.newContext({ viewport: desktop, serviceWorkers: "block" });
const page = await context.newPage();
const requests = [];
const errors = [];
const unexpectedWrites = [];
let heldResponse = null;
let nextReply = "success";
const answer = (success) => [
{ type: "action.completed", success, payload: {
status: success ? "succeeded" : "failed", execution_mode: "dry_run",
} },
{ type: "answer.completed", answer: success ? "模式执行测试完成" : "模式执行测试失败" },
].map((item) => JSON.stringify(item)).join("\n") + "\n";
const fulfill = (route, success = true) => route.fulfill({
status: 200, contentType: "application/x-ndjson", body: answer(success),
});
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(`${message.text()} ${message.location().url || ''}`);
});
async function handleApiRoute(route) {
const pathname = new URL(route.request().url()).pathname;
if (['/api/hardware/seat-state', '/api/hardware/vehicle-state'].includes(pathname)) {
// This legacy GET may attempt automatic firmware recovery. Never touch
// the physical gateway while verifying browser interactions.
await route.fulfill({json:{status:'unavailable',hardware_execution_allowed:false}});
return;
}
// The isolated server deliberately does not launch Codex or publish a
// physical display mirror. Keep these background integration probes local.
if (pathname === '/api/health' || pathname === '/api/extension/display-sync') {
await route.fulfill({json:{status:'ok',assistant:{status:'ready',available:true}}});
return;
}
if (pathname === "/api/assistant/stream") {
requests.push(route.request().postDataJSON());
if (nextReply === "hold") { heldResponse = route; return; }
const success = nextReply !== "fail";
nextReply = "success";
await fulfill(route, success);
return;
}
if (route.request().method() !== "GET") {
unexpectedWrites.push(route.request().url());
await route.fulfill({ status: 403, body: "test forbids vehicle writes" });
return;
}
await route.continue();
}
await context.route("**/api/**", handleApiRoute);
const rail = page.locator(".mode-chips");
const office = rail.locator('button[data-mode-profile-id="office"]');
const waitIdle = () => page.waitForFunction(() => !window.CognitiveAgentAssistant.isPending());
const waitState = (id, state) => page.waitForFunction(
({ id, state }) => document.querySelector(`.mode-chips [data-mode-profile-id="${id}"]`)?.dataset.modeState === state,
{ id, state },
);
async function expectOneCommand(action, command) {
const before = requests.length;
await action();
await page.waitForFunction(() => document.querySelector('#utterance').getAttribute('aria-busy') === 'false');
await waitIdle();
assert.equal(requests.length, before + 1, "gesture must submit exactly once");
assert.equal(requests.at(-1).message, command);
}
try {
await page.goto(baseURL, { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.querySelectorAll('.mode-chips button').length > 3);
await page.waitForFunction(() => document.body.classList.contains('assistant-chat-enabled'));
assert.equal(await page.title(), "511工程-智能座舱");
assert.equal(new URL(page.url()).pathname, "/");
assert.ok((await page.locator('body').innerText()).includes("整车孪生界面"));
assert.ok(await rail.evaluate((element) => element.scrollWidth > element.clientWidth));
// This is a real mouse sequence: pointerdown -> pointerup -> click. The
// prior pointer capture bug cannot be detected by button.dispatchEvent().
await page.evaluate(() => window.CognitiveAgentAssistant.close());
await expectOneCommand(() => office.click(), "进入办公模式");
await waitState("office", "succeeded");
assert.equal(await page.locator('#assistantDrawerToggle').getAttribute('aria-expanded'), 'true');
assert.ok((await page.locator('#assistantMessages').innerText()).includes('进入办公模式'));
// A small physical hand movement is still a click.
const box = await office.boundingBox();
await expectOneCommand(async () => {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 2, box.y + box.height / 2);
await page.mouse.up();
}, "进入办公模式");
// Dragging from a card must scroll, without accidentally running it.
const beforeDrag = requests.length;
const startScroll = await rail.evaluate((element) => element.scrollLeft);
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 - 90, box.y + box.height / 2, { steps: 8 });
await page.mouse.up();
assert.ok(await rail.evaluate((element) => element.scrollLeft) > startScroll);
assert.equal(requests.length, beforeDrag);
await rail.evaluate((element) => { element.scrollLeft = 0; });
await expectOneCommand(() => office.click(), "进入办公模式");
// Keyboard activation and management cards share the same submission path.
await office.focus();
await expectOneCommand(() => page.keyboard.press('Enter'), "进入办公模式");
await office.focus();
await expectOneCommand(() => page.keyboard.press('Space'), "进入办公模式");
await page.locator('#modeGalleryToggle').click();
await expectOneCommand(() => page.locator('#modeGalleryGroups button[data-mode-profile-id="nap"]').click(), "进入小憩模式");
await waitState("nap", "succeeded");
// Busy requests are not duplicated, nor does another card cancel them.
nextReply = 'hold';
const beforeBusy = requests.length;
await office.click();
await waitState('office', 'running');
await office.click();
assert.equal(requests.length, beforeBusy + 1);
assert.equal(await page.evaluate(() => window.CognitiveAgentAssistant.isPending()), true);
await fulfill(heldResponse);
nextReply = 'success';
await waitIdle();
await waitState('office', 'succeeded');
nextReply = 'fail';
await expectOneCommand(() => office.click(), "进入办公模式");
await waitState('office', 'failed');
// A catalog with few visible cards must also work without overflow.
await rail.evaluate(element => {
for (const child of element.children) {
if (child.dataset.modeProfileId !== 'office') child.style.display = 'none';
}
});
await page.waitForFunction(() => {const e=document.querySelector('.mode-chips');return e.scrollWidth <= e.clientWidth;});
await expectOneCommand(() => office.click(), "进入办公模式");
await rail.evaluate(element => {
for (const child of element.children) child.style.removeProperty('display');
});
await page.setViewportSize(desktop);
await page.screenshot({path:path.join(evidenceDir,'mode-rail-desktop.png')});
assert.deepEqual(errors, []);
assert.deepEqual(unexpectedWrites, []);
// Touch input uses native scrolling and also submits once when tapped.
const touchContext = await browser.newContext({viewport:{width:720,height:1000},hasTouch:true,serviceWorkers:'block'});
await touchContext.route('**/api/**', handleApiRoute);
const touchPage = await touchContext.newPage();
await touchPage.goto(baseURL, {waitUntil:'domcontentloaded'});
await touchPage.waitForFunction(() => document.querySelectorAll('.mode-chips button').length > 3);
const beforeTouch = requests.length;
await touchPage.locator('.mode-chips button[data-mode-profile-id="office"]').tap();
await touchPage.waitForFunction(() => document.querySelector('.mode-chips [data-mode-profile-id="office"]').dataset.modeState === 'succeeded');
assert.equal(requests.length,beforeTouch+1);
assert.equal(requests.at(-1).message,'进入办公模式');
await touchPage.screenshot({path:path.join(evidenceDir,'mode-rail-touch.png')});
await touchContext.close();
console.log('PASS: click, jitter, drag, Enter, Space, gallery, busy, failure, resize, touch; no runtime errors');
} finally {
await browser.close();
}
}
main().catch(error => { console.error(error); process.exitCode = 1; });
+256
View File
@@ -0,0 +1,256 @@
"use strict";
const assert = require("node:assert/strict");
const path = require("node:path");
const { chromium } = require("playwright");
const [baseURL, evidenceDir] = process.argv.slice(2);
async function main() {
const browser = await chromium.launch({
headless: true,
...(process.env.N7WEB_BROWSER_CHANNEL ? { channel: process.env.N7WEB_BROWSER_CHANNEL }
: process.platform === "win32" ? { channel: "msedge" } : {}),
});
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 }, serviceWorkers: "block" });
const page = await context.newPage();
const errors = [];
const posted = [];
const responses = [];
let mockedExecution = null;
let statusReads = 0;
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(`${message.text()} ${message.location().url || ""}`);
});
await context.route("**/api/**", async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
if (["/api/hardware/seat-state", "/api/hardware/vehicle-state"].includes(pathname)) {
await route.fulfill({ json: { status: "unavailable", hardware_execution_allowed: false } });
return;
}
if (request.method() !== "GET") posted.push({ pathname, payload: request.postDataJSON() });
if (mockedExecution && pathname === "/api/assistant/stream") {
await route.fulfill({ contentType: "application/x-ndjson", body: [
{ type: "action.completed", success: true, payload: mockedExecution },
{ type: "answer.completed", answer: "动态时间轴已启动,后续阶段会按计划执行。" },
].map((event) => JSON.stringify(event)).join("\n") + "\n" });
return;
}
if (mockedExecution && pathname === "/api/modes/schedule/status") {
statusReads += 1;
assert.equal(new URL(request.url()).searchParams.get("vehicle_id"), "denza_n7");
await route.fulfill({ json: {
...mockedExecution.schedule_execution, vehicle_id: "denza_n7",
phase_index: 1, repeat_index: 0, completed: false,
status: statusReads === 1 ? "running" : "failed",
error: statusReads === 1 ? null : "QA阶段模拟失败",
} });
return;
}
if (["/api/assistant/stream", "/api/mode-profile-drafts/trial", "/api/mode-profiles"].includes(pathname)) {
// Chromium can discard completed streaming response bodies from CDP.
// Fetch the real isolated endpoint and retain its exact response bytes.
const response = await route.fetch();
const text = await response.text();
responses.push({ pathname, text, status: response.status() });
await route.fulfill({ response, body: text });
return;
}
await route.continue();
});
const waitIdle = () => page.waitForFunction(() => !window.CognitiveAgentAssistant.isPending());
const responseFor = (pathname) => page.waitForResponse((response) => new URL(response.url()).pathname === pathname);
try {
await page.goto(baseURL, { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.classList.contains("assistant-chat-enabled")
&& document.querySelectorAll(".mode-chips button").length > 3);
assert.equal(await page.title(), "511工程-智能座舱");
assert.equal(new URL(page.url()).origin, baseURL);
assert.match(await page.locator("body").innerText(), /整车孪生界面/);
if (!await page.locator('[data-execution-mode="dry_run"]').isVisible()) {
await page.locator('#v2SidebarToggle').click();
}
await page.locator('[data-execution-mode="dry_run"]').click();
assert.equal(await page.evaluate(() => window.CognitiveAgentConsole.getExecutionMode()), "dry_run");
// Exercise the same real assistant endpoint and rendered cards for both
// implicit passenger selection and an explicit driver request. These
// previews stay unsaved, so the timeline save/trial tests remain isolated.
for (const [index, testCase] of [
{ text: "构建一个阅读模式", seat: "passenger", label: "副驾", excluded: "主驾" },
{ text: "构建一个主驾阅读模式", seat: "driver", label: "主驾", excluded: "副驾" },
{ text: "构建一个主驾座椅动态模式", seat: "driver", label: "主驾", excluded: "副驾" },
].entries()) {
const responsePromise = responseFor("/api/assistant/stream");
await page.locator("#utterance").fill(testCase.text);
await page.locator("#utterance").press("Enter");
const response = await responsePromise;
assert.equal(response.status(), 200);
const events = responses.filter((item) => item.pathname === "/api/assistant/stream").at(-1)
.text.trim().split("\n").map((line) => JSON.parse(line));
await waitIdle();
const payload = events.find((event) => event.payload?.mode_profile_draft)?.payload;
assert.ok(payload, `Missing selected-seat draft for ${testCase.text}: ${JSON.stringify(events)}`);
const draft = payload.mode_profile_draft;
const actions = draft.schedule ? draft.schedule.phases.flatMap((phase) => phase.operations) : payload.actions;
const seats = actions.filter((action) => action.capability?.startsWith("seat."))
.map((action) => action.target_scope || action.target?.instance || action.target?.zone);
assert.ok(seats.length, `Missing selected-seat actions for ${testCase.text}`);
assert.deepEqual([...new Set(seats)], [testCase.seat]);
const card = page.locator(`[data-assistant-mode-draft="${draft.source_request_id}"]`);
await card.waitFor({ state: "visible" });
const actionText = await card.locator(".assistant-mode-draft-row").allTextContents();
assert.ok(actionText.some((text) => text.includes(testCase.label)), actionText.join("\n"));
assert.ok(actionText.every((text) => !text.includes(testCase.excluded)), actionText.join("\n"));
await card.scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(evidenceDir, `seat-scope-preview-${index + 1}.png`) });
}
const savedModes = [];
for (const [index, text] of ["构建一个座椅的动态模式", "构建一个上车预热的功能模式"].entries()) {
const responsePromise = responseFor("/api/assistant/stream");
await page.locator("#utterance").fill(text);
await page.locator("#utterance").press("Enter");
const response = await responsePromise;
assert.equal(response.status(), 200);
const events = responses.filter((item) => item.pathname === "/api/assistant/stream").at(-1).text.trim().split("\n").map((line) => JSON.parse(line));
await waitIdle();
const draftEvent = events.find((event) => event.payload?.mode_profile_draft);
assert.ok(draftEvent, `Missing schedule draft for ${text}: ${JSON.stringify(events)}`);
const draft = draftEvent.payload.mode_profile_draft;
assert.equal(draft.schedule?.schema, "cognitive-agent-mode-schedule/v1");
assert.ok(draft.schedule.phases.length >= 2);
const card = page.locator(`[data-assistant-mode-draft="${draft.source_request_id}"]`);
assert.equal(await card.locator(".assistant-mode-schedule-phase").count(), draft.schedule.phases.length);
assert.equal(await card.locator(".assistant-mode-draft-row").count(), draft.operation_count);
assert.match(await card.innerText(), /重复 \d+ 次 · 总时长/);
assert.match(await card.innerText(), /停止动态模式/);
if (index === 0) {
assert.match(await card.innerText(), /副驾/);
assert.doesNotMatch(await card.innerText(), /主驾/);
} else {
const temperatures = draft.schedule.phases.flatMap((phase) => phase.operations)
.filter((operation) => operation.capability === "climate.set_temperature")
.map((operation) => operation.parameters.temperature_c);
assert.ok(new Set(temperatures).size >= 2, "preheat must change temperature over time");
for (const value of new Set(temperatures)) assert.ok((await card.innerText()).includes(`${value}℃`));
}
await card.scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(evidenceDir, `schedule-preview-${index + 1}.png`) });
const trialPromise = responseFor("/api/mode-profile-drafts/trial");
await card.locator(".assistant-mode-draft-trial").click();
const trialResponse = await trialPromise;
const trial = JSON.parse(responses.filter((item) => item.pathname === "/api/mode-profile-drafts/trial").at(-1).text);
assert.equal(trialResponse.status(), 200, JSON.stringify(trial));
assert.equal(trial.status, "succeeded", JSON.stringify(trial));
assert.equal(trial.schedule_execution?.status, "completed");
await page.waitForFunction((requestId) => {
const status = document.querySelector(`[data-assistant-mode-draft="${requestId}"] .assistant-mode-draft-status`);
return status?.classList.contains("success") || status?.classList.contains("failed");
}, draft.source_request_id);
assert.match(await card.locator(".assistant-mode-draft-status").innerText(), /试运行通过/);
const savePromise = responseFor("/api/mode-profiles");
await card.locator(".assistant-mode-draft-save").click();
const saveResponse = await savePromise;
const saved = JSON.parse(responses.filter((item) => item.pathname === "/api/mode-profiles").at(-1).text);
assert.ok([200, 201].includes(saveResponse.status()), JSON.stringify(saved));
assert.ok(saved.mode_profile?.id);
savedModes.push(saved.mode_profile);
await page.locator(".assistant-mode-created").filter({ hasText: saved.mode_profile.name }).waitFor({ state: "visible" });
const button = page.locator(`.mode-chips button[data-mode-profile-id="${saved.mode_profile.id}"]`);
await button.waitFor({ state: "visible" });
// Saving reveals the card with smooth scrolling. A tooltip correctly
// closes while its rail is still moving, so wait for that motion to end.
await page.evaluate(() => new Promise((resolve) => {
const rail = document.querySelector('.mode-chips');
let previous = rail.scrollLeft;
let stable = 0;
const sample = () => {
const current = rail.scrollLeft;
stable = Math.abs(current - previous) < 0.1 ? stable + 1 : 0;
previous = current;
if (stable >= 12) resolve();
else requestAnimationFrame(sample);
};
requestAnimationFrame(sample);
}));
await page.mouse.move(1, 1);
const buttonBox = await button.boundingBox();
const railBox = await page.locator('.mode-chips').boundingBox();
const visibleLeft = Math.max(buttonBox.x, railBox.x);
const visibleRight = Math.min(buttonBox.x + buttonBox.width, railBox.x + railBox.width);
assert.ok(visibleRight > visibleLeft, 'saved card is visible in the mode rail');
// A native mouse move does not request another smooth scroll, unlike
// locator.hover() which scrolls the target to the center first.
await page.mouse.move((visibleLeft + visibleRight) / 2, buttonBox.y + buttonBox.height / 2);
const tooltip = page.locator("#modeChipActionPopover");
await tooltip.waitFor({ state: "visible" });
assert.match(await tooltip.innerText(), /重复 \d+ 次 · 总时长/);
assert.equal(await tooltip.locator(".mode-schedule-popover-phase").count(), draft.operation_count);
for (const phase of draft.schedule.phases) assert.ok((await tooltip.innerText()).includes(phase.name));
await tooltip.screenshot({ path: path.join(evidenceDir, `schedule-tooltip-${index + 1}.png`), timeout: 4000 });
await page.mouse.move(1, 1);
}
await page.locator("#modeGalleryToggle").click();
for (const mode of savedModes) {
const shell = page.locator(`#modeGalleryGroups .mode-gallery-card-shell`).filter({ has: page.locator(`button[data-mode-profile-id="${mode.id}"]`) });
const details = shell.locator(".mode-gallery-schedule-details");
await details.locator("summary").click();
assert.match(await details.innerText(), /开始 · 持续/);
assert.match(await details.innerText(), /不自动回滚/);
}
await page.locator("#modeGalleryGroups").evaluate((element) => { element.scrollTop = 0; });
await page.screenshot({ path: path.join(evidenceDir, "schedule-management-desktop.png") });
await page.locator("#modeGalleryClose").click();
await page.setViewportSize({ width: 720, height: 1000 });
if (await page.locator('#v2SidebarToggle').getAttribute('aria-expanded') === 'true') {
await page.locator('#v2SidebarToggle').click();
}
await page.locator(".assistant-mode-draft-scheduled").last().scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(evidenceDir, "schedule-preview-mobile.png") });
assert.equal(await page.locator("body").evaluate((body) => body.scrollWidth > window.innerWidth + 1), false);
// The backend sequence above is real and strictly dry-run. Separately
// verify the asynchronous UI contract with a synthetic running/failed
// response; it must never launch an actual timed hardware execution.
await page.setViewportSize({ width: 1600, height: 1000 });
const mode = savedModes[1];
mockedExecution = {
request_id: "browser-running-qa", status: "running", execution_mode: "dry_run",
orchestration_route: "mode_schedule_execute", vehicle_id: "denza_n7", mode_profile: mode,
schedule_execution: { status: "running", run_id: "qa-run", session_id: "qa-session" },
};
const messagesBefore = await page.locator('#assistantMessages [data-message-role]').count();
await page.locator(`.mode-chips button[data-mode-profile-id="${mode.id}"]`).click();
await waitIdle();
await page.waitForFunction((id) => document.querySelector(`.mode-chips button[data-mode-profile-id="${id}"]`)?.dataset.modeState === "started", mode.id);
const actionStatus = page.locator('.assistant-action-status').last();
assert.match(await actionStatus.innerText(), /已启动动态时间轴/);
await page.waitForFunction(() => Array.from(document.querySelectorAll('.assistant-action-status')).at(-1)?.textContent.includes('阶段 2'));
await page.waitForFunction(() => Array.from(document.querySelectorAll('.assistant-action-status')).at(-1)?.textContent.includes('QA阶段模拟失败'));
assert.equal(await actionStatus.getAttribute('class'), 'assistant-action-status failed');
assert.equal(statusReads, 2);
assert.equal(await page.locator('#assistantMessages [data-message-role]').count(), messagesBefore + 2);
await page.screenshot({ path: path.join(evidenceDir, "schedule-background-failure.png") });
assert.deepEqual(errors, []);
assert.ok(posted.filter((request) => request.pathname === "/api/assistant/stream").every((request) => request.payload.execution_mode === "dry_run"));
assert.equal(posted.filter((request) => request.pathname === "/api/mode-profile-drafts/trial").length, 2);
assert.equal(posted.filter((request) => request.pathname === "/api/mode-profiles").length, 2);
console.log(`PASS ${baseURL}: default passenger and explicit driver static/dynamic previews -> both exact user phrases -> phased preview -> virtual trial -> save -> top tooltip -> management; desktop 1600x1000 and 720x1000; no JS errors; no hardware requests forwarded`);
console.log(`Evidence: ${evidenceDir}`);
} catch (error) {
await page.screenshot({ path: path.join(evidenceDir, "schedule-browser-failure.png") });
console.error(`Page text: ${(await page.locator("body").innerText()).slice(-5000)}`);
throw error;
} finally {
await context.close();
await browser.close();
}
}
main().catch((error) => { console.error(error); process.exitCode = 1; });
+93
View File
@@ -0,0 +1,93 @@
"""Exercise real pointer/click dispatch on the complete console in Chromium."""
from __future__ import annotations
import os
import shutil
import subprocess
import threading
from http import HTTPStatus
from http.server import ThreadingHTTPServer
from pathlib import Path
from unittest.mock import patch
from urllib.parse import urlparse
import pytest
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.auth import AuthConfig, AuthRuntime
from cognitive_agent.server import REPOSITORY_ROOT, SeatDemoHandler
def test_mode_rail_real_browser_interactions(tmp_path: Path) -> None:
node = shutil.which("node")
if not node:
pytest.skip("Node.js is required for the browser interaction regression")
env = dict(os.environ)
bundled_modules = Path(node).resolve().parent.parent / "node_modules"
if bundled_modules.is_dir():
env["NODE_PATH"] = os.pathsep.join(
item for item in (str(bundled_modules), env.get("NODE_PATH")) if item
)
available = subprocess.run(
[node, "-e", "require.resolve('playwright')"],
env=env,
capture_output=True,
encoding="utf-8",
check=False,
)
if available.returncode:
pytest.skip("Playwright is required for the real browser regression")
with patch.dict(
os.environ,
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
},
):
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = tmp_path / "artifacts"
class BrowserHandler(SeatDemoHandler):
auth = AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
def do_GET(self) -> None:
if urlparse(self.path).path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
self._json({"status": "unavailable", "hardware_execution_allowed": False})
return
super().do_GET()
def do_POST(self) -> None:
# Only GET reaches this test server. Browser requests to the
# assistant are intercepted; unexpected writes fail closed.
self._json({"error": "browser_test_write_forbidden"}, HTTPStatus.FORBIDDEN)
def log_message(self, *_args: object) -> None:
pass
BrowserHandler.service = service
server = ThreadingHTTPServer(("127.0.0.1", 0), BrowserHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
completed = subprocess.run(
[
node,
str(Path(__file__).with_name("mode_rail_browser.cjs")),
f"http://127.0.0.1:{server.server_port}",
str(tmp_path),
],
env=env,
capture_output=True,
encoding="utf-8",
timeout=120,
check=False,
)
assert completed.returncode == 0, completed.stdout + completed.stderr
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
service.close()
+109
View File
@@ -0,0 +1,109 @@
"""Full browser -> deterministic assistant -> schedule trial/save, without hardware."""
from __future__ import annotations
import os
import shutil
import subprocess
import threading
from http import HTTPStatus
from http.server import ThreadingHTTPServer
from pathlib import Path
from unittest.mock import MagicMock, patch
from urllib.parse import urlparse
import pytest
from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.auth import AuthConfig, AuthRuntime
from cognitive_agent.server import REPOSITORY_ROOT, SeatDemoHandler
def test_mode_schedule_real_browser_preview_trial_and_save(tmp_path: Path) -> None:
node = shutil.which("node")
if not node:
runtime = Path.home() / ".cache" / "codex-runtimes" / "codex-primary-runtime" / "dependencies" / "node" / "bin"
node = next((str(path) for path in (runtime / "node.exe", runtime / "node") if path.is_file()), None)
if not node:
pytest.skip("Node.js is required for the schedule browser regression")
env = dict(os.environ)
modules = Path(node).resolve().parent.parent / "node_modules"
if modules.is_dir():
env["NODE_PATH"] = os.pathsep.join(value for value in (str(modules), env.get("NODE_PATH")) if value)
probe = subprocess.run([node, "-e", "require.resolve('playwright')"], env=env, capture_output=True, encoding="utf-8", check=False)
if probe.returncode:
pytest.skip("Playwright is required for the schedule browser regression")
with patch.dict(os.environ, {
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run",
"COGNITIVE_AGENT_MODE_PROFILE_ROOT": str(tmp_path / "user-modes"),
}):
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = tmp_path / "artifacts"
# A failed deterministic route must fail visibly, never launch a model or
# make network calls to an external assistant during the browser test.
client = MagicMock()
client.available = False
client.status = "test-offline"
assistant = CodexAssistantService(client, fast_implicit_controls=True)
forbidden = []
class BrowserHandler(SeatDemoHandler):
auth = AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
def do_GET(self) -> None: # noqa: N802
path = urlparse(self.path).path
if path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
# Both legacy GET paths can touch physical adapters or firmware.
self._json({"status": "unavailable", "hardware_execution_allowed": False})
return
if path == "/api/health":
self._json({"status": "ok", "assistant": {"status": "ready", "available": True}})
return
super().do_GET()
def do_POST(self) -> None: # noqa: N802
path = urlparse(self.path).path
if path == "/api/extension/display-sync":
self._json({"status": "ok"})
return
if path not in {"/api/assistant/stream", "/api/mode-profile-drafts/trial", "/api/mode-profiles"}:
forbidden.append(path)
self._json({"error": "browser_test_write_forbidden"}, HTTPStatus.FORBIDDEN)
return
super().do_POST()
def _read_json_body(self, **kwargs):
payload = super()._read_json_body(**kwargs)
if payload.get("execution_mode", "dry_run") != "dry_run":
forbidden.append(f"execution_mode:{payload.get('execution_mode')}")
raise ValueError("Browser regression permits dry_run only")
return payload
def log_message(self, *_args: object) -> None:
pass
BrowserHandler.service = service
BrowserHandler.assistant = assistant
server = ThreadingHTTPServer(("127.0.0.1", 0), BrowserHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
completed = subprocess.run([
node, str(Path(__file__).with_name("mode_schedule_browser.cjs")),
f"http://127.0.0.1:{server.server_port}", str(tmp_path),
], env=env, capture_output=True, encoding="utf-8", timeout=180, check=False)
assert completed.returncode == 0, completed.stdout + completed.stderr
assert not forbidden, forbidden
assert len(list((tmp_path / "user-modes").rglob("*.yaml"))) == 2
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
assistant.close()
service.close()
+70 -2
View File
@@ -52,6 +52,7 @@ class SeatWebApiTests(unittest.TestCase):
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
},
clear=False,
):
@@ -306,6 +307,37 @@ class SeatWebApiTests(unittest.TestCase):
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertIn("GET", response.headers["Access-Control-Allow-Methods"])
def test_schedule_status_stays_authenticated_and_sanitized_beside_public_catalog(self) -> None:
endpoint = f"{self.base_url}/api/modes/schedule/status?vehicle_id=denza_n7"
unauthenticated = urllib.request.build_opener()
with self.assertRaises(urllib.error.HTTPError) as caught:
unauthenticated.open(endpoint, timeout=3)
self.assertEqual(caught.exception.code, 401)
state = {
"status": "running", "vehicle_id": "denza_n7", "session_id": "conv-mode-schedule-api",
"run_id": "schedule-private-safe-id", "phase_index": 0, "repeat_index": 0,
"phase_count": 2, "repeat_count": 2, "executed_phases": 0,
"elapsed_ms": 250, "total_duration_ms": 6000, "completed": False,
"phase_results": [{"raw_can": "private-command-bytes"}],
"hardware_client_id": "private-bench-client",
"error": "private transport stack or secret",
}
with (
patch.object(self.service.mode_schedule_runner, "status", return_value=state) as status,
self.opener.open(endpoint, timeout=3) as response,
):
payload = json.loads(response.read().decode("utf-8"))
status.assert_called_once_with("denza_n7")
self.assertEqual(payload["status"], "running")
self.assertEqual(payload["phase_count"], 2)
self.assertEqual(payload["total_duration_ms"], 6000)
self.assertEqual(payload["completed"], False)
self.assertIn("后续阶段已停止", payload["error"])
self.assertNotIn("phase_results", payload)
self.assertNotIn("hardware_client_id", payload)
self.assertNotIn("private-command-bytes", json.dumps(payload))
self.assertNotIn("private transport", json.dumps(payload))
def test_user_mode_delete_endpoint_removes_only_user_profiles(self) -> None:
original_registry = self.service.mode_profiles
with tempfile.TemporaryDirectory() as directory:
@@ -2489,6 +2521,9 @@ operations:
if item["type"] == "validated_commands"
]
self.assertTrue(validated_payloads)
# The local-first stage publishes known IR controls before CAN parsing;
# subsequent evidence must include the complete combined command set.
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
self.assertTrue(
any(
{"native_climate", "self_extension"}.issubset(
@@ -2497,6 +2532,9 @@ operations:
for payload in validated_payloads
)
)
self.assertTrue(
all("self_extension" in payload["function_configs"] for payload in validated_payloads)
)
first_validated = next(
payload
for payload in validated_payloads
@@ -2749,11 +2787,16 @@ operations:
self.assertEqual(events[-1]["type"], "error")
self.assertEqual(events[-1]["error"], "can_transport_failed")
validated = [
validated_payloads = [
item["payload"]
for item in events
if item["type"] == "validated_commands"
][-1]
]
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
validated = next(
payload for payload in validated_payloads
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
)
self.assertEqual(validated["status"], "processing")
self.assertEqual(
set(validated["function_configs"]),
@@ -3126,6 +3169,31 @@ operations:
planner.assert_called_once()
self.assertEqual(planner.call_args.args[0]["utterance"], utterance)
def test_peripheral_timing_aliases_preserve_sequence_before_local_action_execution(self) -> None:
handler = object.__new__(SeatDemoHandler)
handler.service = SimpleNamespace(is_mode_profile_configuration_request=lambda _text: False)
capabilities = [
{"id": identity, "status": "learned", "device_id": "third-party-light",
"device_name": "第三方氛围灯", "card_uid": "1A53DD95", "action": action, "slot": slot}
for identity, action, slot in (("on", "开启灯光", 1), ("red", "红色灯光", 2), ("off", "关闭灯光", 3))
]
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: capabilities)
handler.nfc = SimpleNamespace(snapshot=lambda: {
"reader": {"online": True}, "card": {"action": "present", "uid": "1A53DD95"},
})
for duration in ("5s", "5S", "500ms", "500MS", "1.5s", "5秒", "半分钟"):
with (
self.subTest(duration=duration),
patch.object(handler, "_execute_active_peripheral_request", return_value={"status": "succeeded"}) as planner,
patch.object(handler, "_execute_local_peripheral_actions", side_effect=AssertionError("sequence flattened")),
):
result = handler._execute_request_with_peripherals("/api/requests", {
"utterance": f"打开第三方氛围灯,红灯闪烁{duration}后,最后关闭第三方氛围灯",
"provider": "local", "execution_mode": "dry_run",
})
self.assertEqual(result["status"], "succeeded")
planner.assert_called_once()
def test_post_request_returns_complete_dry_run(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests",
@@ -715,9 +715,16 @@ def test_every_catalog_mode_rejects_incompatible_explicit_seat_scopes(
for mode_index, mode in enumerate(modes):
assert isinstance(mode, Mapping)
profile = vehicle_service.mode_profiles.get_profile(str(mode["id"]), "denza_n7")
has_driver = any(
operation.domain == "seat" and operation.target_scope in {"driver", "front_left"}
for operation in profile.operations
)
for scope_index, scope in enumerate(
("主驾", "驾驶员", "后排", "二排", "前排", "所有座椅", "全车座椅")
):
if has_driver and scope in {"主驾", "驾驶员"}:
continue # Valid dual-seat subsets have a separate exact-action matrix.
message = f"请让{scope}进入{mode['name']}"
result = vehicle_service.preview_native_can(
message,
@@ -754,7 +761,7 @@ def test_every_catalog_mode_rejects_incompatible_explicit_seat_scopes(
def test_preview_and_process_share_mode_target_safety_gate(
vehicle_service: SeatDemoService,
) -> None:
message = "主驾开启小憩模式"
message = "主驾开启零重力模式"
direct = vehicle_service.process(
message,
provider="local",
@@ -754,7 +754,11 @@ def test_incomplete_ambiguous_or_unsupported_scene_never_creates_partial_draft(
assert result["status"] != "succeeded"
assert result.get("mode_profile_draft") is None
assert result.get("orchestration_route") != "mode_profile_create_preview"
# Seat policy can reject on the creation route before compiling any action.
assert not (result.get("intent_ir") or {}).get("spec", {}).get("actions")
assert not result.get("actions")
assert result.get("command_plan") is None
assert not result.get("command_plans")
assert result.get("validation")
failure = result.get("mode_creation_failure")
assert isinstance(failure, dict)
@@ -0,0 +1,471 @@
"""Creation selects one seat by default without remapping incompatible actuators."""
from __future__ import annotations
import json
import queue
from copy import deepcopy
from unittest.mock import MagicMock, patch
import pytest
from jsonschema import Draft202012Validator
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.codex_assistant import ActiveCodexTurn, CodexAssistantService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.scene_composer import normalize_scene_output
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@pytest.fixture
def policy_service(tmp_path, monkeypatch):
for variable in (
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED",
):
monkeypatch.setenv(variable, "false")
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = tmp_path / "artifacts"
user_root = tmp_path / "user-modes"
service.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes", user_root=user_root,
)
try:
with (
patch.object(service.hardware_execution, "reserve", side_effect=AssertionError("physical CAN forbidden")) as reserve,
patch.object(service._vehicle_mqtt_transport, "bus", side_effect=AssertionError("physical MQTT forbidden")) as mqtt,
):
yield service, AssistantCapabilityCatalog(service), user_root
reserve.assert_not_called()
mqtt.assert_not_called()
finally:
service.close()
def _actions(result):
return (result.get("intent_ir") or {}).get("spec", {}).get("actions", [])
def _seat_scopes(result):
return {
action["target"]["instance"] for action in _actions(result)
if action["domain"] == "seat"
}
def _operation_key(operation):
target = operation.get("target_scope")
if target is None:
raw_target = operation.get("target") or {}
target = raw_target.get("instance") or raw_target.get("zone")
return (
operation["capability"], target,
json.dumps(operation["parameters"], ensure_ascii=False, sort_keys=True),
)
def _preview(service, message, *, plan=None, draft=None):
return service.preview_native_can(
message, provider="local", conversation_id="conv-seat-policy",
execution_mode="hardware", vehicle_id="denza_n7",
assistant_plan=plan, mode_profile_draft_context=draft,
)
def _assert_created(result, scopes):
assert result["status"] == "succeeded", result.get("validation")
assert result.get("mode_profile_draft"), result
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert _seat_scopes(result) == set(scopes)
schedule = result.get("schedule")
if schedule:
for phase in schedule["phases"]:
assert {
op["target_scope"] for op in phase["operations"] if op["domain"] == "seat"
} <= set(scopes)
assert result["mode_profile_draft"]["schedule"] == schedule
def _assistant_preview(service, catalog, message, *, client=None):
if client is None:
client = MagicMock()
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
fast = catalog.fast_answer(message, snapshot)
results, events = [], []
def execute(instruction, progress, assistant_plan=None):
result = _preview(service, instruction, plan=assistant_plan)
results.append(result)
return result
response = assistant.stream(
message, conversation_id="conv-seat-policy", principal="seat-policy-test", history=[],
authoritative_answer=fast.answer if fast else None,
emit=lambda event: events.append(event) or True,
execute_control=execute, capability_snapshot=snapshot,
capability_context=catalog.compact_context(snapshot),
scene_capability_context=catalog.scene_composition_context(snapshot),
control_terms=catalog.control_terms(snapshot),
)
assert len(results) == 1, (response, results)
return response, results[0], client
@pytest.mark.parametrize("name", [
"阅读模式", "观影模式", "办公模式", "座椅舒适模式", "放松模式", "座椅动态模式", "上车预热模式",
])
@pytest.mark.parametrize(("prefix", "scopes"), [
("", {"passenger"}), ("主驾", {"driver"}), ("主驾和副驾", {"driver", "passenger"}),
])
def test_registered_modes_use_selected_seat_through_assistant_fast_path(policy_service, name, prefix, scopes):
service, catalog, user_root = policy_service
response, preview, client = _assistant_preview(service, catalog, f"构建一个{prefix}{name}")
assert response["success"] is True, (response, preview)
_assert_created(preview, scopes)
client.start_thread.assert_not_called()
client.start_turn.assert_not_called()
client.start_structured_turn.assert_not_called()
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("message", "scopes"), [
("构建一个两个座椅的阅读模式", {"driver", "passenger"}),
("构建一个两张座椅的阅读模式", {"driver", "passenger"}),
("构建一个两座椅的阅读模式", {"driver", "passenger"}),
("构建一个前排两座的阅读模式", {"driver", "passenger"}),
("构建一个副驾驶席阅读模式", {"passenger"}),
("构建一个副驾驶位阅读模式", {"passenger"}),
("构建一个副驾驶座椅阅读模式", {"passenger"}),
("构建一个两个座椅的动态模式", {"driver", "passenger"}),
("构建一个两张座椅的动态模式", {"driver", "passenger"}),
("构建一个两座椅的动态模式", {"driver", "passenger"}),
("构建一个前排两座的动态模式", {"driver", "passenger"}),
("构建一个副驾驶座椅动态模式", {"passenger"}),
])
def test_registered_seat_aliases_select_correct_targets_without_model(policy_service, message, scopes):
service, catalog, _ = policy_service
response, preview, client = _assistant_preview(service, catalog, message)
assert response["success"] is True, (
response["answer"], preview.get("validation"), preview.get("mode_profile_draft"),
)
_assert_created(preview, scopes)
assert response["route"] == "scene_template"
client.start_structured_turn.assert_not_called()
client.start_turn.assert_not_called()
@pytest.mark.parametrize("template_id", [
"reading", "cinema", "camping", "quiet-night", "reception", "romantic",
"winter-warmth", "summer-cooling", "long-trip-comfort", "relaxation", "seat-comfort",
"seat-dynamic", "boarding-warmup",
])
@pytest.mark.parametrize(("prefix", "scope"), [("", "passenger"), ("主驾", "driver")])
def test_trusted_template_selection_keeps_nonseat_devices_and_native_parameters(policy_service, template_id, prefix, scope):
service, _, _ = policy_service
template = service.scene_templates.get_template(template_id)
original = deepcopy(template.plan())
result = _preview(service, f"构建一个{prefix}{template.name}", plan=template.plan())
_assert_created(result, {scope})
raw_operations = (
[op for phase in original["schedule"]["phases"] for op in phase["operations"]]
if original.get("schedule") else [op.to_yaml() for op in template.operations]
)
expected = [
_operation_key(op) for op in raw_operations
if op["domain"] != "seat" or op["target_scope"] == scope
]
assert sorted(map(_operation_key, _actions(result))) == sorted(expected)
assert template.plan() == original, "projecting a request must not mutate the registered template"
def _model_output(scopes, *, dynamic=False):
requests = []
if "driver" in scopes:
requests.append({
"capability": "seat.set_driver_massage_native", "target_scope": "driver",
"parameters": {"action": "mode", "mode": "relax"},
})
if "passenger" in scopes:
requests.append({
"capability": "seat.set_backrest_recline", "target_scope": "passenger",
"parameters": {"position_pct": 45},
})
requests.append({
"capability": "climate.set_temperature", "target_scope": "vehicle",
"parameters": {"temperature_c": 24},
})
if not dynamic:
return {"requests": requests, "ambiguities": [], "confidence": 0.95}
return {
"requests": [], "ambiguities": [], "confidence": 0.95,
"schedule": {
"repeat_count": 2,
"phases": [
{"id": "relax", "name": "舒缓", "duration_ms": 30000, "requests": deepcopy(requests)},
{"id": "support", "name": "支撑", "duration_ms": 60000, "requests": deepcopy(requests)},
],
},
}
@pytest.mark.parametrize("dynamic", [False, True])
@pytest.mark.parametrize(("prefix", "scopes"), [
("", {"passenger"}), ("主驾", {"driver"}), ("主驾和副驾", {"driver", "passenger"}),
])
def test_unregistered_model_plan_preserves_correct_explicit_or_default_targets(policy_service, dynamic, prefix, scopes):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output(scopes, dynamic=dynamic), catalog.scene_composition_context(), dynamic=dynamic,
)
dynamic_label = "动态" if dynamic else ""
result = _preview(service, f"构建一个{prefix}星河释压{dynamic_label}模式", plan=plan)
_assert_created(result, scopes)
assert result["mode_profile_draft"]["generation_source"] == "codex_scene_composition"
@pytest.mark.parametrize("dynamic", [False, True])
@pytest.mark.parametrize("prefix", ["", "主驾", "副驾"])
def test_unregistered_model_cannot_invent_cross_seat_actions(policy_service, dynamic, prefix):
service, catalog, user_root = policy_service
plan = normalize_scene_output(
_model_output({"driver", "passenger"}, dynamic=dynamic),
catalog.scene_composition_context(), dynamic=dynamic,
)
try:
result = _preview(service, f"构建一个{prefix}星河释压{'动态' if dynamic else ''}模式", plan=plan)
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert result["hardware_execution_allowed"] is False
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("prefix", "requested_scope", "unexpected_scope"), [
("", "passenger", "driver"), ("主驾", "driver", "passenger"),
])
def test_unexpected_seat_only_in_later_dynamic_phase_rejects_entire_draft(
policy_service, prefix, requested_scope, unexpected_scope,
):
service, catalog, user_root = policy_service
output = _model_output({requested_scope}, dynamic=True)
output["schedule"]["phases"][1]["requests"] = _model_output({unexpected_scope})["requests"]
plan = normalize_scene_output(output, catalog.scene_composition_context(), dynamic=True)
try:
result = _preview(service, f"构建一个{prefix}星河释压动态模式", plan=plan)
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert not result.get("command_plans")
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize("message", [
"构建一个办公模式,空调先24度持续30秒,然后24度持续60秒,循环2次",
"构建一个办公模式,主驾座椅分两个阶段动态舒缓",
])
def test_existing_mode_binding_cannot_authorize_cross_seat_model_schedule(policy_service, message):
service, catalog, user_root = policy_service
original = service.mode_profiles.get_profile("office", "denza_n7")
revision = service.mode_profiles.profile_revision(original)
plan = normalize_scene_output(
_model_output({"driver", "passenger"}, dynamic=True),
catalog.scene_composition_context(), dynamic=True,
)
try:
result = _preview(service, message, plan=plan)
except ValueError:
pass
else:
assert result["status"] != "succeeded", "binding an existing mode is not authorization for model-added seats"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert not result.get("command_plans")
assert service.mode_profiles.profile_revision(service.mode_profiles.get_profile("office", "denza_n7")) == revision
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("message", "scope"), [
("构建一个办公模式,空调先24度持续30秒,然后24度持续60秒,循环2次", "passenger"),
("构建一个办公模式,主驾座椅分两个阶段动态舒缓", "driver"),
])
def test_existing_mode_binding_keeps_valid_selected_seat_schedule_saveable(policy_service, message, scope):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output({scope}, dynamic=True), catalog.scene_composition_context(), dynamic=True,
)
result = _preview(service, message, plan=plan)
_assert_created(result, {scope})
draft = result["mode_profile_draft"]
assert draft["existing_mode_id"] == "office"
assert draft["existing_mode_revision"]
saved = service.save_mode_profile_card(
draft["existing_mode_id"], draft["name"], draft["instruction"],
draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded"
assert saved["mode_profile"]["schedule"] == result["schedule"]
executed = service.execute_mode_profile_card(
saved["mode_profile"]["id"], execution_mode="dry_run", vehicle_id="denza_n7",
)
assert executed["status"] == "succeeded"
assert _seat_scopes(executed) == {scope}
@pytest.mark.parametrize("dynamic", [False, True])
def test_default_seat_preference_does_not_force_seat_actions_into_nonseat_scene(policy_service, dynamic):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output(set(), dynamic=dynamic), catalog.scene_composition_context(), dynamic=dynamic,
)
result = _preview(service, f"构建一个星河恒温{'动态' if dynamic else ''}模式", plan=plan)
_assert_created(result, set())
assert all(action["domain"] == "climate" for action in _actions(result))
def test_explicit_atomic_mode_requirements_default_unqualified_seat_to_passenger(policy_service):
service, _, _ = policy_service
result = _preview(service, "构建一个自定义支撑模式,座椅靠背调整到45%,空调24度")
_assert_created(result, {"passenger"})
assert {_operation_key(action) for action in _actions(result)} == {
("seat.set_backrest_recline", "passenger", '{"position_pct": 45}'),
# Only the seat default changes; the existing local HVAC zone stays driver.
("climate.set_temperature", "driver", '{"temperature_c": 24}'),
}
@pytest.mark.parametrize("dynamic", [False, True])
def test_passenger_position_capability_is_not_retargeted_to_driver(policy_service, dynamic):
service, catalog, _ = policy_service
output = _model_output({"passenger"}, dynamic=dynamic)
requests = output["schedule"]["phases"][0]["requests"] if dynamic else output["requests"]
requests[0]["target_scope"] = "driver"
try:
plan = normalize_scene_output(output, catalog.scene_composition_context(), dynamic=dynamic)
result = _preview(service, f"构建一个主驾星河释压{'动态' if dynamic else ''}模式", plan=plan)
except ValueError:
return
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
@pytest.mark.parametrize(("message", "scopes"), [
("构建一个主驾空调与座椅舒适模式", {"passenger"}),
("构建一个主驾阅读灯与座椅舒适模式", {"passenger"}),
("构建一个副驾空调与主驾座椅舒适模式", {"driver"}),
("构建一个主驾阅读灯与副驾座椅舒适模式", {"passenger"}),
])
def test_other_device_zones_do_not_select_or_broaden_seat_scope(policy_service, message, scopes):
service, catalog, _ = policy_service
plan = normalize_scene_output(_model_output(scopes), catalog.scene_composition_context())
result = _preview(service, message, plan=plan)
_assert_created(result, scopes)
@pytest.mark.parametrize(("template_id", "prefix", "scope"), [
("reading", "", "passenger"), ("reading", "主驾", "driver"),
("seat-dynamic", "", "passenger"), ("seat-dynamic", "主驾", "driver"),
("boarding-warmup", "", "passenger"), ("boarding-warmup", "主驾", "driver"),
])
def test_save_reload_and_simulated_execution_preserve_selected_seat(policy_service, template_id, prefix, scope):
service, _, user_root = policy_service
template = service.scene_templates.get_template(template_id)
preview = _preview(service, f"构建一个{prefix}{template.name}", plan=template.plan())
_assert_created(preview, {scope})
draft = preview["mode_profile_draft"]
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded", saved
service.mode_profiles = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes", user_root=user_root)
executed = service.execute_mode_profile_card(
saved["mode_profile"]["id"], conversation_id="conv-seat-policy-replay",
execution_mode="dry_run", vehicle_id="denza_n7",
)
assert executed["status"] == "succeeded", executed.get("validation")
assert executed["hardware_execution_allowed"] is False
assert _seat_scopes(executed) == {scope}
assert sorted(map(_operation_key, _actions(executed))) == sorted(map(_operation_key, _actions(preview)))
if preview.get("schedule"):
assert executed["schedule"] == preview["schedule"]
assert executed["schedule_execution"]["completed"] is True
assert all(
_seat_scopes(phase["result"]) <= {scope}
for phase in executed["schedule_execution"]["phase_results"]
)
@pytest.mark.parametrize(("initial_scope", "prefix"), [("driver", "主驾"), ("passenger", "")])
def test_unrelated_draft_edit_retains_original_seat_and_explicit_edit_may_extend_it(policy_service, initial_scope, prefix):
service, catalog, _ = policy_service
name = f"{prefix}星河释压模式"
built = _preview(
service, f"构建一个{name}",
plan=normalize_scene_output(_model_output({initial_scope}), catalog.scene_composition_context()),
)
_assert_created(built, {initial_scope})
draft = built["mode_profile_draft"]
revised = _preview(
service, f"把{name}的空调温度调整到26度",
draft={key: draft[key] for key in ("name", "source_request_id", "vehicle_id")},
)
_assert_created(revised, {initial_scope})
original_seat_ops = {_operation_key(a) for a in _actions(built) if a["domain"] == "seat"}
assert {_operation_key(a) for a in _actions(revised) if a["domain"] == "seat"} == original_seat_ops
added = "副驾座椅通风调整到1档" if initial_scope == "driver" else "主驾座椅通风调整到低档"
draft = revised["mode_profile_draft"]
extended = _preview(
service, f"把{name}的{added}",
draft={key: draft[key] for key in ("name", "source_request_id", "vehicle_id")},
)
_assert_created(extended, {"driver", "passenger"})
assert {_operation_key(a) for a in _actions(extended) if a["domain"] == "seat"} >= original_seat_ops
def _structured_client(output):
client = MagicMock()
client.available = True
client.status = "ready"
client.start_thread.return_value = "thread-seat-policy"
client.start_turn.side_effect = AssertionError("scene fell back to unconstrained agent")
def start(thread_id, text, *, application_context, output_schema, timeout=None):
Draft202012Validator(output_schema).validate(output)
turn = ActiveCodexTurn(thread_id=thread_id, turn_id="turn-seat-policy", events=queue.Queue())
turn.events.put({"method": "item/completed", "params": {"item": {
"id": "answer", "type": "agentMessage", "text": json.dumps(output, ensure_ascii=False),
}}})
turn.events.put({"method": "turn/completed", "params": {"turn": {
"id": turn.turn_id, "status": "completed", "items": [], "error": None,
}}})
return turn
client.start_structured_turn.side_effect = start
return client
@pytest.mark.parametrize(("prefix", "scopes"), [("", {"passenger"}), ("主驾", {"driver"})])
@pytest.mark.parametrize("dynamic", [False, True])
def test_unregistered_assistant_structured_path_uses_seat_policy(policy_service, prefix, scopes, dynamic):
service, catalog, _ = policy_service
client = _structured_client(_model_output(scopes, dynamic=dynamic))
response, preview, _ = _assistant_preview(
service, catalog, f"构建一个{prefix}星河释压{'动态' if dynamic else ''}模式", client=client,
)
assert response["success"] is True, (response, preview)
assert response["route"] == "scene_composer"
_assert_created(preview, scopes)
client.start_structured_turn.assert_called_once()
client.start_turn.assert_not_called()
@@ -1237,6 +1237,10 @@ def test_known_mode_name_reuses_repository_profile_even_if_a_plan_is_supplied(
) -> None:
service, user_root = mode_service
nap = service.mode_profiles.get_profile("nap", "denza_n7")
selected_operations = [
operation for operation in nap.operations
if operation.domain != "seat" or operation.target_scope in {"passenger", "front_right"}
]
result = _preview_without_hardware(
service,
"构建一个小憩模式",
@@ -1249,15 +1253,16 @@ def test_known_mode_name_reuses_repository_profile_even_if_a_plan_is_supplied(
draft = _assert_successful_preview(
result,
name=nap.name,
operation_count=len(nap.operations),
operation_count=len(selected_operations),
)
assert draft["generation_source"] == "registered_mode_profile"
assert draft["existing_mode_id"] == "nap"
expected = {
(operation.capability, operation.target_scope): operation.parameters
for operation in nap.operations
for operation in selected_operations
}
actual = _action_map(result)
assert {target for capability, target in actual if capability.startswith("seat.")} == {"passenger"}
for (capability, target), parameters in expected.items():
rendered_target = "passenger" if target == "front_right" and capability.startswith("seat.") else target
for name, value in parameters.items():
@@ -0,0 +1,670 @@
"""Dynamic scene creation through the real assistant, compiler and saved-mode APIs."""
from __future__ import annotations
import json
import queue
from copy import deepcopy
from threading import Event
from unittest.mock import MagicMock, patch
import pytest
from jsonschema import Draft202012Validator
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.codex_assistant import ActiveCodexTurn, CodexAssistantService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.mode_schedule_service import public_schedule_status
from cognitive_agent.application.scene_composer import (
SceneCompositionError,
build_scene_output_schema,
normalize_scene_output,
)
from cognitive_agent.application.schedule_intent import needs_mode_schedule
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.access import HardwareAccessRequiredError
@pytest.fixture
def schedule_service(tmp_path, monkeypatch):
for variable in (
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED", "COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED", "COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED",
):
monkeypatch.setenv(variable, "false")
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = tmp_path / "artifacts"
user_root = tmp_path / "user-modes"
service.mode_profiles = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes", user_root=user_root)
with (
patch.object(service.hardware_execution, "reserve", side_effect=AssertionError("physical vehicle access forbidden")) as reserve,
patch.object(service._vehicle_mqtt_transport, "bus", side_effect=AssertionError("physical MQTT access forbidden")),
):
try:
yield service, AssistantCapabilityCatalog(service), user_root
finally:
service.close()
reserve.assert_not_called()
def assistant_preview(service, catalog, message, conversation="conv-schedule-flow", client=None):
if client is None:
client = MagicMock()
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
fast = catalog.fast_answer(message, snapshot)
results, events, passed_plans = [], [], []
def execute(instruction, progress, assistant_plan=None):
passed_plans.append(deepcopy(assistant_plan))
result = service.preview_native_can(
instruction, provider="local", conversation_id=conversation,
execution_mode="hardware", vehicle_id="denza_n7",
on_commands_validated=progress, assistant_plan=assistant_plan,
)
results.append(result)
return result
response = assistant.stream(
message, conversation_id=conversation, principal="schedule-test", history=[],
authoritative_answer=fast.answer if fast else None,
emit=lambda event: events.append(event) or True,
execute_control=execute, capability_snapshot=snapshot,
capability_context=catalog.compact_context(snapshot),
scene_capability_context=catalog.scene_composition_context(snapshot),
control_terms=catalog.control_terms(snapshot),
)
assert len(results) == 1, (response, results)
return response, results[0], events, passed_plans, client
def create_saved(service, catalog, message="构建一个上车预热的功能模式"):
response, preview, _, _, _ = assistant_preview(service, catalog, message)
assert response["success"] is True, (response, preview)
draft = preview["mode_profile_draft"]
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded"
return preview, saved["mode_profile"]
@pytest.mark.parametrize(("message", "template_id", "count", "repeats", "elapsed"), [
("构建一个座椅的动态模式", "seat-dynamic", 4, 5, 300000),
("构建一个上车预热的功能模式", "boarding-warmup", 7, 1, 420000),
("帮我创建一个座椅动态模式吧", "seat-dynamic", 4, 5, 300000),
("我想构建一个上车预热模式并预览", "boarding-warmup", 7, 1, 420000),
("构建一个上车暖舱模式", "boarding-warmup", 7, 1, 420000),
("构建一个空调预热模式", "boarding-warmup", 7, 1, 420000),
("构建一个动态座椅模式", "seat-dynamic", 4, 5, 300000),
("构建一个座椅交替放松模式", "seat-dynamic", 4, 5, 300000),
])
def test_user_temporal_requests_create_complete_multistage_template_without_model(
schedule_service, message, template_id, count, repeats, elapsed,
):
service, catalog, user_root = schedule_service
response, preview, events, plans, client = assistant_preview(service, catalog, message)
assert response["success"] is True, (response, preview)
assert response["route"] == "scene_template"
assert preview["status"] == "succeeded"
assert preview["execution_mode"] == "dry_run"
assert preview["hardware_execution_allowed"] is False
assert preview["orchestration_route"] == "mode_profile_create_preview"
draft = preview["mode_profile_draft"]
assert draft["source_template_id"] == template_id
assert draft["generation_source"] == "scene_template_copy"
assert draft["operation_count"] == count
expected_schedule = deepcopy(plans[0]["schedule"])
for phase in expected_schedule["phases"]:
phase["operations"] = [
op for op in phase["operations"]
if op["domain"] != "seat" or op["target_scope"] == "passenger"
]
assert draft["schedule"] == preview["schedule"] == expected_schedule
schedule = ModeSchedule.from_dict(preview["schedule"])
assert schedule.repeat_count == repeats
assert schedule.total_duration_ms == elapsed
actions = preview["intent_ir"]["spec"]["actions"]
assert len(actions) == count
assert len({action["id"] for action in actions}) == count
assert {item["phase_id"] for item in preview["schedule_phase_previews"]} == {phase.id for phase in schedule.phases}
assert len(preview["schedule_phase_previews"]) == 2
assert all(item["status"] == "succeeded" for item in preview["schedule_phase_previews"])
assert all(any(key.startswith(phase.id + "-") for key in preview["command_plans"]) for phase in schedule.phases)
assert {
op["target_scope"] for phase in schedule.phases for op in phase.operations
if op["domain"] == "seat"
} == {"passenger"}
assert events
client.start_thread.assert_not_called()
client.start_turn.assert_not_called()
client.start_structured_turn.assert_not_called()
assert not list(user_root.rglob("*.yaml"))
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
@pytest.mark.parametrize("message", ["构建一个座椅的动态模式", "构建一个上车预热的功能模式"])
def test_draft_trial_preserves_all_stages_repeats_and_virtual_time_without_thread(schedule_service, message):
service, catalog, _ = schedule_service
_, preview, _, _, _ = assistant_preview(service, catalog, message)
draft = preview["mode_profile_draft"]
with patch("cognitive_agent.application.mode_schedule.Thread", side_effect=AssertionError("dry-run spawned worker")):
trial = service.trial_mode_profile_draft(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
)
assert trial["status"] == "succeeded"
assert trial["hardware_execution_allowed"] is False
assert trial["schedule"] == preview["schedule"]
execution = trial["schedule_execution"]
schedule = ModeSchedule.from_dict(preview["schedule"])
assert execution["completed"] is True
assert execution["virtual_elapsed_ms"] == schedule.total_duration_ms
assert execution["executed_phases"] == len(schedule.phases) * schedule.repeat_count
assert [step["phase_id"] for step in execution["phase_results"]] == [phase.id for phase in schedule.phases] * schedule.repeat_count
assert len(trial["intent_ir"]["spec"]["actions"]) == schedule.operation_count
@pytest.mark.parametrize("message", ["构建一个座椅的动态模式", "构建一个上车预热的功能模式"])
def test_save_reload_and_direct_execute_keep_temporal_semantics(schedule_service, message):
service, catalog, user_root = schedule_service
preview, public_profile = create_saved(service, catalog, message)
assert public_profile["schedule"] == preview["schedule"]
assert public_profile["behavior"] == "cyclic"
mode_id = public_profile["id"]
service.mode_profiles = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes", user_root=user_root)
reloaded = service.mode_profiles.get_profile(mode_id, "denza_n7")
assert reloaded.schedule.to_dict() == preview["schedule"]
with patch("cognitive_agent.application.mode_schedule.Thread", side_effect=AssertionError("dry-run spawned worker")):
executed = service.execute_mode_profile_card(
mode_id, conversation_id="conv-schedule-execute", execution_mode="dry_run", vehicle_id="denza_n7",
)
assert executed["status"] == "succeeded", executed
assert executed["orchestration_route"] == "mode_schedule_execute"
assert executed["execution_mode"] == "dry_run"
assert executed["hardware_execution_allowed"] is False
assert "mode_profile_draft" not in executed
execution = executed["schedule_execution"]
assert execution["completed"] is True
assert execution["virtual_elapsed_ms"] == reloaded.schedule.total_duration_ms
assert len(execution["phase_results"]) == len(reloaded.schedule.phases) * reloaded.schedule.repeat_count
expected_phases = list(reloaded.schedule.phases) * reloaded.schedule.repeat_count
for expected, actual in zip(expected_phases, execution["phase_results"], strict=True):
phase_result = actual["result"]
assert phase_result["status"] == "succeeded"
assert phase_result["execution_mode"] == "dry_run"
actual_actions = phase_result["intent_ir"]["spec"]["actions"]
assert len(actual_actions) == len(expected.operations)
expected_params = sorted(json.dumps(op["parameters"], sort_keys=True) for op in expected.operations)
actual_params = sorted(json.dumps(op["parameters"], sort_keys=True) for op in actual_actions)
assert actual_params == expected_params
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
def test_natural_language_enter_saved_schedule_runs_timeline_not_flat_actions(schedule_service):
service, catalog, _ = schedule_service
preview, saved = create_saved(service, catalog)
result = service.process(
f"进入{saved['name']}", provider="local", conversation_id="conv-schedule-natural-enter",
execution_mode="dry_run", vehicle_id="denza_n7",
)
assert result["status"] == "succeeded", result
assert result["orchestration_route"] == "mode_schedule_execute"
assert result["schedule"] == preview["schedule"]
temperatures = []
for phase in result["schedule_execution"]["phase_results"]:
temperatures.extend(action["parameters"]["temperature_c"] for action in phase["result"]["intent_ir"]["spec"]["actions"] if action["capability"] == "climate.set_temperature")
assert temperatures == [28, 24]
def test_missing_hardware_lease_rejects_before_any_worker_or_physical_access(schedule_service):
service, catalog, _ = schedule_service
_, saved = create_saved(service, catalog)
with (
patch("cognitive_agent.application.mode_schedule.Thread", side_effect=AssertionError("unauthorized worker started")),
pytest.raises(HardwareAccessRequiredError),
):
service.execute_mode_profile_card(
saved["id"], execution_mode="hardware", vehicle_id="denza_n7",
conversation_id="conv-schedule-unauthorized", hardware_access_bypass=False,
hardware_client_id="hwc-schedule-unauthorized",
)
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
@pytest.mark.parametrize(("field", "value"), [
("name", "其他模式"), ("instruction", "直接执行危险指令"),
("vehicle_id", "other_vehicle"), ("source_request_id", "../../result"),
])
def test_client_cannot_rebind_schedule_draft_trial(schedule_service, field, value):
service, catalog, user_root = schedule_service
_, preview, _, _, _ = assistant_preview(service, catalog, "构建一个上车预热的功能模式")
draft = preview["mode_profile_draft"]
arguments = {key: draft[key] for key in ("name", "instruction", "source_request_id", "vehicle_id")}
arguments[field] = value
with pytest.raises(ValueError):
service.trial_mode_profile_draft(**arguments)
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize("mutation", ["capability", "duration", "temperature", "target"])
def test_tampered_template_schedule_never_creates_or_executes_draft(schedule_service, mutation):
service, catalog, user_root = schedule_service
template = next(item for item in catalog.snapshot(vehicle_id="denza_n7")["scene_templates"] if item["id"] == "boarding-warmup")
plan = deepcopy(template["plan"])
phase = plan["schedule"]["phases"][0]
if mutation == "capability":
phase["operations"][0]["capability"] = "door.set_vehicle_lock"
elif mutation == "duration":
phase["duration_ms"] = 1
elif mutation == "temperature":
phase["operations"][1]["parameters"]["temperature_c"] = 99
else:
phase["operations"][3]["target_scope"] = "passenger"
try:
result = service.preview_native_can(
"构建一个上车预热的功能模式", provider="local", conversation_id="conv-schedule-tamper",
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
)
except ValueError:
result = None
if result is not None:
assert result["status"] != "succeeded"
assert "mode_profile_draft" not in result
assert result.get("hardware_execution_allowed") is not True
assert not list(user_root.rglob("*.yaml"))
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
def test_invalid_later_saved_phase_is_revalidated_before_any_phase_executes(schedule_service):
service, catalog, _ = schedule_service
_, saved = create_saved(service, catalog)
profile = service.mode_profiles.get_profile(saved["id"], "denza_n7")
# Represents a stale or externally edited user profile, not a trusted runtime
# capability change. Its structure remains valid, but the target is unsafe.
profile.schedule.phases[1].operations[0]["parameters"]["temperature_c"] = 99
with patch.object(service.mode_schedule_runner, "simulate", side_effect=AssertionError("invalid later phase reached executor")):
result = service.execute_mode_profile_card(
saved["id"], conversation_id="conv-schedule-stale-capability",
execution_mode="dry_run", vehicle_id="denza_n7",
)
assert result["status"] == "validation_failed", result
assert result["mode_creation_failure"]["code"] == "MODE_SCHEDULE_PHASE_INVALID"
assert "mode_profile_draft" not in result
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
def test_execution_failure_after_validated_first_phase_prevents_later_repeats(schedule_service):
service, catalog, _ = schedule_service
_, saved = create_saved(service, catalog, "构建一个主驾和副驾的座椅动态模式")
preview_method = service._preview_mode_schedule
virtual_method = service.native_can_runtime.execute_virtual
execution_started = False
executed_massage_modes = []
def validate_then_arm(*args, **kwargs):
nonlocal execution_started
result = preview_method(*args, **kwargs)
execution_started = True
return result
def fault_on_second_phase(command_plan, function_config):
if execution_started:
for op in function_config["spec"]["operations"]:
if op["capability"] == "seat.set_driver_massage_native":
executed_massage_modes.append(op["parameters"]["mode"])
if op["parameters"]["mode"] == "slack":
raise RuntimeError("injected lower-level actuator failure")
return virtual_method(command_plan, function_config)
with (
patch.object(service, "_preview_mode_schedule", side_effect=validate_then_arm),
patch.object(service.native_can_runtime, "execute_virtual", side_effect=fault_on_second_phase),
):
result = service.execute_mode_profile_card(
saved["id"], conversation_id="conv-schedule-injected-failure",
execution_mode="dry_run", vehicle_id="denza_n7",
)
execution = result["schedule_execution"]
assert result["status"] == "failed", result
assert execution["completed"] is False
assert execution["error"]
assert execution["executed_phases"] == 1
assert execution["virtual_elapsed_ms"] == 30000
assert execution["repeat_index"] == 0
assert len(execution["phase_results"]) == 2
assert executed_massage_modes == ["relax", "slack"]
assert result["hardware_execution_allowed"] is False
def structured_schedule():
return {
"requests": [], "ambiguities": [], "confidence": 0.96,
"schedule": {
"repeat_count": 2,
"phases": [
{"id": "warm", "name": "暖舱", "duration_ms": 30000,
"requests": [{"capability": "climate.set_temperature", "target_scope": "vehicle", "parameters": {"temperature_c": 28}}]},
{"id": "settle", "name": "恒温", "duration_ms": 60000,
"requests": [{"capability": "climate.set_temperature", "target_scope": "vehicle", "parameters": {"temperature_c": 24}}]},
],
},
}
def structured_client(output):
client = MagicMock()
client.available = True
client.status = "ready"
client.start_thread.return_value = "thread-schedule-test"
client.start_turn.side_effect = AssertionError("dynamic creation fell back to unstructured agent")
def start(thread_id, text, *, application_context, output_schema, timeout=None):
Draft202012Validator(output_schema).validate(output)
turn = ActiveCodexTurn(thread_id=thread_id, turn_id="turn-schedule-test", events=queue.Queue())
turn.events.put({"method": "item/completed", "params": {"item": {"id": "answer", "type": "agentMessage", "text": json.dumps(output, ensure_ascii=False)}}})
turn.events.put({"method": "turn/completed", "params": {"turn": {"id": turn.turn_id, "status": "completed", "items": [], "error": None}}})
return turn
client.start_structured_turn.side_effect = start
return client
@pytest.mark.parametrize("message", [
"构建一个星云动态模式,空调先28度持续30秒,然后24度持续60秒,循环2次",
"构建一个阅读模式,空调先28度持续30秒,然后24度持续60秒,循环2次",
"构建一个上车预热的功能模式,空调先28度持续30秒,然后24度持续60秒,循环2次",
])
def test_explicit_temporal_parameters_use_structured_composition_not_static_template(schedule_service, message):
service, catalog, user_root = schedule_service
output = structured_schedule()
client = structured_client(output)
response, preview, _, plans, _ = assistant_preview(service, catalog, message, client=client)
assert response["route"] == "scene_composer", response
assert response["success"] is True, (response, preview)
assert preview["mode_profile_draft"]["generation_source"] != "scene_template_copy"
client.start_structured_turn.assert_called_once()
client.start_turn.assert_not_called()
assert "schedule" in client.start_structured_turn.call_args.kwargs["output_schema"]["properties"]
assert preview["schedule"] == plans[0]["schedule"]
assert preview["schedule"]["repeat_count"] == 2
assert [phase["duration_ms"] for phase in preview["schedule"]["phases"]] == [30000, 60000]
assert [phase["operations"][0]["parameters"]["temperature_c"] for phase in preview["schedule"]["phases"]] == [28, 24]
assert ModeSchedule.from_dict(preview["schedule"]).total_duration_ms == 180000
assert not list(user_root.rglob("*.yaml"))
def test_dynamic_schema_normalization_preserves_complete_phase_contract(schedule_service):
_, catalog, _ = schedule_service
context = catalog.scene_composition_context(catalog.snapshot(vehicle_id="denza_n7"))
output = structured_schedule()
schema = build_scene_output_schema(context, dynamic=True)
Draft202012Validator(schema).validate(output)
plan = normalize_scene_output(output, context, dynamic=True)
assert plan["ambiguities"] == []
schedule = ModeSchedule.from_dict(plan["schedule"])
assert schedule.total_duration_ms == 180000
assert schedule.operation_count == 2
assert schedule.phases[0].operations[0]["domain"] == "climate"
assert schedule.phases[1].operations[0]["parameters"] == {"temperature_c": 24}
@pytest.mark.parametrize("mutation", [
"static-root", "unknown-root", "unknown-schedule", "unknown-phase", "unknown-capability",
"duplicate-phase", "duplicate-operation", "no-phases", "empty-phase", "bool-duration",
"bool-repeat", "long-duration", "long-total", "nan-parameter", "motion-capability",
])
def test_invalid_structured_dynamic_plans_fail_closed_during_normalization(schedule_service, mutation):
_, catalog, _ = schedule_service
context = catalog.scene_composition_context(catalog.snapshot(vehicle_id="denza_n7"))
output = structured_schedule()
schedule = output["schedule"]
phase = schedule["phases"][0]
if mutation == "static-root":
output["requests"] = deepcopy(phase["requests"])
elif mutation == "unknown-root":
output["execute_now"] = True
elif mutation == "unknown-schedule":
schedule["unbounded"] = True
elif mutation == "unknown-phase":
phase["skip_safety"] = True
elif mutation == "unknown-capability":
phase["requests"][0]["capability"] = "unknown.actuate"
elif mutation == "motion-capability":
phase["requests"][0] = {"capability": "seat.set_driver_axis_native", "target_scope": "driver", "parameters": {"duration_ms": 1000, "axis": "backrest", "direction": "backward"}}
elif mutation == "duplicate-phase":
schedule["phases"][1]["id"] = phase["id"]
elif mutation == "duplicate-operation":
phase["requests"].append(deepcopy(phase["requests"][0]))
elif mutation == "no-phases":
schedule["phases"] = []
elif mutation == "empty-phase":
phase["requests"] = []
elif mutation == "bool-duration":
phase["duration_ms"] = True
elif mutation == "bool-repeat":
schedule["repeat_count"] = True
elif mutation == "long-duration":
phase["duration_ms"] = 900001
elif mutation == "long-total":
schedule["repeat_count"] = 20
phase["duration_ms"] = 900000
elif mutation == "nan-parameter":
phase["requests"][0]["parameters"]["temperature_c"] = float("nan")
with pytest.raises(SceneCompositionError):
normalize_scene_output(output, context, dynamic=True)
def test_dynamic_ambiguity_produces_no_schedule_or_executable_actions(schedule_service):
_, catalog, _ = schedule_service
context = catalog.scene_composition_context(catalog.snapshot(vehicle_id="denza_n7"))
output = {"requests": [], "ambiguities": ["当前未开放该动态动作,请指定其他设备。"], "confidence": 0.2, "schedule": None}
Draft202012Validator(build_scene_output_schema(context, dynamic=True)).validate(output)
plan = normalize_scene_output(output, context, dynamic=True)
assert not plan["requests"]
assert "schedule" not in plan
assert plan["ambiguities"]
def test_model_schedule_with_unresolved_requirement_never_generates_successful_draft(schedule_service):
service, catalog, user_root = schedule_service
context = catalog.scene_composition_context(catalog.snapshot(vehicle_id="denza_n7"))
output = structured_schedule()
output["ambiguities"] = ["不能完成要求的主驾座椅绝对位置控制。"]
plan = normalize_scene_output(output, context, dynamic=True)
assert "metadata" not in plan
with pytest.raises(ValueError, match="未解决"):
service.preview_native_can(
"构建一个流光动态模式", provider="local", conversation_id="conv-schedule-ambiguous",
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
)
assert not list(user_root.rglob("*.yaml"))
assert service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
@pytest.mark.parametrize(("seat", "allowed_targets"), [
("主驾", {"driver", "front_left"}), ("副驾", {"passenger", "front_right"}),
])
def test_single_seat_dynamic_execution_cannot_expand_to_both_seats(schedule_service, seat, allowed_targets):
service, catalog, _ = schedule_service
_, saved = create_saved(service, catalog, "构建一个主驾和副驾的座椅动态模式")
result = service.process(
f"只让{seat}进入{saved['name']}", provider="local", conversation_id="conv-schedule-single-seat",
execution_mode="dry_run", vehicle_id="denza_n7",
)
if result["status"] == "succeeded":
assert result["orchestration_route"] == "mode_schedule_execute"
execution = result["schedule_execution"]
for phase in execution["phase_results"]:
actions = phase["result"]["intent_ir"]["spec"]["actions"]
seat_targets = {action["target"].get("instance") or action["target"].get("zone") for action in actions if action["domain"] == "seat"}
assert seat_targets and seat_targets <= allowed_targets
else:
assert result["status"] in {"validation_failed", "needs_clarification", "needs_confirmation", "failed"}
assert not result.get("schedule_execution", {}).get("phase_results")
assert result.get("hardware_execution_allowed") is not True
def start_waiting_schedule(service):
"""Run only a no-device test callback, holding until the tested stop request."""
calls = []
entered = Event()
schedule = ModeSchedule.from_dict({
"schema": "cognitive-agent-mode-schedule/v1", "repeat_count": 1,
"phases": [
{"id": phase_id, "name": phase_id, "duration_ms": 900000,
"operations": [{"domain": "climate", "capability": "climate.set_temperature",
"target_scope": "vehicle", "operation": "set",
"parameters": {"temperature_c": temperature}, "reason": "test only"}]}
for phase_id, temperature in (("first", 28), ("second", 24))
],
})
def execute(phase):
calls.append(phase.id)
entered.set()
return {"status": "succeeded"}
service.mode_schedule_runner.start("denza_n7", schedule, execute, session_id="conv-stop-status")
assert entered.wait(2)
assert service.mode_schedule_runner.status("denza_n7")["status"] == "running"
return calls
def assistant_control(catalog, message, execute):
client = MagicMock()
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
events = []
response = assistant.stream(
message, conversation_id="conv-stop-status", principal="schedule-status-test", history=[],
emit=lambda event: events.append(event) or True,
execute_control=execute, capability_snapshot=snapshot,
capability_context=catalog.compact_context(snapshot),
scene_capability_context=catalog.scene_composition_context(snapshot),
control_terms=catalog.control_terms(snapshot),
)
client.start_thread.assert_not_called()
client.start_structured_turn.assert_not_called()
client.start_turn.assert_not_called()
return response, events
def test_dry_run_stop_does_not_cancel_a_running_vehicle_schedule(schedule_service):
service, _, _ = schedule_service
calls = start_waiting_schedule(service)
result = service.process(
"停止动态模式", provider="local", conversation_id="conv-stop-status",
execution_mode="dry_run", vehicle_id="denza_n7",
)
assert result["status"] == "succeeded"
assert result["schedule_execution"]["status"] == "simulated"
assert result["hardware_execution_allowed"] is False
assert service.mode_schedule_runner.status("denza_n7")["status"] == "running"
assert calls == ["first"]
persisted = json.loads((service.artifact_root / result["request_id"] / "result.json").read_text(encoding="utf-8"))
assert persisted["orchestration_route"] == "mode_schedule_stop"
assert persisted["trace_events"] == []
def test_hardware_stop_cancels_future_stages_and_persists_the_result(schedule_service):
service, _, _ = schedule_service
calls = start_waiting_schedule(service)
result = service.process(
"停止动态模式", provider="local", conversation_id="conv-stop-status",
execution_mode="hardware", vehicle_id="denza_n7",
)
assert result["status"] == "succeeded"
assert result["schedule_execution"]["status"] == "cancelled"
assert result["schedule_execution"]["completed"] is False
assert service.mode_schedule_runner.status("denza_n7")["status"] == "cancelled"
assert service.mode_schedule_runner.close(timeout=1)
assert calls == ["first"]
persisted = json.loads((service.artifact_root / result["request_id"] / "result.json").read_text(encoding="utf-8"))
assert persisted["orchestration_route"] == "mode_schedule_stop"
assert persisted["schedule_execution"]["status"] == "cancelled"
assert persisted["trace_events"] == []
@pytest.mark.parametrize(("execution_mode", "action_state", "runner_state"), [
("hardware", "cancelled", "cancelled"), ("dry_run", "simulated", "running"),
])
def test_assistant_stop_is_zero_model_success_and_preserves_execution_policy(
schedule_service, execution_mode, action_state, runner_state,
):
service, catalog, _ = schedule_service
calls = start_waiting_schedule(service)
def execute(message, progress):
return service.process(
message, provider="local", conversation_id="conv-stop-status",
execution_mode=execution_mode, vehicle_id="denza_n7",
)
response, events = assistant_control(catalog, "停止动态模式", execute)
assert response["success"] is True, response
assert response["route"] == "vehicle_control"
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["state"] == action_state
assert action["payload"]["orchestration_route"] == "mode_schedule_stop"
assert service.mode_schedule_runner.status("denza_n7")["status"] == runner_state
assert calls == ["first"]
assert "已完成" not in response["answer"]
assert "未改变正在运行" in response["answer"] if execution_mode == "dry_run" else "已停止" in response["answer"]
def test_assistant_running_schedule_is_accepted_without_claiming_all_phases_completed(schedule_service):
service, catalog, _ = schedule_service
preview, profile = create_saved(service, catalog)
start_waiting_schedule(service)
state = service.mode_schedule_runner.status("denza_n7")
state["phase_results"] = [{"command_frames": ["sensitive-can-frame"]}]
def execute(message, progress):
return {
"status": "running", "execution_mode": "hardware", "hardware_execution_allowed": True,
"orchestration_route": "mode_schedule_execute", "schedule_execution": state,
"mode_profile": profile, "matched_modes": [profile], "schedule": preview["schedule"],
}
response, events = assistant_control(catalog, f"进入{profile['name']}", execute)
assert response["success"] is True, response
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["state"] == "running"
assert action["payload"]["status"] == "running"
assert action["payload"]["schedule_execution"]["completed"] is False
assert "phase_results" not in action["payload"]["schedule_execution"]
assert "sensitive-can-frame" not in json.dumps(events, ensure_ascii=False)
assert "尚未完成所有阶段" in action["display_text"]
assert "尚未完成所有阶段" in response["answer"]
completed = next(event for event in events if event["type"] == "work.completed")
assert completed["state"] == "running"
assert "已完成" not in completed["display_text"]
def test_public_schedule_status_exposes_only_lifecycle_and_redacts_raw_error():
state = {
"run_id": "run-public", "vehicle_id": "denza_n7", "status": "failed",
"phase_index": 1, "repeat_index": 0, "completed": False,
"error": "secret-token /private/path/driver.dll CAN payload 123456",
"phase_results": [{"command_plan": {"data_hex": "deadbeef"}}],
"raw_error": {"token": "secret-token"}, "command_plans": {"internal": "data"},
"phase_count": {"nested": "unexpected"},
}
public = public_schedule_status(state)
assert public["status"] == "failed"
assert public["phase_index"] == 1
assert public["completed"] is False
assert public["error"] and public["error"] != state["error"]
assert set(public) == {"run_id", "vehicle_id", "status", "phase_index", "repeat_index", "completed", "error"}
for sensitive in ("secret-token", "driver.dll", "deadbeef", "123456"):
assert sensitive not in json.dumps(public, ensure_ascii=False)
@pytest.mark.parametrize("message", ["外循环通风模式", "内循环模式"])
def test_hvac_circulation_state_is_not_a_temporal_schedule(message):
assert needs_mode_schedule(message) is False
@@ -0,0 +1,278 @@
"""Natural-language edits cannot flatten or silently broaden dynamic phases."""
from __future__ import annotations
import hashlib
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch
import pytest
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.mode_schedule_edit import edit_schedule
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@pytest.fixture
def service(tmp_path):
with patch.dict(
"os.environ",
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
},
):
instance = SeatDemoService(REPOSITORY_ROOT)
instance.artifact_root = tmp_path / "runs"
instance.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes",
user_root=tmp_path / "user-modes",
)
try:
with patch.object(
instance.hardware_execution,
"reserve",
side_effect=AssertionError("schedule edit reached hardware"),
):
yield instance
finally:
instance.close()
def _build(service):
template = service.scene_templates.get_template("boarding-warmup")
result = service.preview_native_can(
"构建一个上车预热模式",
provider="local",
conversation_id="conv-schedule-edit",
execution_mode="hardware",
vehicle_id="denza_n7",
assistant_plan=template.plan(),
)
assert result["status"] == "succeeded", result.get("validation")
return result
def _edit(service, built, change):
draft = built["mode_profile_draft"]
reference = {key: draft[key] for key in ("name", "source_request_id", "vehicle_id")}
return service.preview_native_can(
f"把上车预热模式的{change}",
provider="local",
conversation_id="conv-schedule-edit",
execution_mode="hardware",
vehicle_id="denza_n7",
mode_profile_draft_context=reference,
)
def _parameters(schedule, index, capability, target="vehicle"):
return next(
op["parameters"]
for op in schedule["phases"][index]["operations"]
if op["capability"] == capability and op["target_scope"] == target
)
@pytest.mark.parametrize(
"change",
(
"第二阶段空调温度调整到26度",
"第2个阶段的空调温度变成26度",
"舒适恒温阶段空调温度改为26度",
),
)
def test_draft_temperature_edit_preserves_first_phase_and_timing(service, change):
built = _build(service)
original = deepcopy(built["schedule"])
revised = _edit(service, built, change)
assert revised["status"] == "succeeded", revised.get("mode_creation_failure")
expected = deepcopy(original)
_parameters(expected, 1, "climate.set_temperature")["temperature_c"] = 26
assert revised["schedule"] == expected
assert built["schedule"] == original
assert revised["mode_profile_draft"]["schedule"] == expected
assert revised["mode_profile_draft"]["draft_revision"] == 2
assert revised["mode_profile_draft"]["revises_source_request_id"] == built["request_id"]
assert revised["execution_mode"] == "dry_run"
assert revised["hardware_execution_allowed"] is False
@pytest.mark.parametrize(
("change", "expected_hold", "expected_repeat"),
(
("第二阶段持续时间调整为3分钟", 180000, 1),
("第二阶段保持30秒", 30000, 1),
("第二阶段时长改成2.5分钟", 150000, 1),
("循环次数改成3次", 300000, 3),
("重复两次", 300000, 2),
),
)
def test_draft_time_and_repeat_edits_keep_operations(
service, change, expected_hold, expected_repeat
):
built = _build(service)
revised = _edit(service, built, change)
assert revised["status"] == "succeeded", revised.get("mode_creation_failure")
assert revised["schedule"]["phases"][1]["duration_ms"] == expected_hold
assert revised["schedule"]["repeat_count"] == expected_repeat
for index in (0, 1):
assert (
revised["schedule"]["phases"][index]["operations"]
== built["schedule"]["phases"][index]["operations"]
)
def test_all_phases_temperature_edit_is_explicit_and_preserves_all_other_operations(service):
built = _build(service)
revised = _edit(service, built, "全部阶段空调温度调整到25度")
assert revised["status"] == "succeeded", revised.get("mode_creation_failure")
expected = deepcopy(built["schedule"])
for index in (0, 1):
_parameters(expected, index, "climate.set_temperature")["temperature_c"] = 25
assert revised["schedule"] == expected
@pytest.mark.parametrize(
"change",
(
"空调温度调整到25度",
"第三阶段空调温度调整到25度",
"第二阶段持续16分钟",
"循环21次",
"重复1.5次",
"持续30秒",
"第二阶段主驾靠背调整到90%",
"第二阶段空调温度调整到99度",
"第二阶段打开后备箱",
"第二阶段空调温度调整到26度并打开后备箱",
),
)
def test_ambiguous_unsafe_or_unbounded_edit_leaves_original_draft_unchanged(service, change):
built = _build(service)
original = deepcopy(built["schedule"])
revised = _edit(service, built, change)
assert revised["status"] in {"needs_confirmation", "validation_failed"}
assert not revised.get("mode_profile_draft")
assert revised["hardware_execution_allowed"] is False
source = service._load_mode_profile_draft_reference(
{
key: built["mode_profile_draft"][key]
for key in ("name", "source_request_id", "vehicle_id")
},
conversation_id="conv-schedule-edit",
mode_name="上车预热模式",
vehicle_id="denza_n7",
)[1]
assert source["schedule"] == original
def test_saved_schedule_edit_revalidates_and_persists_timeline_not_flat_actions(service):
built = _build(service)
draft = built["mode_profile_draft"]
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"]
)
mode_id = saved["mode_profile"]["id"]
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
path = Path(profile.source)
before = hashlib.sha256(path.read_bytes()).hexdigest()
rejected = service.preview_native_can(
"把上车预热模式的空调温度调整到26度",
provider="local",
conversation_id="conv-saved-schedule-edit",
execution_mode="hardware",
vehicle_id="denza_n7",
)
assert rejected["status"] == "needs_confirmation"
assert hashlib.sha256(path.read_bytes()).hexdigest() == before
revised = service.preview_native_can(
"把上车预热模式的第二阶段空调温度调整到26度",
provider="local",
conversation_id="conv-saved-schedule-edit",
execution_mode="hardware",
vehicle_id="denza_n7",
)
assert revised["status"] == "succeeded", revised.get("validation")
assert revised["orchestration_route"] == "mode_profile_update"
assert revised["hardware_execution_allowed"] is False
actual = service.mode_profiles.get_profile(mode_id, "denza_n7").schedule.to_dict()
expected = deepcopy(built["schedule"])
_parameters(expected, 1, "climate.set_temperature")["temperature_c"] = 26
assert actual == expected
assert revised["schedule"] == expected
assert revised["mode_profile"]["behavior"] == "cyclic"
def test_combined_phase_action_hold_then_repeat_revision_preserves_timeline(service):
built = _build(service)
revised = _edit(service, built, "第二阶段空调温度调整到26度,持续3分钟")
assert revised["status"] == "succeeded", revised.get("mode_creation_failure")
repeated = _edit(service, revised, "重复两次")
assert repeated["status"] == "succeeded", repeated.get("mode_creation_failure")
expected = deepcopy(built["schedule"])
_parameters(expected, 1, "climate.set_temperature")["temperature_c"] = 26
expected["phases"][1]["duration_ms"] = 180000
expected["repeat_count"] = 2
assert repeated["schedule"] == expected
assert repeated["mode_profile_draft"]["draft_revision"] == 3
draft = repeated["mode_profile_draft"]
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"]
)
assert (
service.mode_profiles.get_profile(
saved["mode_profile"]["id"], "denza_n7"
).schedule.to_dict()
== expected
)
def test_all_phases_is_not_mistaken_for_all_vehicle_zones(service):
source = service.scene_templates.get_template("boarding-warmup").plan()["schedule"]
for phase in source["phases"]:
for operation in phase["operations"]:
if operation["capability"] == "climate.set_temperature":
operation["target_scope"] = "passenger"
schedule = ModeSchedule.from_dict(source)
updated = edit_schedule(
schedule,
"全部阶段空调温度调整到25度",
parse_operations=lambda _: [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"operation": "set",
"parameters": {"temperature_c": 25},
}
],
target_is_explicit=service._mode_update_target_is_explicit,
)
for index in (0, 1):
assert _parameters(updated.to_dict(), index, "climate.set_temperature", "passenger") == {
"temperature_c": 25
}
assert not any(
op["capability"] == "climate.set_temperature" and op["target_scope"] == "vehicle"
for op in updated.phases[index].operations
)
def test_phase_motor_duration_cannot_become_a_hold_duration(service):
built = _build(service)
result = _edit(service, built, "第二阶段副驾靠背向后移动3s")
assert result["status"] in {"needs_confirmation", "validation_failed"}
assert not result.get("mode_profile_draft")
def test_stage_selector_does_not_authorize_delayed_action_semantics(service):
built = _build(service)
result = _edit(service, built, "第二阶段30秒后空调温度调整到26度")
assert result["status"] == "needs_confirmation"
assert not result.get("mode_profile_draft")
@@ -273,7 +273,8 @@ def test_name_only_nap_request_reuses_the_verified_repository_profile(
draft = result["mode_profile_draft"]
assert isinstance(draft, dict)
assert draft["name"] == "小憩模式"
assert draft["operation_count"] == len(preset.operations) == 9
assert draft["operation_count"] == 9
assert len(preset.operations) == 10 # The registry retains both optional seat variants.
assert set(draft["domains"]) == {"seat", "climate", "roof", "ambient_light"}
assert draft["existing_mode_id"] == "nap"
assert isinstance(draft["existing_mode_revision"], str)
@@ -283,6 +284,7 @@ def test_name_only_nap_request_reuses_the_verified_repository_profile(
actions = _intent_actions(result)
assert {action["capability"] for action in actions} == {
operation.capability for operation in preset.operations
if operation.domain != "seat" or operation.target_scope in {"passenger", "front_right"}
} == EXPECTED_NAP_CAPABILITIES
expected_targets_and_parameters = {
operation.capability: (
@@ -292,6 +294,7 @@ def test_name_only_nap_request_reuses_the_verified_repository_profile(
operation.parameters,
)
for operation in preset.operations
if operation.domain != "seat" or operation.target_scope in {"passenger", "front_right"}
}
actual_targets_and_parameters = {
action["capability"]: (
@@ -0,0 +1,347 @@
"""Seat-aware built-in defaults must remain safe and preserve saved user intent."""
from __future__ import annotations
import hashlib
from collections.abc import Iterator
from pathlib import Path
from unittest.mock import patch
import pytest
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
COMFORT_TEMPLATE_IDS = (
"reading",
"cinema",
"camping",
"quiet-night",
"reception",
"romantic",
"winter-warmth",
"summer-cooling",
"long-trip-comfort",
"relaxation",
"seat-comfort",
)
DRIVER_COMFORT_CAPABILITIES = {
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
}
@pytest.fixture
def service(tmp_path: Path) -> Iterator[SeatDemoService]:
with patch.dict(
"os.environ",
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
},
):
instance = SeatDemoService(REPOSITORY_ROOT)
instance.artifact_root = tmp_path / "runs"
instance.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes",
user_root=tmp_path / "user-modes",
)
try:
with patch.object(
instance.hardware_execution,
"reserve",
side_effect=AssertionError("creating or previewing a mode reached real hardware"),
):
yield instance
finally:
instance.close()
def _preview(service: SeatDemoService, instruction: str, *, plan=None) -> dict:
result = service.preview_native_can(
instruction,
provider="local",
conversation_id="conv-registered-seat-coverage",
execution_mode="hardware",
vehicle_id="denza_n7",
assistant_plan=plan,
)
assert result["status"] == "succeeded", result.get("validation")
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
return result
def _action_map(result: dict) -> dict:
return {
(
action["capability"],
action["target"].get("instance") or action["target"].get("zone"),
): action["parameters"]
for action in result["intent_ir"]["spec"]["actions"]
}
@pytest.mark.parametrize("template_id", COMFORT_TEMPLATE_IDS)
@pytest.mark.parametrize(("seat_prefix", "targets"), (
("", {"passenger"}), ("主驾", {"driver"}),
("副驾", {"passenger"}), ("主驾和副驾", {"driver", "passenger"}),
))
def test_comfort_templates_select_only_requested_seat_without_mutating_seat_seeds(
service: SeatDemoService,
template_id: str,
seat_prefix: str,
targets: set[str],
) -> None:
template = service.scene_templates.get_template(template_id)
original_revision = template.revision
result = _preview(service, f"构建一个{seat_prefix}{template.name}", plan=template.plan())
actions = _action_map(result)
seat_keys = {key for key in actions if key[0].startswith("seat.")}
assert {target for _, target in seat_keys} == targets
assert {
operation.target_scope for operation in template.operations if operation.domain == "seat"
} == {"driver", "passenger"}
assert all(
operation.capability in DRIVER_COMFORT_CAPABILITIES
for operation in template.operations
if operation.domain == "seat" and operation.target_scope == "driver"
), "driver positional or timed movement must not enter static comfort defaults"
assert not any(
"massage" in capability for capability, target in seat_keys if target == "passenger"
)
assert actions == {
(operation.capability, operation.target_scope): operation.parameters
for operation in template.operations
if operation.domain != "seat" or operation.target_scope in targets
}
assert result["mode_profile_draft"]["generation_source"] == "scene_template_copy"
assert service.scene_templates.get_template(template_id).revision == original_revision
@pytest.mark.parametrize(
("mode_id", "mode_name"),
(
("office", "办公模式"),
("nap", "小憩模式"),
("meditation_space", "冥想空间模式"),
("leisure_lounger", "休闲躺椅模式"),
),
)
@pytest.mark.parametrize(("seat_prefix", "targets"), (
("", {"passenger"}), ("主驾", {"driver"}),
("副驾", {"passenger"}), ("主驾和副驾", {"driver", "passenger"}),
))
def test_registered_cabin_modes_select_only_requested_seat_on_creation(
service: SeatDemoService,
mode_id: str,
mode_name: str,
seat_prefix: str,
targets: set[str],
) -> None:
result = _preview(service, f"构建一个{seat_prefix}{mode_name}")
actions = _action_map(result)
assert {target for capability, target in actions if capability.startswith("seat.")} == targets
if "passenger" in targets:
assert ("seat.set_backrest_recline", "passenger") in actions
draft = result["mode_profile_draft"]
if seat_prefix:
assert "existing_mode_id" not in draft, "a selected-seat copy must not overwrite the original preset"
else:
assert draft["existing_mode_id"] == mode_id
assert draft["generation_source"] == "registered_mode_profile"
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
assert actions == {
(operation.capability, operation.target_scope): operation.parameters
for operation in profile.operations
if operation.domain != "seat" or operation.target_scope in targets
}
@pytest.mark.parametrize("template_id", ("fresh-air", "rain-defrost"))
def test_air_quality_and_visibility_templates_do_not_invent_seat_actions(
service: SeatDemoService,
template_id: str,
) -> None:
template = service.scene_templates.get_template(template_id)
result = _preview(service, f"构建一个{template.name}", plan=template.plan())
assert not any(capability.startswith("seat.") for capability, _ in _action_map(result))
@pytest.mark.parametrize(
"template_id",
(
"long-trip-comfort",
"summer-cooling",
"winter-warmth",
"reception",
"quiet-night",
"romantic",
),
)
def test_non_posture_templates_keep_both_seat_positions_unchanged(
service: SeatDemoService,
template_id: str,
) -> None:
template = service.scene_templates.get_template(template_id)
assert all(
"position_pct" not in operation.parameters and "duration_ms" not in operation.parameters
for operation in template.operations
if operation.domain == "seat"
)
def test_native_driver_and_passenger_thermal_levels_are_not_confused(
service: SeatDemoService,
) -> None:
for template_id in (
"reading",
"camping",
"quiet-night",
"reception",
"romantic",
"long-trip-comfort",
"seat-comfort",
):
template = service.scene_templates.get_template(template_id)
params = {(op.capability, op.target_scope): op.parameters for op in template.operations}
assert params[("seat.set_driver_ventilation_native", "driver")] == {
"state": "on",
"level": 2,
}
assert params[("seat.set_ventilation", "passenger")] == {"state": "on", "level": 1}
def test_upgraded_template_does_not_silently_add_seats_to_a_saved_custom_mode(
service: SeatDemoService,
tmp_path: Path,
) -> None:
# Simulate a previously saved user preference with the same name as a template.
original = _preview(service, "构建一个观影模式,空调温度调整到25度")
draft = original["mode_profile_draft"]
assert len(_action_map(original)) == 1
saved = service.create_mode_profile_card(
draft["name"],
draft["instruction"],
draft["source_request_id"],
draft["vehicle_id"],
)
assert saved["status"] == "succeeded"
paths = list((tmp_path / "user-modes").rglob("*.yaml"))
hashes = {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths}
repeated = _preview(service, "构建一个观影模式")
assert repeated["mode_profile_draft"]["generation_source"] == "registered_mode_profile"
assert _action_map(repeated) == _action_map(original)
assert {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths} == hashes
assert len(service.scene_templates.get_template("cinema").operations) == 11
def test_explicit_seat_only_requirement_is_not_expanded_to_other_seat_defaults(
service: SeatDemoService,
) -> None:
result = _preview(service, "构建一个阅读模式,副驾座椅通风关闭")
assert _action_map(result) == {("seat.set_ventilation", "passenger"): {"state": "off"}}
assert result["mode_profile_draft"]["generation_source"] == "explicit_scene_requirements"
@pytest.mark.parametrize("mode_id", ("office", "nap", "meditation_space", "leisure_lounger"))
@pytest.mark.parametrize(("seat", "spoken_seat"), (("driver", "主驾"), ("passenger", "副驾驶")))
def test_explicit_single_seat_invocation_selects_without_mutating_cabin_mode(
service: SeatDemoService,
mode_id: str,
seat: str,
spoken_seat: str,
) -> None:
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
original_revision = service.mode_profiles.profile_revision(profile)
expected = {
(operation.capability, operation.target_scope): operation.parameters
for operation in profile.operations
if operation.domain != "seat" or operation.target_scope == seat
}
result = service.preview_native_can(
f"请让{spoken_seat}进入{profile.name}",
provider="local",
conversation_id="conv-explicit-seat-mode-selection",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
assert result["status"] == "succeeded", result.get("validation")
assert _action_map(result) == expected
assert (
service.mode_profiles.profile_revision(
service.mode_profiles.get_profile(mode_id, "denza_n7"),
)
== original_revision
)
assert any(
operation.target_scope != seat
for operation in service.mode_profiles.get_profile(mode_id, "denza_n7").operations
if operation.domain == "seat"
), "single-seat execution must not edit the persisted dual-seat profile"
@pytest.mark.parametrize(
("mode_id", "spoken_seat"),
[
(mode_id, seat)
for mode_id in ("zero_gravity", "easy_entry_exit", "rocking_horse", "office")
for seat in ("主驾", "后排", "所有座椅")
if (mode_id, seat) != ("office", "主驾")
],
)
def test_mode_invocation_still_rejects_genuinely_unsupported_seat_scopes(
service: SeatDemoService,
mode_id: str,
spoken_seat: str,
) -> None:
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
result = service.preview_native_can(
f"请让{spoken_seat}进入{profile.name}",
provider="local",
conversation_id="conv-explicit-seat-mode-rejection",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
assert result["status"] in {"no_action", "needs_confirmation"}
assert _action_map(result) == {}
assert any(
issue.get("reason") == "mode_target_scope_mismatch"
for issue in result["intent_ir"]["spec"].get("ambiguities", [])
)
@pytest.mark.parametrize(
"message",
("创建一个主驾办公模式", "修改办公模式,只要主驾", "主驾办公模式"),
)
def test_mode_seat_selection_requires_an_explicit_invocation_not_configuration(
service: SeatDemoService,
message: str,
) -> None:
profile = service.mode_profiles.get_profile("office", "denza_n7")
assert service._explicit_mode_seat_selection(profile, message) is None
@pytest.mark.parametrize("mode_id", ("office", "nap", "meditation_space", "leisure_lounger"))
def test_unscoped_mode_invocation_keeps_registered_native_driver_actions(
service: SeatDemoService,
mode_id: str,
) -> None:
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
result = service.preview_native_can(
f"进入{profile.name}",
provider="local",
conversation_id="conv-mode-native-seat-preserved",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
assert result["status"] == "succeeded"
assert _action_map(result) == {
(operation.capability, operation.target_scope): operation.parameters
for operation in profile.operations
}
@@ -0,0 +1,250 @@
from __future__ import annotations
from unittest.mock import patch
import pytest
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.scene_composer import normalize_scene_output
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
@pytest.fixture(scope="module")
def scene_service(tmp_path_factory):
service = SeatDemoService()
service.artifact_root = tmp_path_factory.mktemp("scene-explicit-seat-scope")
service.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes",
user_root=tmp_path_factory.mktemp("scene-explicit-seat-user-modes"),
)
try:
yield service, AssistantCapabilityCatalog(service).scene_composition_context()
finally:
service.close()
def _plan(context, scopes):
requests = []
if "driver" in scopes:
requests.append({
"capability": "seat.set_driver_massage_native",
"target_scope": "driver",
"parameters": {"action": "mode", "mode": "relax"},
})
if "passenger" in scopes:
requests.append({
"capability": "seat.set_backrest_recline",
"target_scope": "passenger",
"parameters": {"position_pct": 45},
})
requests.append({
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"parameters": {"temperature_c": 24},
})
return normalize_scene_output({
"requests": requests, "ambiguities": [], "confidence": 0.95,
}, context)
def _preview(
scene_service, message, scopes=("driver", "passenger"), *, execution_mode="hardware",
):
service, context = scene_service
with patch.object(
service.hardware_execution, "reserve",
side_effect=AssertionError("scene scope validation must not execute hardware"),
):
return service.preview_native_can(
message,
provider="local",
conversation_id="conv-explicit-scene-seat-scope",
execution_mode=execution_mode,
vehicle_id="denza_n7",
assistant_plan=_plan(context, scopes),
)
@pytest.mark.parametrize("message", (
"构建一个主驾座椅舒适模式",
"创建一个仅主驾舒适模式",
"创建一个只给驾驶员坐着放松的功能模式",
"构建一个副驾座椅舒适模式",
"构建一个仅副驾舒适模式",
"构建一个右前座椅舒适模式",
"构建一个后排座椅舒适模式",
"构建一个主驾座椅与副驾空调舒适模式",
"构建一个主驾阅读灯与副驾座椅舒适模式",
))
def test_explicit_scene_seat_scope_rejects_cross_seat_plan_atomically(scene_service, message):
result = _preview(scene_service, message)
assert result["status"] != "succeeded"
assert result.get("mode_profile_draft") is None
assert result["hardware_execution_allowed"] is False
assert result.get("command_plan") is None
assert result.get("command_plans") == {}
intent = result.get("intent_ir") or {}
assert (intent.get("spec") or {}).get("actions", []) == []
assert any(
"座位范围" in item["message"] or item.get("code") == "MODE_SEAT_SCOPE_UNAVAILABLE"
for item in result["validation"]
)
@pytest.mark.parametrize(("message", "scopes"), (
("构建一个主驾座椅舒适模式", ("driver",)),
("构建一个副驾座椅舒适模式", ("passenger",)),
("创建一个仅主驾舒适模式", ("driver",)),
("构建一个主驾和副驾座椅舒适模式", ("driver", "passenger")),
("构建一个主副驾座椅舒适模式", ("driver", "passenger")),
("构建一个云端座椅舒适模式", ("passenger",)),
("构建一个副驾空调与座椅舒适模式", ("passenger",)),
("构建一个座椅舒适与副驾阅读灯模式", ("passenger",)),
("构建一个主驾座椅与副驾空调舒适模式", ("driver",)),
("构建一个主驾阅读灯与副驾座椅舒适模式", ("passenger",)),
))
def test_scene_scope_preserves_supported_targets_without_borrowing_other_device_zones(
scene_service, message, scopes,
):
result = _preview(scene_service, message, scopes)
assert result["status"] == "succeeded", result.get("validation")
assert result["mode_profile_draft"]
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
actions = result["intent_ir"]["spec"]["actions"]
assert {
action["target"]["instance"]
for action in actions if action["domain"] == "seat"
} == set(scopes)
@pytest.mark.parametrize("message", (
"构建一个主驾舒适模式,副驾不要动",
"构建一个主驾舒适模式,不要动副驾",
))
def test_explicit_atomic_requirements_do_not_receive_scene_scope_relaxation(
scene_service, message,
):
result = _preview(scene_service, message, execution_mode="dry_run")
assert result["status"] != "succeeded"
assert result.get("mode_profile_draft") is None
assert result["hardware_execution_allowed"] is False
def test_unqualified_explicit_seat_requirement_defaults_to_passenger_without_model_additions(scene_service):
result = _preview(scene_service, "构建一个舒展模式,座椅靠背调整到45%", execution_mode="dry_run")
assert result["status"] == "succeeded", result.get("validation")
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["mode_profile_draft"]["operation_count"] == 1
actions = result["intent_ir"]["spec"]["actions"]
assert len(actions) == 1
assert actions[0]["capability"] == "seat.set_backrest_recline"
assert actions[0]["target"]["instance"] == "passenger"
assert actions[0]["parameters"] == {"position_pct": 45}
def test_explicit_atomic_requirements_override_label_without_adding_model_actions(scene_service):
result = _preview(scene_service, "构建一个主驾舒适模式,副驾靠背调整到45%")
assert result["status"] == "succeeded"
assert result["execution_mode"] == "dry_run"
actions = result["intent_ir"]["spec"]["actions"]
assert len(actions) == 1
assert actions[0]["capability"] == "seat.set_backrest_recline"
assert actions[0]["target"]["instance"] == "passenger"
assert actions[0]["parameters"]["position_pct"] == 45
def test_rear_window_scene_is_not_rejected_as_an_unsupported_seat(scene_service):
service, context = scene_service
plan = normalize_scene_output({
"requests": [{
"capability": "window.set_position",
"target_scope": "rear_right",
"parameters": {"position_pct": 45},
}],
"ambiguities": [], "confidence": 0.95,
}, context)
result = service.preview_native_can(
"构建一个后排换气模式", provider="local",
conversation_id="conv-rear-window-not-seat",
execution_mode="dry_run", vehicle_id="denza_n7", assistant_plan=plan,
)
assert result["status"] == "succeeded"
assert result["mode_profile_draft"]
@pytest.mark.parametrize(("text", "expected"), (
("进入主驾舒适模式", frozenset({"driver"})),
("进入仅副驾舒适模式", frozenset({"passenger"})),
("进入主副驾座椅舒适模式", frozenset({"driver", "passenger"})),
("进入主驾座椅与副驾的空调舒适模式", frozenset({"driver"})),
("进入副驾座椅与主驾的阅读灯模式", frozenset({"passenger"})),
("进入座椅舒适与右前车窗模式", None),
("进入座椅舒适与后排阅读灯模式", None),
("进入后排座椅舒适模式", frozenset({"unsupported"})),
))
def test_scene_scope_resolution_only_uses_seat_or_unqualified_scene_targets(text, expected):
assert resolve_scene_seat_scopes(text) == expected
def test_new_creation_cannot_borrow_draft_reference_to_bypass_explicit_scope(scene_service):
service, context = scene_service
built = _preview(scene_service, "构建一个主驾独享舒适模式", ("driver",))
reference = {key: built["mode_profile_draft"][key] for key in (
"name", "source_request_id", "vehicle_id",
)}
result = service.preview_native_can(
"构建一个主驾独享舒适模式", provider="local",
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
vehicle_id="denza_n7", assistant_plan=_plan(context, ("driver", "passenger")),
mode_profile_draft_context=reference,
)
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
@pytest.mark.parametrize(("initial_scope", "name", "change", "added_capability"), (
("driver", "主驾释压舒适模式", "副驾座椅通风调整为1档", "seat.set_ventilation"),
("passenger", "副驾释压舒适模式", "主驾座椅通风调整为低档", "seat.set_driver_ventilation_native"),
))
def test_explicit_revision_can_extend_old_label_scope_then_save_and_replay(
scene_service, initial_scope, name, change, added_capability,
):
service, _context = scene_service
built = _preview(scene_service, f"构建一个{name}", (initial_scope,))
source_draft = built["mode_profile_draft"]
reference = {key: source_draft[key] for key in (
"name", "source_request_id", "vehicle_id",
)}
revised = service.preview_native_can(
f"把{name}的{change}", provider="local",
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
vehicle_id="denza_n7", mode_profile_draft_context=reference,
)
assert revised["status"] == "succeeded", revised.get("validation")
draft = revised["mode_profile_draft"]
assert draft["generation_source"] == "draft_revision"
assert draft["operation_count"] == source_draft["operation_count"] + 1
actions = revised["intent_ir"]["spec"]["actions"]
assert added_capability in {action["capability"] for action in actions}
assert {
action["target"]["instance"] for action in actions if action["domain"] == "seat"
} == {"driver", "passenger"}
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded"
replayed = service.execute_mode_profile_card(
saved["mode_profile"]["id"], execution_mode="dry_run", vehicle_id="denza_n7",
)
assert replayed["status"] == "succeeded", replayed.get("validation")
assert {
(action["capability"], action["target"]["instance"])
for action in replayed["intent_ir"]["spec"]["actions"] if action["domain"] == "seat"
} == {
(action["capability"], action["target"]["instance"])
for action in actions if action["domain"] == "seat"
}
+139 -5
View File
@@ -12,6 +12,7 @@ import pytest
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.scene_composer import normalize_scene_output
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@@ -94,6 +95,7 @@ def _assistant_preview(
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
fast_answer = catalog.fast_answer(message, snapshot)
events: list[dict[str, object]] = []
raw_results: list[dict[str, object]] = []
passed_plans: list[dict[str, object] | None] = []
@@ -122,6 +124,7 @@ def _assistant_preview(
conversation_id=conversation_id,
principal="scene-template-hybrid-test",
history=[],
authoritative_answer=fast_answer.answer if fast_answer else None,
emit=lambda event: events.append(event) or True,
execute_control=execute_control,
capability_snapshot=snapshot,
@@ -141,7 +144,7 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
templates = snapshot["scene_templates"]
assert isinstance(templates, list)
assert len(templates) == 12
assert len(templates) == 15
assert all(template["assistant_callable"] is True for template in templates)
with patch.object(
@@ -166,11 +169,30 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
assert result["orchestration_route"] == "mode_profile_create_preview"
draft = result["mode_profile_draft"]
assert draft["generation_source"] == "scene_template_copy"
assert draft["operation_count"] == template["operation_count"]
assert draft["source_template_id"] == template["id"]
assert draft["source_template_revision"] == template["template_revision"]
assert "existing_mode_id" not in draft
expected_requests = plan["requests"]
if plan.get("schedule"):
expected_schedule = deepcopy(plan["schedule"])
for phase in expected_schedule["phases"]:
phase["operations"] = [
op for op in phase["operations"]
if op["domain"] != "seat" or op["target_scope"] == "passenger"
]
assert draft["schedule"] == expected_schedule
expected_requests = [
{**op, "parameters": [{"name": key, "value": value}
for key, value in op["parameters"].items()]}
for phase in expected_schedule["phases"] for op in phase["operations"]
]
else:
expected_requests = [
request for request in expected_requests
if request["domain"] != "seat" or request["target_scope"] == "passenger"
]
assert draft["operation_count"] == len(expected_requests)
expected = {
(
str(request["capability"]),
@@ -184,7 +206,7 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
sort_keys=True,
),
)
for request in plan["requests"]
for request in expected_requests
}
actual = {
(
@@ -308,7 +330,10 @@ def test_purpose_descriptions_use_a_concise_name_and_zero_model_template_route(
assert draft["name"] == expected_name
assert draft["generation_source"] == "scene_template_copy"
assert draft["source_template_id"] == template_id
assert draft["operation_count"] == len(template.operations)
assert draft["operation_count"] == sum(
operation.domain != "seat" or operation.target_scope == "passenger"
for operation in template.operations
)
assert passed_plans[0]["metadata"]["template_id"] == template_id
assert events[0]["engine"] == "n7web-scene-template"
assert not list(user_root.rglob("*.yaml"))
@@ -339,6 +364,115 @@ def test_explicit_settings_on_a_purpose_scene_bypass_template_defaults(
client.start_turn.assert_not_called()
@pytest.mark.parametrize(
("message", "expected_name", "template_id"),
(
("构建一个座椅舒适的功能模式", "座椅舒适模式", "seat-comfort"),
("构建一个让人坐着放松的功能模式", "坐着放松模式", "relaxation"),
("请帮我创建一个适合乘坐舒适的模式吧", "乘坐舒适模式", "seat-comfort"),
("帮我构建一个让我们坐着舒服的功能模式", "坐着舒服模式", "seat-comfort"),
("创建一个座椅放松模式并预览", "座椅放松模式", "relaxation"),
("构建一个长途舒适模式", "长途舒适模式", "long-trip-comfort"),
),
)
def test_seating_comfort_requests_preview_only_passenger_with_no_model_or_hardware_call(
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
message: str,
expected_name: str,
template_id: str,
) -> None:
service, catalog, user_root = hybrid_service
with patch.object(
service.hardware_execution,
"reserve",
side_effect=AssertionError("seat comfort preview reached hardware execution"),
):
response, result, _events, client, _plans = _assistant_preview(
service, catalog, message, conversation_id="conv-seat-comfort-preview",
)
assert response["route"] == "scene_template"
assert response["success"] is True
assert result["status"] == "succeeded"
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["mode_profile_draft"]["name"] == expected_name
assert result["mode_profile_draft"]["source_template_id"] == template_id
seat_actions = {
(capability, target): parameters
for (capability, target), parameters in _action_map(result).items()
if capability.startswith("seat.")
}
assert {target for _capability, target in seat_actions} == {"passenger"}
assert all(
"driver_axis" not in capability and "driver_position" not in capability
for capability, _target in seat_actions
)
assert ("seat.set_driver_massage_native", "passenger") not in seat_actions
if template_id in {"seat-comfort", "long-trip-comfort"}:
assert seat_actions["seat.set_ventilation", "passenger"] == {
"state": "on", "level": 1,
}
client.start_structured_turn.assert_not_called()
client.start_turn.assert_not_called()
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize("name", ("云端座椅舒展模式", "晚间乘坐释压模式"))
def test_unregistered_seat_scene_keeps_both_explicit_targets_through_native_merge(
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
name: str,
) -> None:
service, catalog, user_root = hybrid_service
plan = normalize_scene_output({
"requests": [
{"capability": "seat.set_driver_massage_native", "target_scope": "driver",
"parameters": {"action": "mode", "mode": "relax"}},
{"capability": "seat.set_driver_heating_native", "target_scope": "driver",
"parameters": {"state": "on", "level": 2}},
{"capability": "seat.set_backrest_recline", "target_scope": "passenger",
"parameters": {"position_pct": 45}},
{"capability": "seat.set_heating", "target_scope": "passenger",
"parameters": {"state": "on", "level": 1}},
],
"ambiguities": [], "confidence": 0.95,
}, catalog.scene_composition_context())
with patch.object(
service.hardware_execution, "reserve",
side_effect=AssertionError("scene composition reached hardware execution"),
):
result = service.preview_native_can(
f"构建一个主驾和副驾的{name}", provider="local", conversation_id="conv-seat-generated",
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
)
assert result["status"] == "succeeded"
assert result["execution_mode"] == "dry_run"
assert result["mode_profile_draft"]["generation_source"] == "codex_scene_composition"
assert set(_action_map(result)) == {
("seat.set_driver_massage_native", "driver"),
("seat.set_driver_heating_native", "driver"),
("seat.set_backrest_recline", "passenger"),
("seat.set_heating", "passenger"),
}
assert not list(user_root.rglob("*.yaml"))
def test_unscoped_seat_instruction_inside_mode_creation_defaults_to_passenger(
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
) -> None:
service, _catalog, _user_root = hybrid_service
result = service.preview_native_can(
"构建一个舒展模式,座椅靠背调整到45%", provider="local",
conversation_id="conv-seat-explicit-target", execution_mode="hardware",
vehicle_id="denza_n7",
)
assert result["status"] == "succeeded"
assert _action_map(result) == {
("seat.set_backrest_recline", "passenger"): {"position_pct": 45},
}
assert result.get("mode_profile_draft")
assert result["hardware_execution_allowed"] is False
def test_template_metadata_cannot_authorize_modified_or_stale_template_actions(
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
) -> None:
@@ -440,7 +574,7 @@ def test_template_copy_can_be_revised_twice_without_losing_sibling_actions(
assert shade_revised["execution_mode"] == final["execution_mode"] == "dry_run"
assert shade_revised["hardware_execution_allowed"] is False
assert final["hardware_execution_allowed"] is False
assert len(built_actions) == len(shade_actions) == len(final_actions) == 7
assert len(built_actions) == len(shade_actions) == len(final_actions) == 10
assert built_actions[shade_key] == {"position_pct": 0}
assert shade_actions[shade_key] == final_actions[shade_key] == {"position_pct": 100}
assert built_actions[temperature_key] == shade_actions[temperature_key] == {"temperature_c": 24}
+435
View File
@@ -0,0 +1,435 @@
"use strict";
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 web = path.resolve(__dirname, "../../apps/web-console");
const assistant = fs.readFileSync(path.join(web, "assistant-chat.js"), "utf8");
const app = fs.readFileSync(path.join(web, "app.js"), "utf8");
function functionSource(source, name) {
const start = source.search(new RegExp(`(?: )?(?:async )?function ${name}\\(`));
assert.ok(start >= 0, `missing production function ${name}`);
const rest = source.slice(start);
const next = rest.slice(1).search(/\n(?: )?(?:async )?function /);
return next < 0 ? rest : rest.slice(0, next + 1);
}
class Element {
constructor(tagName) {
this.tagName = tagName;
this.children = [];
this.dataset = {};
this.attributes = {};
this.listeners = {};
this.className = "";
this.textContent = "";
this.classList = { add: (name) => { this.className += ` ${name}`; } };
}
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = children; }
setAttribute(name, value) { this.attributes[name] = value; }
addEventListener(name, listener) { this.listeners[name] = listener; }
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
querySelectorAll(selector) {
return descendants(this).filter((item) => {
if (selector.startsWith(".")) return item.className.split(/\s+/).includes(selector.slice(1));
const match = selector.match(/^\[data-([\w-]+)(?:="([^"]*)")?\]$/);
if (match) {
const key = match[1].replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
return key in item.dataset && (match[2] === undefined || item.dataset[key] === match[2]);
}
return item.tagName === selector;
});
}
}
function descendants(element) {
return element.children.flatMap((item) => typeof item === "object" ? [item, ...descendants(item)] : []);
}
function textOf(element) {
return [element.textContent, ...descendants(element).map((item) => item.textContent)].filter(Boolean).join("\n");
}
function harness() {
const body = new Element("body");
const calls = [];
const document = {
body,
createElement: (tag) => new Element(tag),
createTextNode: (text) => Object.assign(new Element("#text"), { textContent: text }),
querySelectorAll: (selector) => body.querySelectorAll(selector),
};
const context = {
document, window: {}, CSS: { escape: String },
$: (selector, parent = body) => parent.querySelector(selector),
rememberModeDraftContext() {}, supersedeModeDraftCards() {}, followLatest() {},
normalizeModeName: String,
saveModeProfileDraft: (...args) => calls.push({ kind: "save", args }),
tryModeProfileDraft: (...args) => calls.push({ kind: "trial", args }),
modeSecondsLabel: (value) => String(Number(value) / 1000),
LEGACY_DYNAMIC_MODE_IDS: new Set(["rocking_horse", "dynamic_cradle"]),
};
vm.createContext(context);
const constants = app.slice(app.indexOf("const MODE_DOMAIN_LABELS"), app.indexOf("function modeOperationParameters"))
+ app.slice(app.indexOf("const CONFIG_CAPABILITY_LABELS"), app.indexOf("function makeConfigElement"));
const assistantNames = ["modeDraftRows", "modeDraftScheduleView", "appendModeDraftRows", "appendModeDraftState"];
const appNames = ["formatConfigValue", "modeOperationParameters", "formatModeOperationValue", "modeOperationView", "modeTimelineTargetSummary", "modeTimelineStepView", "modeScheduleDurationLabel", "modeScheduleView", "modeExecutionActionViews", "modeScheduleTrialMatches", "modeScheduleExecutionView", "isUserModeProfile", "isDynamicModeProfile", "createModeGalleryCard", "renderModeActionPopover"];
vm.runInContext(constants + assistantNames.map((name) => functionSource(assistant, name)).join("\n")
+ appNames.map((name) => functionSource(app, name)).join("\n"), context);
return { context, body, calls };
}
function operation(capability, parameters, scope = "vehicle") {
return { domain: capability.split(".")[0], capability, target_scope: scope, operation: "set", parameters };
}
function schedule(overrides = {}) {
return {
schema: "cognitive-agent-mode-schedule/v1",
repeat_count: 2,
phases: [
{ id: "warm", name: "快速预热", duration_ms: 60000, operations: [
operation("climate.set_temperature", { temperature_c: 28 }),
operation("seat.set_driver_massage_native", { action: "mode", mode: "relax" }, "driver"),
operation("seat.set_heating", { state: "on", level: 1 }, "passenger"),
] },
{ id: "comfortable", name: "舒适保持", duration_ms: 30000, operations: [
operation("climate.set_temperature", { temperature_c: 24 }),
operation("seat.set_driver_massage_native", { action: "off" }, "driver"),
operation("seat.set_heating", { state: "off" }, "passenger"),
] },
],
...overrides,
};
}
test("assistant keeps phases, relative timing, repeat count, total duration and stop guidance", () => {
const { context } = harness();
const view = context.modeDraftScheduleView({ schedule: schedule() });
assert.equal(view.summary, "2 个阶段 · 重复 2 次 · 总时长 3分");
assert.equal(view.operationCount, 6);
assert.equal(view.phases[0].timing, "+0秒 开始 · 持续 1分");
assert.equal(view.phases[1].timing, "+1分 开始 · 持续 30秒");
assert.match(view.stopHint, /停止动态模式/);
assert.match(view.stopHint, /每轮/);
});
test("driver and passenger scopes and changed device values are not collapsed", () => {
const { context } = harness();
const view = context.modeDraftScheduleView({ schedule: schedule() });
assert.deepEqual(Array.from(view.phases[0].rows, (row) => [row.label, row.value]), [
["空调温度", "28℃"], ["主驾座椅按摩", "放松模式"], ["副驾座椅加热", "开启 · 1档"],
]);
assert.deepEqual(Array.from(view.phases[1].rows, (row) => [row.label, row.value]), [
["空调温度", "24℃"], ["主驾座椅按摩", "关闭"], ["副驾座椅加热", "关闭"],
]);
});
test("phase previews never apply whole-instruction zero-gravity grouping", () => {
const { context } = harness();
const view = context.modeDraftScheduleView({ instruction: "副驾零重力", schedule: schedule({ phases: [{
duration_ms: 1500, operations: [
operation("seat.set_longitudinal_position", { position_pct: 80 }, "passenger"),
operation("seat.set_backrest_recline", { position_pct: 60 }, "passenger"),
],
}] }) });
assert.equal(view.phases[0].rows.length, 2);
assert.equal(view.phases[0].name, "阶段 1");
assert.equal(view.phases[0].timing, "+0秒 开始 · 持续 1.5秒");
});
test("per-zone climate targets are explicit even within the same phase", () => {
const { context } = harness();
const view = context.modeDraftScheduleView({ schedule: schedule({ phases: [{
duration_ms: 2000, operations: [
operation("climate.set_temperature", { temperature_c: 26 }, "front_left"),
operation("climate.set_temperature", { temperature_c: 24 }, "front_right"),
],
}] }) });
assert.deepEqual(Array.from(view.phases[0].rows, (row) => [row.label, row.value]), [
["主驾空调温度", "26℃"], ["副驾空调温度", "24℃"],
]);
});
for (const [name, value] of [
["absent", undefined], ["unknown schema", schedule({ schema: "other" })],
["zero repeat", schedule({ repeat_count: 0 })], ["unbounded repeat", schedule({ repeat_count: 21 })],
["fractional repeat", schedule({ repeat_count: 1.5 })], ["string repeat", schedule({ repeat_count: "2" })],
["empty phases", schedule({ phases: [] })], ["non-array phases", schedule({ phases: {} })],
["zero duration", schedule({ phases: [{ duration_ms: 0, operations: [] }] })],
["negative duration", schedule({ phases: [{ duration_ms: -1, operations: [] }] })],
["invalid duration", schedule({ phases: [{ duration_ms: Infinity, operations: [] }] })],
["missing operations", schedule({ phases: [{ duration_ms: 1000 }] })],
]) {
test(`schedule formatter safely rejects ${name}`, () => {
const { context } = harness();
assert.equal(context.modeDraftScheduleView({ schedule: value }), null);
assert.equal(context.modeScheduleView({ schedule: value }), null);
});
}
test("hover formatter prefers stage operations over an overlapping flat projection", () => {
const { context } = harness();
const mode = { schedule: schedule(), operations: [operation("climate.set_temperature", { temperature_c: 24 })] };
const rows = context.modeExecutionActionViews(mode);
assert.equal(rows.length, 6);
assert.equal(rows[0].phase, "阶段 1:快速预热");
assert.equal(rows[3].phase, "阶段 2:舒适保持");
assert.equal(rows[0].parameters, "目标温度:28 ℃");
assert.equal(rows[3].parameters, "目标温度:24 ℃");
assert.equal(rows[1].target, "主驾");
assert.equal(rows[2].target, "副驾");
assert.equal(context.modeScheduleView(mode).summary, context.modeDraftScheduleView(mode).summary);
assert.equal(context.isDynamicModeProfile({ ...mode, behavior: "static" }), true);
});
test("legacy static modes and seat ModeTimeline retain their original action semantics", () => {
const { context } = harness();
const operations = [operation("climate.set_temperature", { temperature_c: 24 })];
assert.equal(context.modeExecutionActionViews({ operations }).length, 1);
assert.equal(context.isDynamicModeProfile({ operations, behavior: "static" }), false);
const mode = { operations, timeline: {
entry: { targets: { backrest_recline: { position_pct: 40 } }, timeout_ms: 15000 },
phases: [{ name: "抬起", targets: { backrest_recline: { position_pct: 50 } }, duration_ms: 2000 }],
} };
const views = context.modeExecutionActionViews(mode);
assert.equal(views.length, 3);
assert.equal(views[1].action, "进入动态模式初始位置");
assert.match(views[1].parameters, /保护上限 15s/);
assert.equal(views[2].action, "循环阶段 1:抬起");
assert.match(views[2].parameters, /持续 2s/);
assert.equal(context.isDynamicModeProfile(mode), true);
});
test("assistant renders a stage card, preserving opaque save/trial request context", () => {
const { context, body, calls } = harness();
const message = { body: new Element("div"), article: new Element("article") };
body.append(message.body);
const draft = { name: "上车预热模式", source_request_id: "opaque-preview-id", schedule: schedule(), operation_count: 6 };
const flatActions = [];
context.appendModeDraftState(message, draft, flatActions);
const card = message.body.children[0];
assert.equal(card.querySelectorAll(".assistant-mode-schedule-phase").length, 2);
assert.equal(card.querySelectorAll(".assistant-mode-draft-row").length, 6);
assert.equal(card.querySelector(".assistant-mode-draft-badge").textContent, "6 项动作");
assert.match(textOf(card), /28℃/);
assert.match(textOf(card), /24℃/);
assert.match(textOf(card), /重复 2 次 · 总时长 3分/);
assert.match(textOf(card), /停止动态模式/);
card.querySelector(".assistant-mode-draft-save").listeners.click();
card.querySelector(".assistant-mode-draft-trial").listeners.click();
assert.equal(calls[0].kind, "save");
assert.equal(calls[0].args[2], draft);
assert.equal(calls[1].kind, "trial");
assert.equal(calls[1].args[1], draft);
assert.equal(calls[1].args[2], flatActions);
});
test("management schedule details are independently expandable without executing the card", () => {
const { context } = harness();
const card = context.createModeGalleryCard({ id: "custom-warm", name: "上车预热模式", origin: "user", schedule: schedule() });
const details = card.querySelector(".mode-gallery-schedule-details");
assert.ok(details);
assert.equal(details.querySelector("summary").textContent, "查看分阶段执行动作");
assert.equal(details.querySelectorAll("li").length, 2);
assert.equal(details.querySelectorAll("button").length, 0);
assert.match(textOf(details), /快速预热/);
assert.match(textOf(details), /舒适保持/);
assert.match(textOf(details), /主驾 · 设置主驾座椅按摩:动作:切换按摩模式,工作模式:放松/);
assert.match(textOf(card.querySelector(".mode-gallery-card-meta")), /重复 2 次 · 总时长 3分/);
});
test("popover presents stage timings and stop guidance without interpreting text as HTML", () => {
const { context } = harness();
const popover = new Element("aside");
for (const [tag, key] of [["strong", "modeActionTitle"], ["b", "modeActionCount"], ["ol", "modeActionList"]]) {
const item = new Element(tag);
item.dataset[key] = "true";
popover.append(item);
}
context.ensureModeActionPopover = () => popover;
const value = schedule();
value.phases[0].name = '<img src=x onerror="alert(1)">';
context.renderModeActionPopover({ name: "测试动态模式", schedule: value });
assert.equal(popover.querySelectorAll(".mode-chip-action-copy").length, 6);
assert.equal(popover.querySelectorAll(".mode-schedule-popover-phase").length, 6);
assert.match(textOf(popover), /阶段 1:<img/);
assert.match(textOf(popover), /停止动态模式/);
assert.match(textOf(popover), /\+1分 开始 · 持续 30秒/);
assert.equal(popover.querySelectorAll("img").length, 0);
});
test("execution distinguishes accepted schedule, virtual trial and stopped device state", () => {
const { context } = harness();
const running = context.modeScheduleExecutionView({ status: "running", schedule_execution: { status: "running" } });
assert.equal(running.state, "processing");
assert.equal(running.title, "动态时间轴已启动");
assert.doesNotMatch(running.title, /完成/);
const trial = context.modeScheduleExecutionView({ status: "succeeded", schedule_execution: { status: "completed", completed: true, virtual_elapsed_ms: 90000 } });
assert.equal(trial.title, "动态时间轴模拟试运行通过");
assert.match(trial.detail, /未向车辆下发控制/);
const stop = context.modeScheduleExecutionView({ status: "succeeded", schedule_execution: { status: "cancelled" } });
assert.equal(stop.title, "动态时间轴已停止");
assert.match(stop.detail, /不会自动回滚/);
assert.equal(context.modeScheduleExecutionView({ status: "failed", schedule_execution: { status: "failed" } }), null);
});
test("virtual trial verifies temporal structure, not just a flattened action set", () => {
const { context } = harness();
const draft = { schedule: schedule() };
const response = () => ({ schedule: schedule(), schedule_execution: { status: "completed", completed: true, virtual_elapsed_ms: 180000 } });
assert.equal(context.modeScheduleTrialMatches(draft, response()), true);
assert.equal(context.modeScheduleTrialMatches({}, {}), true, "legacy static drafts remain unchanged");
for (const mutate of [
(value) => { value.schedule.repeat_count = 1; },
(value) => { value.schedule.phases.reverse(); },
(value) => { value.schedule.phases[0].duration_ms = 45000; value.schedule.phases[1].duration_ms = 45000; },
(value) => { value.schedule.phases[0].operations[0].parameters.temperature_c = 26; },
(value) => { value.schedule.phases[0].operations[1].target_scope = "passenger"; },
(value) => { value.schedule_execution.virtual_elapsed_ms = 90000; },
(value) => { value.schedule_execution.completed = false; },
(value) => { value.schedule_execution.status = "running"; },
(value) => { delete value.schedule; },
]) {
const value = response();
mutate(value);
assert.equal(context.modeScheduleTrialMatches(draft, value), false);
}
});
function pollHarness(statuses = []) {
const { context } = harness();
const timers = new Map();
const requests = [];
const updates = [];
const modeUpdates = [];
const vehicle = { value: "denza_n7" };
let timerId = 0;
context.activeSchedulePoll = null;
context.AbortController = AbortController;
context.conversation = "conversation-1";
context.conversationId = () => context.conversation;
context.setActionStatus = (_message, copy, state) => updates.push({ copy, state });
const originalQuery = context.$;
context.$ = (selector, parent) => selector === "#vehicleSelect" ? vehicle : originalQuery(selector, parent);
context.window.setTimeout = (callback, delay) => {
assert.equal(delay, 2000);
timers.set(++timerId, callback);
return timerId;
};
context.window.clearTimeout = (id) => timers.delete(id);
context.window.CognitiveAgentConsole = { updateScheduleExecutionStatus: (...args) => modeUpdates.push(args) };
context.window.CognitiveAgentNetwork = { request: async (url, options) => {
requests.push({ url, options });
const next = statuses.shift();
if (next instanceof Error) throw next;
if (typeof next === "function") return next();
return { ok: true, status: 200, json: async () => next };
} };
vm.runInContext(["stopScheduleStatusPoll", "startScheduleStatusPoll"].map((name) => functionSource(assistant, name)).join("\n"), context);
const start = (session = "session-1") => context.startScheduleStatusPoll({
vehicle_id: "denza_n7", mode_profile: { id: "test-mode" }, schedule_execution: { session_id: session },
}, {});
const tick = async () => {
const [id, callback] = timers.entries().next().value || [];
assert.ok(callback, "poll timer must be scheduled");
timers.delete(id);
await callback();
};
return { context, timers, requests, updates, modeUpdates, vehicle, start, tick };
}
test("polls only the active vehicle using GET and updates the original status until completion", async () => {
const h = pollHarness([
{ status: "running", session_id: "session-1", vehicle_id: "denza_n7", phase_index: 1, repeat_index: 0 },
{ status: "completed", session_id: "session-1", completed: true },
]);
h.start();
assert.equal(h.requests.length, 0);
await h.tick();
assert.equal(h.requests.length, 1);
assert.equal(h.requests[0].url, "/api/modes/schedule/status?vehicle_id=denza_n7");
assert.equal(h.requests[0].options.method, "GET");
assert.equal(h.requests[0].options.body, undefined);
assert.match(h.updates[0].copy, /第 1 轮 · 阶段 2/);
assert.equal(h.updates[0].state, "working");
await h.tick();
assert.equal(h.timers.size, 0);
assert.equal(h.context.activeSchedulePoll, null);
assert.match(h.updates[1].copy, /时间轴已结束/);
assert.deepEqual(h.modeUpdates, [["test-mode", "completed"]]);
});
for (const status of ["failed", "cancelled", "stopped", "idle"]) {
test(`terminal ${status} ends polling without a new request or action`, async () => {
const h = pollHarness([{ status, session_id: "session-1", error: "阶段下发失败" }]);
h.start();
await h.tick();
assert.equal(h.requests.length, 1);
assert.equal(h.timers.size, 0);
assert.equal(h.updates[0].state, status === "failed" ? "failed" : "success");
if (status === "failed") assert.match(h.updates[0].copy, /阶段下发失败/);
if (["cancelled", "stopped"].includes(status)) assert.match(h.updates[0].copy, /已停止/);
});
}
test("a transient status failure is visible and retried, not reported as execution failure", async () => {
const h = pollHarness([new Error("offline"), { status: "completed", completed: true }]);
h.start();
await h.tick();
assert.match(h.updates[0].copy, /暂时无法获取时间轴状态/);
assert.equal(h.updates[0].state, "working");
assert.equal(h.timers.size, 1);
await h.tick();
assert.equal(h.timers.size, 0);
});
test("vehicle or conversation changes terminate observation before another request", async () => {
for (const kind of ["vehicle", "conversation"]) {
const h = pollHarness();
h.start();
if (kind === "vehicle") h.vehicle.value = "another-vehicle";
else h.context.conversation = "conversation-2";
await h.tick();
assert.equal(h.requests.length, 0);
assert.equal(h.context.activeSchedulePoll, null);
assert.equal(h.timers.size, 0);
}
});
test("replacement session does not apply a different schedule outcome to the old message", async () => {
const h = pollHarness([{ status: "failed", session_id: "another-session" }]);
h.start();
await h.tick();
assert.match(h.updates[0].copy, /已被新的运行任务替换/);
assert.doesNotMatch(h.updates[0].copy, /执行失败/);
assert.equal(h.timers.size, 0);
assert.equal(h.modeUpdates.length, 0);
});
test("stop aborts an in-flight status read and ignores its late completion", async () => {
let resolve;
const response = new Promise((done) => { resolve = done; });
const h = pollHarness([() => response]);
h.start();
const pending = h.tick();
assert.equal(h.requests.length, 1);
assert.equal(h.timers.size, 0, "an in-flight read cannot overlap another timer");
h.context.stopScheduleStatusPoll();
assert.equal(h.requests[0].options.signal.aborted, true);
resolve({ ok: true, status: 200, json: async () => ({ status: "completed", completed: true }) });
await pending;
assert.equal(h.updates.length, 0);
assert.equal(h.timers.size, 0);
});
test("cleanup is wired to new conversation, vehicle changes and page exit", () => {
assert.match(assistant, /if \(!target\?\.closest\("#newConversation"\)\) return;\s+stopScheduleStatusPoll\(\);/);
assert.match(assistant, /event\.target\?\.id === "vehicleSelect"\) stopScheduleStatusPoll\(\)/);
assert.match(assistant, /window\.addEventListener\("pagehide", stopScheduleStatusPoll\)/);
});
@@ -284,7 +284,7 @@ def test_mode_catalog_projects_bounded_action_details_without_free_form_reasons(
snapshot = _catalog(vehicle_service).snapshot()
office = next(item for item in snapshot["modes"] if item["id"] == "office")
assert len(office["operations"]) == office["operation_count"] == 10
assert len(office["operations"]) == office["operation_count"] == 12
temperature = next(
item
for item in office["operations"]
@@ -327,7 +327,7 @@ def test_saved_mode_detail_fast_answer_is_exact_read_only_and_natural(
"遮阳帘",
"氛围灯",
"副驾空调温度设为25℃",
"共10项动作",
"共12项动作",
"本次仅查询,未下发车辆控制",
)
)
@@ -358,7 +358,7 @@ def test_common_scene_templates_are_read_only_callable_catalog_entries(
templates = snapshot["scene_templates"]
assert snapshot["source_status"]["scene_templates"] == "ready"
assert len(templates) == 12
assert len(templates) == 15
assert {item["name"] for item in templates} >= {
"观影模式",
"露营模式",
@@ -0,0 +1,177 @@
"""Execute the mode preview formatter against actual seat/light parameter shapes."""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
@pytest.fixture(scope="module")
def render_rows():
node = shutil.which("node")
if not node:
runtime = (
Path.home() / ".cache" / "codex-runtimes" / "codex-primary-runtime"
/ "dependencies" / "node" / "bin"
)
node = next((str(path) for path in (runtime / "node.exe", runtime / "node") if path.is_file()), None)
assert node, "Node.js is required to validate rendered mode action text"
source = (ROOT / "apps" / "web-console" / "assistant-chat.js").read_text(encoding="utf-8")
start = source.index(" function modeDraftRows(")
end = source.index("\n function ", start + 1)
script = source[start:end] + "\nconst input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));\n"
script += "console.log(JSON.stringify(modeDraftRows(input.draft, input.actions)));"
def render(actions, instruction=""):
completed = subprocess.run(
[node, "-e", script],
input=json.dumps({"draft": {"instruction": instruction}, "actions": actions}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return json.loads(completed.stdout)
return render
def action(capability, parameters, zone="driver"):
return {
"domain": capability.split(".")[0],
"capability": capability,
"target": {"zone": zone},
"parameters": parameters,
}
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
@pytest.mark.parametrize(
("parameters", "expected"),
[
({"state": "on", "level": 1}, "开启 · 高档"),
({"state": "on", "level": 2}, "开启 · 低档"),
({"state": "on"}, "开启 · 低档"),
({"state": "off", "level": 1}, "关闭"),
],
)
def test_driver_native_comfort_uses_its_own_level_scale(render_rows, kind, parameters, expected):
rows = render_rows([action(f"seat.set_driver_{kind}_native", parameters, "front_left")])
assert rows[0]["label"] == f"主驾座椅{'通风' if kind == 'ventilation' else '加热'}"
assert rows[0]["value"] == expected
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
@pytest.mark.parametrize("level", [1, 2, 3, 4, 5])
def test_passenger_levels_are_not_translated_using_driver_can_codes(render_rows, kind, level):
rows = render_rows([action(f"seat.set_{kind}", {"state": "on", "level": level}, "passenger")])
assert rows[0]["label"] == f"副驾座椅{'通风' if kind == 'ventilation' else '加热'}"
assert rows[0]["value"] == f"开启 · {level}档"
@pytest.mark.parametrize(
("parameters", "expected"),
[
({"action": "off", "mode": "relax"}, "关闭"),
({"action": "mode", "mode": "wave"}, "波浪模式"),
({"action": "mode", "mode": "pulse"}, "脉冲模式"),
({"action": "mode", "mode": "stretch"}, "舒展模式"),
({"action": "mode", "mode": "slack"}, "舒缓模式"),
({"action": "mode", "mode": "relax"}, "放松模式"),
({"action": "strength", "strength": 1}, "轻柔力度"),
({"action": "strength", "strength": 2}, "中等力度"),
({"action": "strength", "strength": 3}, "较强力度"),
({"action": "mode"}, "按摩模式待确认"),
({"action": "strength"}, "按摩力度待确认"),
],
)
def test_massage_preview_distinguishes_operation_mode_and_strength(render_rows, parameters, expected):
rows = render_rows([action("seat.set_driver_massage_native", parameters)])
assert rows[0]["label"] == "主驾座椅按摩"
assert rows[0]["value"] == expected
def test_ambient_native_color_and_brightness_remain_distinct_rows(render_rows):
rows = render_rows([
action("ambient_light.set_color_index", {"color_index": 127}, "vehicle"),
action("ambient_light.set_brightness_level", {"brightness_level": 2}, "vehicle"),
])
assert [(row["label"], row["value"]) for row in rows] == [
("氛围灯颜色", "色号 127"),
("氛围灯亮度", "2档"),
]
@pytest.mark.parametrize("brightness", [0, 20, 100])
def test_rgb_illumination_keeps_exact_color_and_brightness(render_rows, brightness):
rows = render_rows([
action("ambient_light.set_illumination", {"color_hex": "#FFB347", "brightness_pct": brightness}, "vehicle"),
])
assert rows[0]["value"] == f"颜色 #FFB347 · 亮度 {brightness}%"
def test_zero_gravity_grouping_keeps_driver_comfort_visible(render_rows):
rows = render_rows([
action("seat.set_longitudinal_position", {"position_pct": 100}, "passenger"),
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "front_right"),
action("seat.set_driver_massage_native", {"action": "mode", "mode": "relax"}),
], "副驾零重力模式,并开启主驾放松按摩")
assert [(row["label"], row["value"]) for row in rows] == [
("副驾座椅", "零重力模式"),
("主驾座椅按摩", "放松模式"),
]
def test_position_and_power_rows_keep_existing_values(render_rows):
rows = render_rows([
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "passenger"),
action("ambient_light.set_power", {"state": "off"}, "vehicle"),
action("seat.set_ventilation", {"state": "off", "level": 2}, "passenger"),
])
assert [(row["label"], row["value"]) for row in rows] == [
("副驾靠背角度", "81%(150°)"),
("氛围灯", "关闭"),
("副驾座椅通风", "关闭"),
]
@pytest.mark.parametrize(
("capability", "mode", "label", "value"),
[
("climate.set_control_mode", "auto", "空调控制模式", "自动控制"),
("climate.set_control_mode", "manual", "空调控制模式", "手动控制"),
("climate.set_cycle_mode", "internal", "空调循环", "内循环"),
("climate.set_cycle_mode", "external", "空调循环", "外循环"),
("climate.set_outlet_mode", "face", "空调出风模式", "吹面"),
("climate.set_outlet_mode", "face_feet", "空调出风模式", "吹面 / 吹脚"),
("climate.set_outlet_mode", "feet", "空调出风模式", "吹脚"),
("climate.set_outlet_mode", "feet_defrost", "空调出风模式", "吹脚 / 除霜"),
("climate.set_outlet_mode", "defrost", "空调出风模式", "除霜"),
("climate.set_outlet_mode", "face_feet_defrost", "空调出风模式", "吹面 / 吹脚 / 除霜"),
("climate.set_outlet_mode", "face_defrost", "空调出风模式", "吹面 / 除霜"),
],
)
def test_climate_modes_use_capability_specific_labels(render_rows, capability, mode, label, value):
rows = render_rows([action(capability, {"mode": mode}, "vehicle")])
assert (rows[0]["label"], rows[0]["value"]) == (label, value)
def test_climate_control_circulation_and_outlet_are_not_conflated(render_rows):
rows = render_rows([
action("climate.set_control_mode", {"mode": "auto"}, "vehicle"),
action("climate.set_cycle_mode", {"mode": "internal"}, "vehicle"),
action("climate.set_outlet_mode", {"mode": "face_feet"}, "vehicle"),
action("climate.set_fan_level", {"level": 2}, "vehicle"),
])
assert [(row["label"], row["value"]) for row in rows] == [
("空调控制模式", "自动控制"),
("空调循环", "内循环"),
("空调出风模式", "吹面 / 吹脚"),
("空调风量", "2档"),
]
@@ -0,0 +1,56 @@
from __future__ import annotations
from copy import deepcopy
import pytest
from cognitive_agent.application.mode_creation_seat_policy import (
ModeCreationSeatPolicyError,
creation_seat_scopes,
project_seed_operations,
)
@pytest.mark.parametrize(("text", "expected"), [
("构建一个阅读模式", {"passenger"}),
("构建一个座椅动态模式", {"passenger"}),
("构建一个主驾阅读模式", {"driver"}),
("为驾驶员构建一个舒适模式", {"driver"}),
("构建一个副驾阅读模式", {"passenger"}),
("构建一个副驾驶座椅舒适模式", {"passenger"}),
("构建一个副驾驶席阅读模式", {"passenger"}),
("为副驾驶位构建阅读模式", {"passenger"}),
("构建一个主副驾阅读模式", {"driver", "passenger"}),
("构建一个双座舒适模式", {"driver", "passenger"}),
("构建一个前排两座舒适模式", {"driver", "passenger"}),
("构建一个两个座椅阅读模式", {"driver", "passenger"}),
("构建一个两张座椅阅读模式", {"driver", "passenger"}),
("构建一个两座椅阅读模式", {"driver", "passenger"}),
("构建一个主驾空调舒适模式", {"passenger"}),
("构建一个模式,主驾温度26度", {"passenger"}),
("构建一个副驾座椅舒适模式,主驾空调26度", {"passenger"}),
("构建一个主驾舒适模式,副驾空调26度", {"driver"}),
("构建一个阅读模式,不要动主驾", {"passenger"}),
("构建一个主驾阅读模式,副驾不要动", {"driver"}),
("构建一个模式,只联动副驾,不要联动主驾", {"passenger"}),
("构建一个后排座椅舒适模式", {"unsupported"}),
("构建一个未知座位模式", {"unsupported"}),
("构建一个任意座位模式", {"unsupported"}),
])
def test_creation_seat_selection(text, expected):
assert creation_seat_scopes(text) == expected
def test_seed_projection_is_non_mutating_and_does_not_borrow_capabilities():
seed = [
{"domain": "seat", "capability": "seat.set_driver_ventilation_native", "target_scope": "driver", "parameters": {"level": 2}},
{"domain": "seat", "capability": "seat.set_backrest_recline", "target_scope": "passenger", "parameters": {"position_pct": 45}},
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "passenger", "parameters": {"temperature_c": 24}},
]
original = deepcopy(seed)
projected = project_seed_operations(seed, frozenset({"driver"}))
assert projected == [seed[0], seed[2]]
projected[0]["parameters"]["level"] = 1
assert seed == original
with pytest.raises(ModeCreationSeatPolicyError):
project_seed_operations(seed[1:], frozenset({"driver"}))
@@ -0,0 +1,186 @@
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
import pytest
from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
_catalog_exact_mode_match_count,
_catalog_exact_scene_template_plan,
)
from cognitive_agent.application.mode_profile_intent import (
mode_creation_source_name,
parse_mode_profile_create,
)
from cognitive_agent.application.scene_composer import (
SCENE_COMPOSER_INSTRUCTIONS,
SCHEDULE_COMPOSER_INSTRUCTIONS,
scene_context_for_mode_creation,
)
from cognitive_agent.application.scene_templates import SceneTemplateRegistry
ROOT = Path(__file__).resolve().parents[2]
@pytest.mark.parametrize(("message", "name"), [
("构建主驾阅读模式", "主驾阅读模式"),
("为主驾构建一个阅读模式", "主驾阅读模式"),
("帮我为副驾驶构建一个阅读模式", "副驾阅读模式"),
("给驾驶席创建一个阅读模式", "主驾阅读模式"),
("可以帮我为主驾构建一个阅读模式吗?", "主驾阅读模式"),
("构建一个阅读模式,针对主驾", "主驾阅读模式"),
("构建一个阅读模式,仅联动副驾座椅", "副驾阅读模式"),
("构建一个阅读模式,给主驾使用,先看看", "主驾阅读模式"),
("构建一个主驾驶座椅专属的阅读模式", "主驾阅读模式"),
("构建一个主驾适合看书的功能模式", "主驾看书模式"),
("构建一个适合主驾看书的功能模式", "主驾看书模式"),
("为主驾和副驾创建一个阅读模式", "主副驾阅读模式"),
("构建一个双座阅读模式", "主副驾阅读模式"),
("构建一个阅读模式,针对双座", "主副驾阅读模式"),
])
def test_scoped_creation_keeps_distinct_name_and_fast_template_source(message, name):
assert parse_mode_profile_create(message) == (name, "")
registry = SceneTemplateRegistry(ROOT / "config" / "scene_templates")
template = registry.resolve_exact_name(name)
assert template is not None and template.id == "reading"
@pytest.mark.parametrize(("message", "expected"), [
("为主驾构建一个阅读模式,空调调整到26度", ("主驾阅读模式", "空调调整到26度")),
("构建一个阅读模式,针对主驾,空调调整到26度", ("阅读模式", "针对主驾,空调调整到26度")),
("构建一个主驾阅读模式,靠背向后移动3s", ("主驾阅读模式", "靠背向后移动3s")),
("构建一个名为主驾驶阅读模式", ("主驾驶阅读模式", "")),
("构建一个“阅读模式”,针对主驾", ("阅读模式", "")),
])
def test_explicit_names_and_actual_device_instructions_are_preserved(message, expected):
assert parse_mode_profile_create(message) == expected
@pytest.mark.parametrize("message", [
"不要为主驾构建一个阅读模式", "如何为主驾构建一个阅读模式?",
"为主驾构建一个阅读模式,但不要执行", "引用为主驾构建一个阅读模式",
])
def test_scope_grammar_does_not_bypass_authorization(message):
assert parse_mode_profile_create(message) is None
@pytest.mark.parametrize("name", ["后排阅读模式", "左后排阅读模式", "第三排阅读模式", "未知座位阅读模式"])
def test_unsupported_seats_never_alias_to_front_seat_templates(name):
assert mode_creation_source_name(name) == name
assert SceneTemplateRegistry(ROOT / "config" / "scene_templates").resolve_exact_name(name) is None
@pytest.mark.parametrize(("name", "source", "template_id"), [
("主驾座椅舒适模式", "座椅舒适模式", "seat-comfort"),
("主驾和副驾座椅舒适模式", "座椅舒适模式", "seat-comfort"),
("主副驾座椅动态模式", "座椅动态模式", "seat-dynamic"),
("副驾座椅的动态模式", "座椅的动态模式", "seat-dynamic"),
])
def test_seat_word_is_preserved_when_it_is_the_template_subject(name, source, template_id):
assert mode_creation_source_name(name) == source
template = SceneTemplateRegistry(ROOT / "config" / "scene_templates").resolve_exact_name(name)
assert template is not None and template.id == template_id
def test_catalog_seed_fallback_is_exact_and_preserves_explicit_match_priority():
template = SceneTemplateRegistry(ROOT / "config" / "scene_templates").get_template("reading").public_dict(include_plan=True)
template["assistant_callable"] = True
catalog = {
"source_status": {"scene_templates": "ready", "modes": "ready"},
"scene_templates": [template],
"modes": [{"id": "office", "name": "办公模式"}],
}
assert _catalog_exact_scene_template_plan(catalog, "主驾阅读模式") == template["plan"]
assert _catalog_exact_scene_template_plan(catalog, "后排阅读模式") is None
assert _catalog_exact_mode_match_count(catalog, "主驾办公模式") == 1
catalog["modes"].extend([
{"id": "driver-office-a", "name": "主驾办公模式"},
{"id": "driver-office-b", "name": "主驾办公模式"},
])
assert _catalog_exact_mode_match_count(catalog, "主驾办公模式") == 2
@pytest.mark.parametrize(("message", "expected"), [
("构建一个神游模式", {"passenger"}),
("构建一个主驾神游模式", {"driver", "front_left"}),
("构建一个主副驾神游模式", {"driver", "front_left", "passenger"}),
("构建一个神游模式,主驾空调26度", {"passenger"}),
("构建一个后排神游模式", set()),
])
def test_model_context_scopes_only_seat_capabilities_and_does_not_mutate_source(message, expected):
context = {
"composition_revision": "revision", "vehicle": {"id": "denza_n7"},
"capabilities": [
{"id": "seat.driver", "domain": "seat", "target_scopes": ["driver", "front_left"]},
{"id": "seat.passenger", "domain": "seat", "target_scopes": ["passenger"]},
{"id": "climate.zone", "domain": "climate", "target_scopes": ["driver", "passenger"]},
{"id": "roof.shade", "domain": "roof", "target_scopes": ["vehicle"]},
],
}
original = deepcopy(context)
projected = scene_context_for_mode_creation(context, message)
scopes = {scope for capability in projected["capabilities"] if capability["domain"] == "seat"
for scope in capability["target_scopes"]}
assert scopes == expected
assert [capability for capability in projected["capabilities"] if capability["domain"] != "seat"] == original["capabilities"][2:]
assert context == original
assert projected["seat_scope_policy"]["default_scope"] == "passenger"
def test_static_and_dynamic_prompts_use_one_selected_seat():
assert "defaults to the passenger seat only" in SCENE_COMPOSER_INSTRUCTIONS
assert "retarget a passenger capability to the driver" in SCENE_COMPOSER_INSTRUCTIONS
assert "selected seat scope in every phase" in SCHEDULE_COMPOSER_INSTRUCTIONS
def test_identical_explicit_mode_names_do_not_share_cached_plans_across_seats():
context = {"composition_revision": "revision", "vehicle": {"id": "denza_n7"}, "capabilities": []}
# Explicitly named drafts may intentionally keep the same display name even
# when the outer request selects a different seat.
driver = scene_context_for_mode_creation(context, "为主驾构建一个名为神游模式")
passenger = scene_context_for_mode_creation(context, "为副驾构建一个名为神游模式")
driver_key = CodexAssistantService._scene_cache_key(None, mode_name="神游模式", scene_context=driver)
passenger_key = CodexAssistantService._scene_cache_key(None, mode_name="神游模式", scene_context=passenger)
assert driver_key != passenger_key
@pytest.mark.parametrize("seat", [
"两个座椅", "两张座椅", "两座椅", "两座", "两个座位", "前排两座",
"前排两座椅", "前排两个座椅", "前排两张座椅", "双座", "双座椅",
])
@pytest.mark.parametrize("pattern", ["构建一个{}阅读模式", "为{}构建一个阅读模式", "构建一个阅读模式,针对{}"])
def test_all_two_seat_wordings_have_one_canonical_name(seat, pattern):
message = pattern.format(seat)
assert parse_mode_profile_create(message) == ("主副驾阅读模式", "")
assert mode_creation_source_name(f"{seat}阅读模式") == "阅读模式"
@pytest.mark.parametrize("message", [
"构建一个主驾阅读模式,针对副驾", "构建一个副驾阅读模式,针对主驾",
"为主驾构建一个副驾阅读模式", "为副驾构建一个主驾阅读模式",
"构建一个主副驾阅读模式,只针对主驾", "构建一个主驾阅读模式,针对双座",
"为主驾构建一个名为副驾阅读模式", "构建一个“副驾阅读模式”,针对主驾",
])
def test_conflicting_seat_selection_cannot_silently_become_a_two_seat_creation(message):
assert parse_mode_profile_create(message) is None
@pytest.mark.parametrize(("name", "source"), [
("副驾驶座椅阅读模式", "阅读模式"), ("主驾座椅的阅读模式", "阅读模式"),
("主驾座椅办公模式", "办公模式"), ("副驾座位的观影模式", "观影模式"),
("副驾座椅看书模式", "看书模式"), ("主驾座椅露营模式", "露营模式"),
("主驾座椅舒适模式", "座椅舒适模式"), ("副驾座椅动态模式", "座椅动态模式"),
("乘客座椅舒适模式", "座椅舒适模式"), ("两个座椅的动态模式", "座椅动态模式"),
("两张座椅舒适模式", "座椅舒适模式"), ("前排两座动态模式", "座椅动态模式"),
])
def test_seat_location_wrappers_do_not_hide_generic_scene_sources_or_actual_seat_topics(name, source):
assert mode_creation_source_name(name) == source
@pytest.mark.parametrize("message", [
"构建一个两个座椅的动态模式", "构建一个两张座椅的动态模式", "构建一个前排两座动态模式",
])
def test_double_seat_dynamic_topic_is_not_lost_with_the_scope_qualifier(message):
assert parse_mode_profile_create(message) == ("主副驾座椅动态模式", "")
+53 -2
View File
@@ -5,6 +5,8 @@ import shutil
import subprocess
from pathlib import Path
import pytest
from cognitive_agent.application.mode_profile import ModeProfileRegistry
ROOT = Path(__file__).resolve().parents[2]
@@ -44,8 +46,18 @@ def test_runtime_mode_catalog_exposes_safe_executable_action_details() -> None:
if mode["id"] == "office"
)
assert len(office["operations"]) == office["operation_count"] == 10
assert len(office["operations"]) == office["operation_count"] == 12
assert office["operations"][0] == {
"domain": "seat",
"capability": "seat.set_driver_ventilation_native",
"target_scope": "driver",
"operation": "set",
"parameters": {"state": "on", "level": 2},
}
assert next(
operation for operation in office["operations"]
if operation["capability"] == "seat.set_longitudinal_position"
) == {
"domain": "seat",
"capability": "seat.set_longitudinal_position",
"target_scope": "passenger",
@@ -241,7 +253,7 @@ def test_top_mode_action_formatter_uses_device_target_action_and_parameter_value
actions = json.loads(completed.stdout)
assert len(actions) == office["operation_count"]
assert actions[0] == {
assert next(action for action in actions if action["action"] == "调节座椅前后位置") == {
"device": "座椅",
"target": "副驾",
"action": "调节座椅前后位置",
@@ -309,6 +321,7 @@ const context = {
modeProfiles() { return [savedMode]; },
setModeRailSelection(mode) { calls.push(["select", mode?.id]); },
closeModeGallery(options) { calls.push(["close", options.restoreFocus]); },
hideModeActionPopover() {},
showToast(message) { calls.push(["toast", message]); },
executeSavedModeProfile(mode, options) {
calls.push(["execute", mode.id, options.command]);
@@ -438,6 +451,44 @@ console.log(JSON.stringify({ result: context.result, calls }));
}
@pytest.mark.parametrize(("phase", "input_mode"), (
("capturing", "manual"),
("capturing", "wake"),
("finalizing", "manual"),
("finalizing", "wake"),
("arming", "manual"),
))
def test_mode_click_during_dictation_does_not_create_stuck_running_state(
phase: str, input_mode: str,
) -> None:
app = (WEB / "app.js").read_text(encoding="utf-8")
start = app.index("function applyModeCommand")
end = app.index("\n\nfunction applyRuntimeTargetPolicy", start)
script = "const snippet = " + json.dumps(app[start:end]) + ";\n"
script += "const speech = " + json.dumps({"phase": phase, "inputMode": input_mode}) + ";\n"
script += r"""
const vm = require("node:vm");
const calls = [];
const context = {
window: {
CognitiveAgentAssistant: { isPending() { return false; } },
CognitiveAgentSpeech: speech,
},
showToast(message) { calls.push(message); },
};
vm.createContext(context);
vm.runInContext(snippet + '\nthis.result = applyModeCommand("进入办公模式", "office");', context);
console.log(JSON.stringify({ result: context.result, calls }));
"""
completed = subprocess.run(
[node_runtime(), "-e", script], check=True, capture_output=True, encoding="utf-8",
)
assert json.loads(completed.stdout) == {
"result": False,
"calls": ["请等待语音整理完成后再发送"],
}
def test_top_and_management_mode_cards_share_the_assistant_submission_entrypoint() -> None:
app = (WEB / "app.js").read_text(encoding="utf-8")
install_start = app.index('document.addEventListener("DOMContentLoaded"')
+86 -1
View File
@@ -11,6 +11,7 @@ from cognitive_agent.application.mode_scene_catalog import (
ModeSceneMqttBurstPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
@@ -153,7 +154,7 @@ def test_repository_presets_are_exported_without_internal_command_flows() -> Non
assert catalog["scene_count"] >= 13
assert len({scene["id"] for scene in catalog["scenes"]}) == catalog["scene_count"]
by_id = {scene["id"]: scene for scene in catalog["scenes"]}
assert by_id["office"]["operation_count"] == 10
assert by_id["office"]["operation_count"] == len(registry.get_profile("office", "denza_n7").operations)
assert by_id["rocking_horse"]["behavior"] == "cyclic"
assert by_id["rocking_horse"]["timeline"]["repeat"] == "until_stopped_or_timeout"
encoded = json.dumps(catalog, ensure_ascii=False)
@@ -403,3 +404,87 @@ def test_mode_scene_mqtt_burst_publishes_only_one_exact_card_three_times() -> No
assert publisher.snapshot()["state"] == "burst_complete"
finally:
publisher.close()
def _schedule() -> dict:
return ModeSchedule.from_dict({
"schema": "cognitive-agent-mode-schedule/v1",
"repeat_count": 2,
"phases": [
{"id": "warm", "name": "暖舱", "duration_ms": 1000, "operations": [
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
"operation": "set", "parameters": {"temperature_c": 28}},
]},
{"id": "settle", "name": "舒适", "duration_ms": 2000, "operations": [
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
"operation": "set", "parameters": {"temperature_c": 24}},
{"domain": "seat", "capability": "seat.set_backrest_recline", "target_scope": "passenger",
"operation": "set", "parameters": {"position_pct": 45}},
]},
],
}).to_dict()
def test_schedule_catalog_preserves_phases_but_never_exports_flat_executable_actions(tmp_path: Path) -> None:
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
schedule = _schedule()
profile = registry.save_mode(
"分阶段舒适模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
schedule=ModeSchedule.from_dict(schedule),
command_flow={"source_request_id": "req-private", "raw_can": "private-transport-bytes"},
)
catalog = build_mode_scene_catalog(registry, "denza_n7")
scene = next(item for item in catalog["scenes"] if item["id"] == profile.id)
assert scene["schedule"] == schedule
assert scene["behavior"] == "cyclic"
assert scene["operation_count"] == 3
assert scene["domains"] == ["climate", "seat"]
assert scene["actions"] == [], "legacy clients must not execute all timeline stages at once"
assert scene["execution"] == {
"kind": "server_schedule", "mode_id": profile.id,
"client_actions_supported": False, "requires_authentication": True,
}
assert "服务端时间轴" in scene["execution_notice"]
assert scene["preview_actions_scope"] == "single_cycle"
preview = scene["preview_actions"]
assert [action["phase_id"] for action in preview] == ["warm", "settle", "settle"]
assert [action["at_ms"] for action in preview] == [0, 1000, 1000]
assert [action["phase_duration_ms"] for action in preview] == [1000, 2000, 2000]
assert [action["celsius"] for action in preview if action["type"] == "climate_temperature"] == [28, 24]
assert preview[-1]["target_scope"] == "passenger"
assert preview[-1]["parameters"] == {"position_pct": 45}
assert ModeSchedule.from_dict(scene["schedule"]).total_duration_ms == 6000
encoded = json.dumps(catalog)
assert "source_request_id" not in encoded
assert "private-transport-bytes" not in encoded
assert "command_flow" not in encoded
def test_schedule_publisher_updates_after_save_reload_duration_change_and_delete(tmp_path: Path) -> None:
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
publisher = ModeSceneCatalogPublisher(tmp_path / "catalog.json", "denza_n7")
registry.add_change_listener(publisher.publish)
assert publisher.snapshot()["scene_count"] == 0
schedule = _schedule()
created = registry.save_mode(
"动态释压模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
schedule=ModeSchedule.from_dict(schedule),
)
first = publisher.snapshot()
assert first["scenes"][0]["schedule"] == schedule
registry.reload()
assert publisher.snapshot() == first
schedule["phases"][1]["duration_ms"] = 5000
registry.save_mode(
created.name, schedule["phases"][0]["operations"], vehicle_id="denza_n7",
mode_id=created.id, schedule=ModeSchedule.from_dict(schedule),
)
changed = publisher.snapshot()
assert changed["catalog_revision"] != first["catalog_revision"]
assert changed["scenes"][0]["profile_revision"] != first["scenes"][0]["profile_revision"]
assert changed["scenes"][0]["schedule"]["phases"][1]["duration_ms"] == 5000
assert changed["scenes"][0]["actions"] == []
assert json.loads(publisher.path.read_text(encoding="utf-8")) == changed
registry.delete_user_mode(created.id, "denza_n7")
assert publisher.snapshot()["scene_count"] == 0
assert publisher.snapshot()["scenes"] == []
+513
View File
@@ -0,0 +1,513 @@
from __future__ import annotations
from queue import Queue
from threading import Event, Lock, current_thread
from unittest.mock import patch
import pytest
from cognitive_agent.application.mode_schedule import (
MODE_SCHEDULE_SCHEMA,
ModeSchedule,
ModeSchedulePhase,
ModeScheduleRunner,
)
def operation(capability="climate.set_temperature", target="whole_vehicle", **parameters):
return {
"domain": capability.split(".")[0], "capability": capability,
"target_scope": target, "operation": "set",
"parameters": parameters or {"temperature_c": 24}, "reason": "舒适阶段",
}
def document(*, repeat=1, duration=1000):
return {
"schema": MODE_SCHEDULE_SCHEMA, "repeat_count": repeat,
"phases": [
{"id": "warmup", "name": "预热", "duration_ms": duration,
"operations": [operation(temperature_c=28)]},
{"id": "comfort", "name": "舒适", "duration_ms": duration,
"operations": [operation(temperature_c=24)]},
],
}
def test_round_trip_has_independent_parameters_and_counts():
source = document(repeat=3, duration=1500)
schedule = ModeSchedule.from_dict(source)
assert schedule.to_dict() == source
assert schedule.total_duration_ms == 9000
assert schedule.operation_count == 2
source["phases"][0]["operations"][0]["parameters"]["temperature_c"] = 10
returned = schedule.to_dict()
returned["phases"][0]["operations"][0]["parameters"]["temperature_c"] = 11
assert schedule.phases[0].operations[0]["parameters"]["temperature_c"] == 28
@pytest.mark.parametrize("value", [None, [], "schedule", 1, True])
def test_schedule_root_must_be_object(value):
with pytest.raises(ValueError):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("field", ["schema", "repeat_count", "phases"])
def test_schedule_requires_all_contract_fields(field):
value = document()
del value[field]
with pytest.raises(ValueError, match="missing"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("value", [0, 21, -1, 1.0, "2", True, False, None, float("nan")])
def test_repeat_count_is_a_bounded_non_boolean_integer(value):
with pytest.raises(ValueError, match="repeat_count"):
ModeSchedule.from_dict(document(repeat=value))
@pytest.mark.parametrize("value", [0, 999, 900001, 1000.0, "1000", True, None, float("inf")])
def test_phase_duration_is_a_bounded_non_boolean_integer(value):
with pytest.raises(ValueError, match="duration_ms"):
ModeSchedule.from_dict(document(duration=value))
@pytest.mark.parametrize("count", [0, 1, 9])
def test_phase_count_bounds(count):
value = document()
value["phases"] = [dict(value["phases"][0], id=f"phase-{index}") for index in range(count)]
with pytest.raises(ValueError, match="2 to 8"):
ModeSchedule.from_dict(value)
def test_total_duration_includes_repeats_and_enforces_one_hour():
assert ModeSchedule.from_dict(document(repeat=2, duration=900000)).total_duration_ms == 3600000
with pytest.raises(ValueError, match="60 minutes"):
ModeSchedule.from_dict(document(repeat=3, duration=900000))
@pytest.mark.parametrize("path", [(), ("phases", 0), ("phases", 0, "operations", 0)])
def test_unknown_fields_rejected_at_every_contract_level(path):
value = document()
target = value
for key in path:
target = target[key]
target["untrusted_instruction"] = "bypass validation"
with pytest.raises(ValueError, match="unknown"):
ModeSchedule.from_dict(value)
def test_non_string_object_keys_are_rejected():
value = document()
value[1] = "unknown"
with pytest.raises(ValueError, match="string keys"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("schema", ["v2", "", None, 1, True])
def test_schema_is_exact(schema):
value = document()
value["schema"] = schema
with pytest.raises(ValueError, match="schema"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("field", ["id", "name", "duration_ms", "operations"])
def test_phase_requires_all_contract_fields(field):
value = document()
del value["phases"][0][field]
with pytest.raises(ValueError, match="missing"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("phase_id", ["", "Bad ID", "hello.world", "座椅", "x" * 65, 5])
def test_phase_ids_are_bounded_identifiers(phase_id):
value = document()
value["phases"][0]["id"] = phase_id
with pytest.raises(ValueError):
ModeSchedule.from_dict(value)
def test_duplicate_phase_ids_rejected():
value = document()
value["phases"][1]["id"] = value["phases"][0]["id"]
with pytest.raises(ValueError, match="ids must be unique"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("value", [[], [operation()] * 13, {}, "operations"])
def test_operation_count_and_array_contract(value):
source = document()
source["phases"][0]["operations"] = value
with pytest.raises(ValueError):
ModeSchedule.from_dict(source)
@pytest.mark.parametrize("capability", [
"seat.set_driver_axis_native", "seat.set_driver_position_native", "seat.move",
"seat.apply_dynamic_mode", "door.set_vehicle_lock", "window.set_motion", "custom.anything",
])
def test_only_stable_scene_composable_capabilities_allowed(capability):
value = document()
value["phases"][0]["operations"] = [operation(capability)]
with pytest.raises(ValueError, match="stable"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("kind", ["toggle", "increase", "decrease", "activate", "apply", None, True])
def test_relative_or_non_set_operation_types_rejected(kind):
value = document()
value["phases"][0]["operations"][0]["operation"] = kind
with pytest.raises(ValueError, match="absolute set"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("name", ["duration_ms", "duration", "seconds", "direction", "delta", "relative", "axis", "toggle"])
def test_phase_holds_cannot_be_smuggled_as_axis_motion(name):
value = document()
value["phases"][0]["operations"][0]["parameters"][name] = 1000
with pytest.raises(ValueError, match="relative, motion or timing"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("value", [float("nan"), float("inf"), -float("inf"), {}, [], None])
def test_non_json_or_non_scalar_parameters_rejected(value):
source = document()
source["phases"][0]["operations"][0]["parameters"]["temperature_c"] = value
with pytest.raises(ValueError):
ModeSchedule.from_dict(source)
@pytest.mark.parametrize("value", ["toggle", "relative", "increase", "decrease"])
def test_relative_parameter_values_rejected(value):
source = document()
source["phases"][0]["operations"][0]["parameters"]["state"] = value
with pytest.raises(ValueError, match="relative state"):
ModeSchedule.from_dict(source)
def test_domain_must_match_capability():
value = document()
value["phases"][0]["operations"][0]["domain"] = "seat"
with pytest.raises(ValueError, match="stable"):
ModeSchedule.from_dict(value)
@pytest.mark.parametrize("target", ["passenger", "front_right"])
def test_duplicate_physical_seat_target_aliases_rejected(target):
value = document()
value["phases"][0]["operations"] = [
operation("seat.set_ventilation", "passenger", state="on", level=1),
operation("seat.set_ventilation", target, state="off"),
]
with pytest.raises(ValueError, match="duplicate"):
ModeSchedule.from_dict(value)
def test_driver_and_passenger_comfort_are_valid_independent_operations():
value = document()
value["phases"][0]["operations"] = [
operation("seat.set_driver_ventilation_native", "driver", state="on", level=2),
operation("seat.set_driver_massage_native", "driver", action="mode", mode="relax"),
operation("seat.set_ventilation", "passenger", state="on", level=1),
operation("seat.set_backrest_recline", "passenger", position_pct=40),
]
assert ModeSchedule.from_dict(value).operation_count == 5
def test_direct_dataclass_construction_cannot_bypass_duration_or_repeat_limits():
with pytest.raises(ValueError, match="duration_ms"):
ModeSchedulePhase("a", "a", True, (operation(),))
phase = ModeSchedulePhase.from_dict(document()["phases"][0])
second = ModeSchedulePhase.from_dict(document()["phases"][1])
with pytest.raises(ValueError, match="repeat_count"):
ModeSchedule((phase, second), True)
def test_simulation_has_no_thread_or_real_wait_and_preserves_repetition_order():
schedule = ModeSchedule.from_dict(document(repeat=3, duration=2000))
calls = []
caller = current_thread()
def execute(phase):
assert current_thread() is caller
calls.append(phase.id)
return {"success": True}
with (
patch("cognitive_agent.application.mode_schedule.Thread", side_effect=AssertionError("thread")),
patch("cognitive_agent.application.mode_schedule.Event.wait", side_effect=AssertionError("wait")),
):
result = ModeScheduleRunner.simulate(schedule, execute)
assert result["status"] == "completed"
assert result["completed"] is True
assert result["execution_mode"] == "dry_run"
assert result["virtual_elapsed_ms"] == 12000
assert result["executed_phases"] == 6
assert result["phase_index"] == 1
assert result["repeat_index"] == 2
assert calls == ["warmup", "comfort"] * 3
assert [item["virtual_start_ms"] for item in result["phase_results"]] == list(range(0, 12000, 2000))
@pytest.mark.parametrize("failed_result", [
None, [], {}, {"success": False}, {"status": "failed"}, {"status": "submitted_unverified"},
{"status": "error", "success": True}, {"status": "succeeded", "success": False},
{"success": True, "error": "rejected by safety"},
{"status": "pending"}, {"success": 1},
])
def test_simulation_fails_closed_on_unconfirmed_or_failed_execution(failed_result):
calls = []
def execute(phase):
calls.append(phase.id)
return failed_result
result = ModeScheduleRunner.simulate(ModeSchedule.from_dict(document(repeat=3)), execute)
assert result["status"] == "failed"
assert result["completed"] is False
assert result["error"]
assert result["virtual_elapsed_ms"] == 0
assert calls == ["warmup"]
def test_simulation_stops_on_second_phase_exception_and_reports_prior_hold():
def execute(phase):
if phase.id == "comfort":
raise RuntimeError("runtime safety rejection")
return {"status": "succeeded"}
result = ModeScheduleRunner.simulate(ModeSchedule.from_dict(document(repeat=3)), execute)
assert result["completed"] is False
assert result["error"] == "RuntimeError: runtime safety rejection"
assert result["virtual_elapsed_ms"] == 1000
assert result["executed_phases"] == 1
def test_executor_mutations_do_not_change_next_repetition_or_source():
values = []
schedule = ModeSchedule.from_dict(document(repeat=2))
def execute(phase):
values.append(phase.operations[0]["parameters"]["temperature_c"])
phase.operations[0]["parameters"]["temperature_c"] = 999
return {"status": "ok"}
assert ModeScheduleRunner.simulate(schedule, execute)["completed"]
assert values == [28, 24, 28, 24]
assert schedule.phases[0].operations[0]["parameters"]["temperature_c"] == 28
class ManualClock:
def __init__(self):
self.value = 0.0
self.holds = Queue()
def __call__(self):
return self.value
def wait(self, cancelled, seconds):
release = Event()
self.holds.put((seconds, release))
while not cancelled.is_set():
if release.wait(0.01):
self.value += seconds
return False
return True
def release(self, seconds=1.0):
expected, release = self.holds.get(timeout=2)
assert expected == seconds
release.set()
def test_runner_holds_without_repeated_dispatch_and_finishes_finite_repeats():
clock = ManualClock()
calls = Queue()
runner = ModeScheduleRunner(clock=clock, wait=clock.wait)
schedule = ModeSchedule.from_dict(document(repeat=2))
try:
runner.start("car", schedule, lambda phase: calls.put(phase.id) or {"success": True}, session_id="a")
for expected in ["warmup", "comfort", "warmup", "comfort"]:
assert calls.get(timeout=2) == expected
assert calls.empty()
state = runner.status("car")
assert state["status"] == "running"
assert state["completed"] is False
assert state["session_id"] == "a"
clock.release()
# Joining a bounded, virtual-time worker is deterministic and does not
# cancel it as close() would do while it leaves its final hold.
runner._runs["car"].thread.join(2)
assert runner.status("car")["status"] == "completed"
assert runner.status("car")["elapsed_ms"] == 4000
assert runner.status("car")["executed_phases"] == 4
finally:
assert runner.close()
def test_cancel_interrupts_real_hold_before_next_phase():
started = Event()
calls = []
runner = ModeScheduleRunner()
def execute(phase):
calls.append(phase.id)
started.set()
return {"status": "success"}
runner.start("car", ModeSchedule.from_dict(document(duration=900000)), execute)
assert started.wait(2)
assert runner.cancel("car")["status"] == "cancelled"
assert runner.close(timeout=1)
assert calls == ["warmup"]
assert runner.status("car")["completed"] is False
def test_wrong_session_cannot_cancel_active_timeline():
started = Event()
runner = ModeScheduleRunner()
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: started.set() or {"success": True}, session_id="a")
assert started.wait(2)
assert runner.cancel("car", session_id="b")["status"] == "running"
assert runner.cancel("car", session_id="a")["status"] == "cancelled"
assert runner.close()
def test_replace_waits_for_inflight_callback_and_never_overlaps_same_car():
entered = Event()
release = Event()
second_entered = Event()
calls = []
inside = 0
guard = Lock()
runner = ModeScheduleRunner()
def first(phase):
nonlocal inside
with guard:
inside += 1
entered.set()
assert release.wait(2)
calls.append("old")
with guard:
inside -= 1
return {"success": True}
def second(phase):
with guard:
assert inside == 0
calls.append("new")
second_entered.set()
return {"success": True}
try:
old = runner.start("car", ModeSchedule.from_dict(document()), first, session_id="old")
old_run = runner._runs["car"]
assert entered.wait(2)
new = runner.start("car", ModeSchedule.from_dict(document()), second, session_id="new")
assert old["run_id"] != new["run_id"]
assert old_run.status == "cancelled"
assert old_run.cancel_reason == "replaced"
assert not second_entered.is_set()
release.set()
assert second_entered.wait(2)
assert calls == ["old", "new"]
finally:
release.set()
assert runner.close()
def test_different_vehicles_do_not_share_a_global_execution_lock():
first_entered, second_entered, release = Event(), Event(), Event()
runner = ModeScheduleRunner()
def first(phase):
first_entered.set()
assert release.wait(2)
return {"success": True}
try:
runner.start("car-a", ModeSchedule.from_dict(document()), first)
assert first_entered.wait(2)
runner.start("car-b", ModeSchedule.from_dict(document()), lambda phase: second_entered.set() or {"success": True})
assert second_entered.wait(2)
finally:
release.set()
assert runner.close()
@pytest.mark.parametrize("failure", ["return", "exception"])
def test_runner_stops_later_phases_and_repeats_after_failure(failure):
calls = []
runner = ModeScheduleRunner(wait=lambda event, duration: False)
def execute(phase):
calls.append(phase.id)
if phase.id == "comfort":
if failure == "exception":
raise ValueError("bad runtime capability")
return {"status": "rejected", "error": "bad runtime capability"}
return {"status": "ok"}
runner.start("car", ModeSchedule.from_dict(document(repeat=3)), execute)
runner._runs["car"].thread.join(2)
assert runner.status("car")["status"] == "failed"
assert "bad runtime capability" in runner.status("car")["error"]
assert runner.status("car")["executed_phases"] == 1
assert calls == ["warmup", "comfort"]
assert runner.close()
def test_close_prevents_start_and_reports_uninterruptible_callback_honestly():
entered, release = Event(), Event()
runner = ModeScheduleRunner()
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: entered.set() or release.wait(2) or {"success": True})
assert entered.wait(2)
assert runner.close(timeout=0) is False
assert runner.status("car")["status"] == "cancelled"
assert runner.status("car")["cancel_reason"] == "closed"
with pytest.raises(RuntimeError, match="closed"):
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: {"success": True})
release.set()
assert runner.close()
def test_idle_and_terminal_cancellation_are_idempotent():
runner = ModeScheduleRunner(wait=lambda event, duration: False)
assert runner.status("unknown")["status"] == "idle"
assert runner.cancel("unknown")["status"] == "idle"
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: {"success": True})
runner._runs["car"].thread.join(2)
assert runner.cancel("car")["status"] == "completed"
assert runner.close()
assert runner.close()
def test_start_revalidates_caller_mutation_before_accepting_schedule():
runner = ModeScheduleRunner()
schedule = ModeSchedule.from_dict(document())
schedule.phases[0].operations[0]["operation"] = "toggle"
with pytest.raises(ValueError, match="absolute set"):
runner.start("car", schedule, lambda phase: {"success": True})
assert runner.status("car")["status"] == "idle"
assert runner.close()
def test_failed_thread_start_is_not_left_running_and_close_remains_safe():
runner = ModeScheduleRunner()
with (
patch("cognitive_agent.application.mode_schedule.Thread.start", side_effect=RuntimeError("thread unavailable")),
pytest.raises(RuntimeError, match="thread unavailable"),
):
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: {"success": True})
assert runner.status("car")["status"] == "failed"
assert runner.close()
def test_injected_cancelled_wait_cannot_leave_stale_running_status():
runner = ModeScheduleRunner(wait=lambda event, duration: True)
runner.start("car", ModeSchedule.from_dict(document()), lambda phase: {"success": True})
runner._runs["car"].thread.join(2)
assert runner.status("car")["status"] == "cancelled"
assert runner.close()
@@ -46,6 +46,26 @@ def test_saved_mode_update_parser_accepts_natural_variants(
assert parse_mode_profile_update(message) == expected
@pytest.mark.parametrize("change", (
"第二阶段保持30秒", "第二阶段持续16分钟", "循环21次", "重复两次",
"重复1.5次", "持续30秒", "全部阶段持续2分钟",
))
def test_schedule_update_predicates_stay_on_control_plane_even_when_bounds_are_invalid(change):
assert parse_mode_profile_update(f"把上车预热模式的{change}") == ("上车预热模式", change)
@pytest.mark.parametrize("message", (
"把上车预热模式的第二阶段保持30秒合适吗?",
"不要把上车预热模式的第二阶段保持30秒",
"上车预热模式的第二阶段不要保持30秒",
"把上车预热模式的重复两次,但是不要保存",
"解释把上车预热模式的重复两次",
"创建一个上车预热模式,第二阶段保持30秒",
))
def test_schedule_update_predicates_do_not_authorize_queries_negation_or_creation(message):
assert parse_mode_profile_update(message) is None
@pytest.mark.parametrize(
"message",
(
@@ -36,6 +36,9 @@ EXPECTED_SCENE_CAPABILITIES = {
"seat.set_legrest_height",
"seat.set_longitudinal_position",
"seat.set_ventilation",
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"window.set_position",
}
@@ -19,6 +19,57 @@ from cognitive_agent.application.codex_assistant import (
CONTROL_TERMS = ("空调", "座椅", "车窗", "遮阳帘", "氛围灯", "阅读灯", "出风口")
@pytest.mark.parametrize(("message", "mode_name"), [
("构建一个观影模式", "观影模式"),
("为主驾构建一个观影模式", "主驾观影模式"),
])
def test_local_first_callbacks_do_not_intercept_scoped_mode_draft_creation(message, mode_name):
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
events = []
control_calls = []
def execute_control(instruction, _progress, plan):
control_calls.append((instruction, plan))
return _preview_result(mode_name)
result = assistant.stream(
message, conversation_id="conv-merged-scene-router", principal="test",
history=[], emit=events.append, execute_control=execute_control,
preview_control=lambda *_args: pytest.fail("mode previews must not partially execute via local-first controls"),
supplement_control=lambda *_args: pytest.fail("mode previews must not become unresolved control supplements"),
control_terms=CONTROL_TERMS,
capability_snapshot=_catalog_with_templates(_scene_template()),
scene_capability_context=_scene_context(),
)
assert result["success"] is True
assert result["route"] == "scene_template"
assert [instruction for instruction, _ in control_calls] == [message]
assert not client.structured_calls and not client.legacy_calls
def test_local_first_callbacks_preserve_schedule_stop_route_and_answer():
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
events = []
result = assistant.stream(
"停止动态模式", conversation_id="conv-merged-schedule-stop", principal="test",
history=[], emit=events.append,
execute_control=lambda instruction, _progress: {
"request_id": "req-stop-merged", "utterance": instruction,
"status": "succeeded", "execution_mode": "dry_run",
"orchestration_route": "mode_schedule_stop",
"schedule_execution": {"status": "idle"},
},
preview_control=lambda *_args: pytest.fail("schedule stop must retain its scheduler route"),
supplement_control=lambda *_args: pytest.fail("schedule stop is not an unresolved control"),
control_terms=CONTROL_TERMS,
)
assert result["success"] is True
assert "已模拟停止动态模式" in result["answer"]
assert not client.structured_calls and not client.legacy_calls
def _structured_plan(
*,
temperature: float = 24,
@@ -793,6 +844,11 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
]
call = client.structured_calls[0]
expected_context = _scene_context()
expected_context["seat_scope_policy"] = {
"selected_scopes": ["passenger"],
"default_scope": "passenger",
"applies_to": "seat capabilities in every phase",
}
expected_context["natural_language_routing"] = {
"original_utterance": "构建一个星夜模式",
"parsed_clauses": [],
@@ -15,6 +15,8 @@ ROOT = Path(__file__).resolve().parents[2]
(
("帮我构建一个适合看书的功能模式", ("看书模式", "")),
("构建一个适合放松的功能模式", ("放松模式", "")),
("构建一个座椅舒适的功能模式", ("座椅舒适模式", "")),
("构建一个让人坐着放松的功能模式", ("坐着放松模式", "")),
("请帮我创建适合阅读的模式吧", ("阅读模式", "")),
("我想生成一个用来读书的模式并预览", ("读书模式", "")),
("请生成用于放松的模式,先看看", ("放松模式", "")),
@@ -67,6 +69,8 @@ def test_explicit_names_are_never_rewritten(
("帮我构建一个适合看书的功能模式", "reading"),
("请创建一个用于阅读的功能模式", "reading"),
("构建一个适合放松的功能模式", "relaxation"),
("构建一个座椅舒适的功能模式", "seat-comfort"),
("构建一个让人坐着放松的功能模式", "relaxation"),
("我想创建一套用来舒缓的模式", "relaxation"),
("帮我构建一个适合看电影的功能模式", "cinema"),
),
@@ -0,0 +1,114 @@
from __future__ import annotations
from copy import deepcopy
import pytest
from jsonschema import Draft202012Validator
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.mode_profile_intent import parse_mode_profile_create
from cognitive_agent.application.scene_composer import build_scene_output_schema
from cognitive_agent.application.seat_service import SeatDemoService
@pytest.fixture(scope="module")
def catalog():
service = SeatDemoService()
try:
yield AssistantCapabilityCatalog(service)
finally:
service.close()
@pytest.mark.parametrize("message", (
"构建一个座椅舒适的功能模式",
"构建一个让人坐着放松的功能模式",
"可以帮我创建一个适合座椅放松的功能模式吗?",
"构建一个主驾按摩的功能模式",
"创建一个空调舒适的功能模式",
"构建一个适合看书的功能模式,阅读灯打开",
))
def test_mode_creation_is_not_claimed_by_capability_fast_answer(catalog, message):
assert parse_mode_profile_create(message) is not None
assert catalog.fast_answer(message) is None
@pytest.mark.parametrize("message", (
"支持主驾座椅按摩吗?",
"主驾与副驾座椅有哪些功能?",
"座椅通风支持哪些档位?",
"副驾座椅支持按摩吗?",
))
def test_genuine_seat_questions_remain_read_only_capability_answers(catalog, message):
assert parse_mode_profile_create(message) is None
assert catalog.fast_answer(message) is not None
@pytest.mark.parametrize("message", (
"不要构建座椅舒适模式",
"构建一个座椅舒适模式,但不要执行",
"取消创建坐着放松模式",
"如何构建一个座椅舒适模式?",
"解释构建一个座椅舒适的功能模式这句话",
))
def test_negations_and_read_only_seat_mode_requests_never_authorize_creation(message):
assert parse_mode_profile_create(message) is None
def _response(capability, target, parameters):
return {
"requests": [{"capability": capability, "target_scope": target, "parameters": parameters}],
"ambiguities": [],
"confidence": 0.95,
}
@pytest.mark.parametrize(("capability", "target", "parameters", "valid"), (
("seat.set_driver_ventilation_native", "driver", {"state": "on", "level": 2}, True),
("seat.set_driver_heating_native", "driver", {"state": "on", "level": 1}, True),
("seat.set_driver_heating_native", "front_left", {"state": "off"}, True),
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax"}, True),
("seat.set_driver_massage_native", "driver", {"action": "strength", "strength": 1}, True),
("seat.set_driver_massage_native", "driver", {"action": "off"}, True),
("seat.set_driver_massage_native", "passenger", {"action": "mode", "mode": "relax"}, False),
("seat.set_driver_massage_native", "driver", {"action": "mode"}, False),
("seat.set_driver_massage_native", "driver", {"action": "mode", "strength": 1}, False),
("seat.set_driver_massage_native", "driver", {"action": "off", "mode": "relax"}, False),
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax", "strength": 1}, False),
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "unknown"}, False),
("seat.set_driver_heating_native", "passenger", {"state": "on", "level": 2}, False),
("seat.set_driver_ventilation_native", "driver", {"state": "on", "level": 3}, False),
("seat.set_driver_axis_native", "driver", {"axis": "backrest", "direction": "backward", "duration_ms": 500}, False),
("seat.set_driver_position_native", "driver", {"slot": 1}, False),
("seat.set_backrest_recline", "driver", {"position_pct": 45}, False),
))
def test_scene_schema_respects_real_driver_and_passenger_capabilities(
catalog, capability, target, parameters, valid,
):
context = catalog.scene_composition_context()
validator = Draft202012Validator(build_scene_output_schema(context))
assert validator.is_valid(_response(capability, target, parameters)) is valid
def test_scene_context_removes_driver_capabilities_when_runtime_marks_them_unavailable(catalog):
snapshot = deepcopy(catalog.snapshot())
for capability in snapshot["capabilities"]:
if capability["id"].startswith("seat.set_driver_"):
capability["assistant_callable"] = False
context = catalog.scene_composition_context(snapshot)
assert not any(item["id"].startswith("seat.set_driver_") for item in context["capabilities"])
validator = Draft202012Validator(build_scene_output_schema(context))
assert not validator.is_valid(_response(
"seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax"},
))
def test_seat_comfort_detail_answer_uses_seat_specific_natural_language(catalog):
answer = catalog.fast_answer("座椅舒适模式会控制哪些设备?")
assert answer is not None
assert "主驾座椅通风开启低档" in answer.answer
assert "主驾座椅按摩设为放松模式" in answer.answer
assert "副驾座椅通风" in answer.answer
assert "副驾座椅靠背" in answer.answer
assert "action=" not in answer.answer
assert "主驾主驾" not in answer.answer