feat: add seat-scoped static and dynamic assistant modes

This commit is contained in:
vee
2026-09-12 14:56:30 +08:00
parent d09b5967f8
commit d8dad22ec0
60 changed files with 5847 additions and 149 deletions
+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; });
+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()
+10 -10
View File
@@ -621,7 +621,7 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-131", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-132", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
@@ -656,7 +656,7 @@ operations:
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-27", timeout=3
f"{self.base_url}/assistant-chat.js?v=codex-assistant-28", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -697,7 +697,7 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=dual-seat-131"', html)
self.assertIn('src="/app.js?v=dual-seat-132"', html)
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
@@ -718,8 +718,8 @@ operations:
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-27"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-27"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-28"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-28"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertNotIn('"admin"', console_script)
@@ -992,14 +992,14 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v278"', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v279"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-66', service_worker)
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-27', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-27', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-28', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-28', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-131"', service_worker)
self.assertIn('"/app.js?v=dual-seat-132"', service_worker)
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
@@ -1456,7 +1456,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-131", timeout=3
f"{self.base_url}/app.js?v=dual-seat-132", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -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
}
@@ -87,7 +87,10 @@ def test_explicit_scene_seat_scope_rejects_cross_seat_plan_atomically(scene_serv
assert result.get("command_plans") == {}
intent = result.get("intent_ir") or {}
assert (intent.get("spec") or {}).get("actions", []) == []
assert any("座位范围" in item["message"] for item in result["validation"])
assert any(
"座位范围" in item["message"] or item.get("code") == "MODE_SEAT_SCOPE_UNAVAILABLE"
for item in result["validation"]
)
@pytest.mark.parametrize(("message", "scopes"), (
@@ -96,9 +99,9 @@ def test_explicit_scene_seat_scope_rejects_cross_seat_plan_atomically(scene_serv
("创建一个仅主驾舒适模式", ("driver",)),
("构建一个主驾和副驾座椅舒适模式", ("driver", "passenger")),
("构建一个主副驾座椅舒适模式", ("driver", "passenger")),
("构建一个云端座椅舒适模式", ("driver", "passenger")),
("构建一个副驾空调与座椅舒适模式", ("driver", "passenger")),
("构建一个座椅舒适与副驾阅读灯模式", ("driver", "passenger")),
("构建一个云端座椅舒适模式", ("passenger",)),
("构建一个副驾空调与座椅舒适模式", ("passenger",)),
("构建一个座椅舒适与副驾阅读灯模式", ("passenger",)),
("构建一个主驾座椅与副驾空调舒适模式", ("driver",)),
("构建一个主驾阅读灯与副驾座椅舒适模式", ("passenger",)),
))
@@ -120,7 +123,6 @@ def test_scene_scope_preserves_supported_targets_without_borrowing_other_device_
@pytest.mark.parametrize("message", (
"构建一个主驾舒适模式,副驾不要动",
"构建一个主驾舒适模式,不要动副驾",
"构建一个舒展模式,座椅靠背调整到45%",
))
def test_explicit_atomic_requirements_do_not_receive_scene_scope_relaxation(
scene_service, message,
@@ -131,6 +133,19 @@ def test_explicit_atomic_requirements_do_not_receive_scene_scope_relaxation(
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"
+36 -14
View File
@@ -144,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) == 13
assert len(templates) == 15
assert all(template["assistant_callable"] is True for template in templates)
with patch.object(
@@ -169,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"]),
@@ -187,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 = {
(
@@ -311,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"))
@@ -353,7 +375,7 @@ def test_explicit_settings_on_a_purpose_scene_bypass_template_defaults(
("构建一个长途舒适模式", "长途舒适模式", "long-trip-comfort"),
),
)
def test_seating_comfort_requests_preview_both_seats_with_no_model_or_hardware_call(
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,
@@ -380,16 +402,13 @@ def test_seating_comfort_requests_preview_both_seats_with_no_model_or_hardware_c
for (capability, target), parameters in _action_map(result).items()
if capability.startswith("seat.")
}
assert {target for _capability, target in seat_actions} == {"driver", "passenger"}
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_driver_ventilation_native", "driver"] == {
"state": "on", "level": 2,
}
assert seat_actions["seat.set_ventilation", "passenger"] == {
"state": "on", "level": 1,
}
@@ -422,7 +441,7 @@ def test_unregistered_seat_scene_keeps_both_explicit_targets_through_native_merg
side_effect=AssertionError("scene composition reached hardware execution"),
):
result = service.preview_native_can(
f"构建一个{name}", provider="local", conversation_id="conv-seat-generated",
f"构建一个主驾和副驾的{name}", provider="local", conversation_id="conv-seat-generated",
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
)
assert result["status"] == "succeeded"
@@ -437,7 +456,7 @@ def test_unregistered_seat_scene_keeps_both_explicit_targets_through_native_merg
assert not list(user_root.rglob("*.yaml"))
def test_explicit_unscoped_seat_instruction_keeps_target_ambiguity_guard(
def test_unscoped_seat_instruction_inside_mode_creation_defaults_to_passenger(
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
) -> None:
service, _catalog, _user_root = hybrid_service
@@ -446,8 +465,11 @@ def test_explicit_unscoped_seat_instruction_keeps_target_ambiguity_guard(
conversation_id="conv-seat-explicit-target", execution_mode="hardware",
vehicle_id="denza_n7",
)
assert result["status"] != "succeeded"
assert result.get("mode_profile_draft") is None
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
@@ -552,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) == 13
assert len(templates) == 15
assert {item["name"] for item in templates} >= {
"观影模式",
"露营模式",
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
assert 'aria-label="AI助手对话"' in html
assert 'placeholder="向 AI助手提问或交代工作"' in html
assert 'aria-label="发送消息"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-27"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-27"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-28"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-28"' in html
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-27"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-27"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-28"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-28"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
):
assert asset in html
assert f'"{asset}"' in service_worker
assert html.index("/assistant-chat.js?v=codex-assistant-27") < html.index(
assert html.index("/assistant-chat.js?v=codex-assistant-28") < html.index(
"/energy-monitor.js?v=energy-monitor-4"
)
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
+1 -1
View File
@@ -47,7 +47,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
):
assert asset in html
assert f'"{asset}"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
@@ -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) == ("主副驾座椅动态模式", "")
+12 -2
View File
@@ -46,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",
@@ -237,7 +247,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": "调节座椅前后位置",
+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",
(
+7 -1
View File
@@ -792,7 +792,13 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
"roof.set_shade_position",
]
call = client.structured_calls[0]
assert call["application_context"] == _scene_context()
expected_context = _scene_context()
expected_context["seat_scope_policy"] = {
"selected_scopes": ["passenger"],
"default_scope": "passenger",
"applies_to": "seat capabilities in every phase",
}
assert call["application_context"] == expected_context
assert call["output_schema"]["type"] == "object"
assert call["output_schema"]["additionalProperties"] is False
assert "requests" in call["output_schema"]["required"]