feat: group Cloud Workbench trusted records

Merge PR #146 after Code Queue rebase to main a444387 and validation. This improves the deployed workbench UI surface for trusted records with explicit DEV-LIVE/SOURCE/DRY-RUN/BLOCKED labels. It does not claim M3 DEV-LIVE and does not change PROD or secrets.
This commit is contained in:
Lyon
2026-05-23 00:24:27 +08:00
committed by GitHub
parent a44438779f
commit 88e647e380
6 changed files with 499 additions and 35 deletions
+27 -2
View File
@@ -234,7 +234,28 @@ function summarizeEvidence(record) {
sha256: record.sha256,
serviceId: record.serviceId,
environment: record.environment,
dryRun: record.metadata?.dryRun === true
traceId: record.metadata?.traceId ?? null,
dryRun: record.metadata?.dryRun === true,
createdAt: record.createdAt ?? null
};
}
function summarizeAuditEvent(event) {
return {
auditId: event.auditId,
traceId: event.traceId,
action: event.action,
targetType: event.targetType,
targetId: event.targetId,
projectId: event.projectId ?? null,
gatewaySessionId: event.gatewaySessionId ?? null,
operationId: event.operationId ?? null,
workerSessionId: event.workerSessionId ?? null,
serviceId: event.serviceId,
environment: event.environment,
outcome: event.outcome ?? null,
dryRun: event.metadata?.dryRun === true,
occurredAt: event.occurredAt
};
}
@@ -321,8 +342,12 @@ export function buildMvpGateSummary({ report, plan }) {
capabilityId: operation.capabilityId,
agentSessionId: operation.agentSessionId ?? null,
workerSessionId: operation.workerSessionId ?? null,
environment: operation.environment
environment: operation.environment,
dryRun: operation.input?.dryRun === true,
requestedAt: operation.requestedAt ?? null,
updatedAt: operation.updatedAt ?? null
})),
auditEvents: plan.auditEvents.map(summarizeAuditEvent),
evidenceRecords: plan.evidenceRecords.map(summarizeEvidence),
steps,
realDevGate: {
@@ -73,6 +73,21 @@ const requiredWiringColumns = Object.freeze([
"trace/evidence"
]);
const requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
"接线/operation",
"audit 记录",
"evidence 证据",
"trace/messageId",
"audit.event.query + SOURCE 回退",
"evidence.record.query + SOURCE 回退",
"conversationId、traceId 和 messageId",
"失败原因=",
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
"当前显示 SOURCE 回退,不能冒充 DEV-LIVE"
]);
const requiredHardwareStatusTerms = Object.freeze([
"hardware-source-status",
"hardwareGroup",
@@ -208,6 +223,11 @@ function runStaticSmoke() {
evidence: requiredWiringColumns
});
addCheck(checks, blockers, "trusted-record-groups", trustedRecordGroups(files), "Trusted Records panel groups Code Agent, wiring/operation, audit, and evidence summaries with source labels and live-failure fallback.", {
blocker: "observability_blocker",
evidence: requiredTrustedRecordTerms
});
addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", {
blocker: "contract_blocker",
evidence: requiredHardwareStatusTerms
@@ -512,6 +532,38 @@ function defaultTopologyIsM3Only() {
);
}
function trustedRecordGroups({ html, app, styles }) {
const source = `${html}\n${app}\n${styles}`;
return (
/id=["']records-list["']/u.test(html) &&
/function\s+renderRecords\s*\(/u.test(app) &&
/function\s+codeAgentRecordCards\s*\(/u.test(app) &&
/function\s+operationRecordCards\s*\(/u.test(app) &&
/function\s+auditRecordCards\s*\(/u.test(app) &&
/function\s+evidenceRecordCards\s*\(/u.test(app) &&
/function\s+auditCard\s*\(/u.test(app) &&
/function\s+evidenceCard\s*\(/u.test(app) &&
/function\s+recordGroup\s*\(/u.test(app) &&
/function\s+recordField\s*\(/u.test(app) &&
/function\s+sourceKindLabel\s*\(/u.test(app) &&
/function\s+liveFailureCard\s*\(/u.test(app) &&
/state\.liveSurface/u.test(app) &&
/renderRecords\(state\.liveSurface\)/u.test(app) &&
/traceId: error\.traceId \|\| traceId/u.test(app) &&
/error\.traceId = traceId/u.test(app) &&
requiredTrustedRecordTerms.every((term) => source.includes(term)) &&
/\.record-group\s*\{/u.test(styles) &&
/\.record-group-title\s*\{/u.test(styles) &&
/\.record-group-list\s*\{/u.test(styles) &&
/replaceChildren\(el\.recordsList, \.\.\.groups\)/u.test(app) &&
/gateSummary\.operations\.map/u.test(app) &&
/gateSummary\.auditEvents\.slice\(0,\s*4\)\.map/u.test(app) &&
/gateSummary\.evidenceRecords\.map/u.test(app) &&
/message\.status === "failed" \? "blocked" : "source"/u.test(app) &&
!/message\.status === "completed"\s*\?\s*"dev-live"/u.test(app)
);
}
function finalRouteFallback(app) {
const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? "";
const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]);
@@ -591,6 +643,8 @@ function sourceEvidenceNotDevLive(source) {
const m3 = gateSummary.milestones.find((item) => item.id === "M3");
return (
m3?.status === "blocked" &&
gateSummary.operations.every((operation) => operation.dryRun === true) &&
gateSummary.auditEvents.every((event) => event.dryRun === true) &&
gateSummary.evidenceRecords.every((record) => record.dryRun === true) &&
gateSummary.safety.allowNetwork === false &&
["来源 SOURCE", "演练 DRY-RUN", "开发实况 DEV-LIVE", "阻塞 BLOCKED"].every((label) => source.includes(label))
+215 -29
View File
@@ -92,7 +92,8 @@ const state = {
conversationId: null,
codeAgentAvailability: null,
chatMessages: [],
chatPending: false
chatPending: false,
liveSurface: null
};
initRoutes();
@@ -220,12 +221,14 @@ function initCommandBar() {
event.preventDefault();
const value = el.commandInput.value.trim();
if (!value || state.chatPending) return;
const traceId = nextProtocolId("trc");
const userMessage = {
id: nextProtocolId("msg"),
role: "user",
title: `用户 ${shortTime(new Date().toISOString())}`,
text: value,
status: "sent",
traceId,
createdAt: new Date().toISOString()
};
const pendingMessage = {
@@ -234,6 +237,7 @@ function initCommandBar() {
title: "Code Agent 处理中",
text: "正在调用真实 Code Agent / Codex 后端,请稍候。",
status: "running",
traceId,
createdAt: new Date().toISOString()
};
state.chatMessages.push(userMessage, pendingMessage);
@@ -242,9 +246,10 @@ function initCommandBar() {
renderAgentChatStatus("running");
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
try {
const result = await sendAgentMessage(value, state.conversationId);
const result = await sendAgentMessage(value, state.conversationId, traceId);
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const status = result.status === "completed" ? "completed" : "failed";
@@ -278,6 +283,7 @@ function initCommandBar() {
title: "发送失败",
text: `发送失败:${error.message}`,
status: "failed",
traceId: error.traceId || traceId,
updatedAt: new Date().toISOString(),
error: {
message: error.message
@@ -289,6 +295,7 @@ function initCommandBar() {
state.chatPending = false;
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
}
});
@@ -300,6 +307,7 @@ function initCommandBar() {
renderAgentChatStatus("idle");
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
});
}
@@ -329,8 +337,7 @@ function renderProbePending() {
renderMethodList(rpcReadMethods, "pending");
}
async function sendAgentMessage(message, conversationId) {
const traceId = nextProtocolId("trc");
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc")) {
const response = await fetchJson("/v1/agent/chat", {
method: "POST",
headers: {
@@ -346,9 +353,14 @@ async function sendAgentMessage(message, conversationId) {
});
if (!response.ok) {
throw new Error(response.error || "Code Agent 请求失败");
const error = new Error(response.error || "Code Agent 请求失败");
error.traceId = traceId;
throw error;
}
return response.data;
return {
...response.data,
traceId: response.data?.traceId || traceId
};
}
async function loadLiveSurface() {
@@ -452,6 +464,7 @@ function nextProtocolId(prefix) {
}
function renderLiveSurface(live) {
state.liveSurface = live;
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const reachable = coreProbes.some((probe) => probe.ok);
const runtimeSummary = runtimeSummaryFrom(live);
@@ -904,33 +917,206 @@ function renderWiringList(summary = null) {
}
function renderRecords(live) {
const sourceCards = gateSummary.evidenceRecords.map((record) =>
infoCard({
title: record.evidenceId,
detail: `${recordKindLabel(record.kind)} / ${record.operationId} / ${record.dryRun ? "演练 DRY-RUN" : "未知来源"}`,
tone: record.dryRun ? "dry-run" : "source"
const groups = [
recordGroup({
title: "Code Agent 对话记录",
meta: "trace/messageId",
cards: codeAgentRecordCards()
}),
recordGroup({
title: "接线/operation",
meta: "hwlab-patch-panel + operation",
cards: operationRecordCards()
}),
recordGroup({
title: "audit 记录",
meta: "audit.event.query + SOURCE 回退",
cards: auditRecordCards(live)
}),
recordGroup({
title: "evidence 证据",
meta: "evidence.record.query + SOURCE 回退",
cards: evidenceRecordCards(live)
})
);
];
const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : [];
const liveCards = liveRecords.map((record) => {
const m3Trusted = hasTrustedM3LiveEvidence(record);
return infoCard({
title: record.evidenceId,
detail: [
m3Trusted ? "开发实况 DEV-LIVE M3" : "阻塞 BLOCKED:只读 evidence 查询不是 M3 trusted loop",
recordKindLabel(record.kind ?? "record"),
record.operationId ?? "n/a",
record.serviceId ?? "未知服务"
].join(" / "),
tone: m3Trusted ? "dev-live" : "blocked"
});
});
if (live && !live.evidence.ok) {
liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" }));
replaceChildren(el.recordsList, ...groups);
}
function codeAgentRecordCards() {
const messages = state.chatMessages.filter((message) => message.traceId || message.messageId);
if (messages.length === 0) {
return [
infoCard({
title: "尚无 Code Agent 请求记录",
detail: "发送消息后会在这里显示 conversationId、traceId 和 messageId;未发送时仅显示 SOURCE 说明。",
tone: "source"
})
];
}
replaceChildren(el.recordsList, ...liveCards, ...sourceCards);
return messages.slice(-4).reverse().map((message) =>
infoCard({
title: `${roleLabel(message.role)} ${statusLabel(message.status)}`,
detail: [
sourceKindLabel(message.status === "failed" ? "blocked" : "source"),
recordField("conversation", state.conversationId),
recordField("trace", message.traceId),
recordField("message", message.messageId),
message.error?.message ? `失败原因=${message.error.message}` : null
].filter(Boolean).join(" / "),
tone: message.status === "failed" ? "blocked" : "source"
})
);
}
function operationRecordCards() {
const wiring = gateSummary.topology.patchPanel;
const wiringCards = wiring.activeConnections.map((link) =>
infoCard({
title: `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`,
detail: [
sourceKindLabel("source"),
recordField("patch-panel", wiring.serviceId),
`状态=${statusLabel(wiring.state)}`,
recordField("statusId", wiring.patchPanelStatusId)
].filter(Boolean).join(" / "),
tone: "source"
})
);
const operationCards = gateSummary.operations.map((operation) =>
infoCard({
title: operation.operationId,
detail: [
sourceKindLabel(operation.dryRun ? "dry-run" : "source"),
`状态=${statusLabel(operation.status)}`,
recordField("resource", operation.resourceId),
recordField("capability", operation.capabilityId),
recordField("worker", operation.workerSessionId)
].filter(Boolean).join(" / "),
tone: operation.dryRun ? "dry-run" : "source"
})
);
return [...wiringCards, ...operationCards];
}
function auditRecordCards(live) {
const cards = [];
if (live) {
if (live.audit.ok) {
const liveEvents = live.audit.data?.events ?? [];
if (liveEvents.length === 0) {
cards.push(infoCard({
title: "audit.event.query",
detail: "只读查询成功但无 M3 trusted loop audit;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。",
tone: "source"
}));
} else {
cards.push(...liveEvents.map((event) => auditCard(event, hasTrustedM3LiveEvidence(event) ? "dev-live" : "blocked")));
}
} else {
cards.push(liveFailureCard("audit.event.query", live.audit.error));
}
} else {
cards.push(infoCard({
title: "audit.event.query",
detail: "等待同源只读查询;未完成前显示 SOURCE/DRY-RUN 回退摘要。",
tone: "source"
}));
}
cards.push(...gateSummary.auditEvents.slice(0, 4).map((event) => auditCard(event, event.dryRun ? "dry-run" : "source")));
return cards;
}
function evidenceRecordCards(live) {
const cards = [];
if (live) {
if (live.evidence.ok) {
const liveRecords = live.evidence.data?.records ?? [];
if (liveRecords.length === 0) {
cards.push(infoCard({
title: "evidence.record.query",
detail: "只读查询成功但无 M3 trusted loop evidence;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。",
tone: "source"
}));
} else {
cards.push(...liveRecords.map((record) => evidenceCard(record, hasTrustedM3LiveEvidence(record) ? "dev-live" : "blocked")));
}
} else {
cards.push(liveFailureCard("evidence.record.query", live.evidence.error));
}
} else {
cards.push(infoCard({
title: "evidence.record.query",
detail: "等待同源只读查询;未完成前显示 SOURCE/DRY-RUN 回退摘要。",
tone: "source"
}));
}
cards.push(...gateSummary.evidenceRecords.map((record) => evidenceCard(record, record.dryRun ? "dry-run" : "source")));
return cards;
}
function auditCard(event, tone) {
return infoCard({
title: event.auditId,
detail: [
sourceKindLabel(tone),
tone === "blocked" ? "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定" : null,
recordField("action", event.action),
recordField("target", event.targetId),
recordField("operation", event.operationId),
recordField("trace", event.traceId),
recordField("service", event.serviceId)
].filter(Boolean).join(" / "),
tone
});
}
function evidenceCard(record, tone) {
return infoCard({
title: record.evidenceId,
detail: [
sourceKindLabel(tone),
tone === "blocked" ? "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定" : null,
recordKindLabel(record.kind ?? "record"),
recordField("operation", record.operationId),
recordField("trace", record.traceId ?? record.metadata?.traceId),
recordField("service", record.serviceId)
].filter(Boolean).join(" / "),
tone
});
}
function liveFailureCard(title, reason) {
return infoCard({
title,
detail: `阻塞 BLOCKED:开发实况查询失败,失败原因:${reason || "未知错误"};当前显示 SOURCE 回退,不能冒充 DEV-LIVE。`,
tone: "blocked"
});
}
function recordGroup({ title, meta, cards }) {
const section = document.createElement("section");
section.className = "record-group";
const header = document.createElement("div");
header.className = "record-group-head";
header.append(textSpan(title, "record-group-title"), textSpan(meta, "record-group-meta"));
const body = document.createElement("div");
body.className = "record-group-list";
body.append(...cards);
section.append(header, body);
return section;
}
function recordField(label, value) {
if (value === undefined || value === null || value === "") return null;
return `${label}=${value}`;
}
function sourceKindLabel(tone) {
return statusLabel(tone);
}
function renderDiagnostics(live) {
+128 -4
View File
@@ -596,7 +596,10 @@ export const gateSummary = {
"capabilityId": "cap_m5-relay-set",
"agentSessionId": null,
"workerSessionId": null,
"environment": "dev"
"environment": "dev",
"dryRun": true,
"requestedAt": "2026-05-21T00:02:00.000Z",
"updatedAt": "2026-05-21T00:02:30.000Z"
},
{
"operationId": "op_m5-agent-0001",
@@ -606,7 +609,124 @@ export const gateSummary = {
"capabilityId": "cap_m5-relay-set",
"agentSessionId": "agt_m5-0001",
"workerSessionId": "wkr_m5-0001",
"environment": "dev"
"environment": "dev",
"dryRun": true,
"requestedAt": "2026-05-21T00:03:00.000Z",
"updatedAt": "2026-05-21T00:03:30.000Z"
}
],
"auditEvents": [
{
"auditId": "aud_m5-project-selected",
"traceId": "trc_m5-project",
"action": "project.select",
"targetType": "project",
"targetId": "proj_m5-mvp-e2e",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": null,
"operationId": null,
"workerSessionId": null,
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:00:00.000Z"
},
{
"auditId": "aud_m5-gateway-started",
"traceId": "trc_m5-gateway",
"action": "gateway.session.start",
"targetType": "gateway_session",
"targetId": "gws_m5-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": "gws_m5-0001",
"operationId": null,
"workerSessionId": null,
"serviceId": "hwlab-gateway",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:00:30.000Z"
},
{
"auditId": "aud_m5-wiring-applied",
"traceId": "trc_m5-wiring",
"action": "wiring.apply",
"targetType": "wiring_config",
"targetId": "wir_m5-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": "gws_m5-0001",
"operationId": null,
"workerSessionId": null,
"serviceId": "hwlab-patch-panel",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:01:00.000Z"
},
{
"auditId": "aud_m5-direct-operation",
"traceId": "trc_m5-direct-0001",
"action": "hardware.operation.request",
"targetType": "hardware_operation",
"targetId": "op_m5-direct-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": "gws_m5-0001",
"operationId": "op_m5-direct-0001",
"workerSessionId": null,
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:02:30.000Z"
},
{
"auditId": "aud_m5-agent-session",
"traceId": "trc_m5-agent-session",
"action": "agent.session.start",
"targetType": "agent_session",
"targetId": "agt_m5-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": null,
"operationId": null,
"workerSessionId": "wkr_m5-0001",
"serviceId": "hwlab-agent-mgr",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:02:45.000Z"
},
{
"auditId": "aud_m5-agent-operation",
"traceId": "trc_m5-agent-0001",
"action": "hardware.operation.request",
"targetType": "hardware_operation",
"targetId": "op_m5-agent-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": "gws_m5-0001",
"operationId": "op_m5-agent-0001",
"workerSessionId": "wkr_m5-0001",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:03:30.000Z"
},
{
"auditId": "aud_m5-cleanup",
"traceId": "trc_m5-cleanup",
"action": "worker.cleanup",
"targetType": "worker_session",
"targetId": "wkr_m5-0001",
"projectId": "proj_m5-mvp-e2e",
"gatewaySessionId": "gws_m5-0001",
"operationId": null,
"workerSessionId": "wkr_m5-0001",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "succeeded",
"dryRun": true,
"occurredAt": "2026-05-21T00:05:00.000Z"
}
],
"evidenceRecords": [
@@ -620,7 +740,9 @@ export const gateSummary = {
"sha256": "4cd5cc2c942501b55f7a32f4184e9a1c920b31f13110f44ec68ddc0ad7dc1e34",
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"dryRun": true
"traceId": "trc_m5-direct-0001",
"dryRun": true,
"createdAt": "2026-05-21T00:02:40.000Z"
},
{
"evidenceId": "evi_m5-agent-0001",
@@ -632,7 +754,9 @@ export const gateSummary = {
"sha256": "96e7ffa99efd78b3d5d1b1dfd08be2f077f618dacf786b47f764794714c15c1c",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"dryRun": true
"traceId": "trc_m5-agent-0001",
"dryRun": true,
"createdAt": "2026-05-21T00:03:40.000Z"
}
],
"steps": [
+41
View File
@@ -38,6 +38,20 @@ const distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-c
const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8");
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
const frontendSource = `${html}\n${app}\n${artifactPublisher}`;
const requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
"接线/operation",
"audit 记录",
"evidence 证据",
"trace/messageId",
"audit.event.query + SOURCE 回退",
"evidence.record.query + SOURCE 回退",
"conversationId、traceId 和 messageId",
"失败原因=",
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
"当前显示 SOURCE 回退,不能冒充 DEV-LIVE"
]);
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
@@ -92,6 +106,21 @@ assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*
assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u);
assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/);
assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/);
assert.match(app, /function renderRecords/);
assert.match(app, /function codeAgentRecordCards/);
assert.match(app, /function operationRecordCards/);
assert.match(app, /function auditRecordCards/);
assert.match(app, /function evidenceRecordCards/);
assert.match(app, /function auditCard/);
assert.match(app, /function evidenceCard/);
assert.match(app, /function recordGroup/);
assert.match(app, /function recordField/);
assert.match(app, /function sourceKindLabel/);
assert.match(app, /function liveFailureCard/);
assert.match(app, /state\.liveSurface/);
assert.match(app, /renderRecords\(state\.liveSurface\)/);
assert.match(app, /traceId: error\.traceId \|\| traceId/);
assert.match(app, /error\.traceId = traceId/);
for (const hardwareTerm of [
"Gateway/Box 在线",
"BOX-SIMU 端口",
@@ -110,6 +139,8 @@ for (const hardwareTerm of [
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
}
assert.match(app, /dataset\.sourceKind/);
assert.match(styles, /\.record-group\s*{/);
assert.match(styles, /\.record-group-title\s*{/);
assert.match(styles, /\.hardware-section\s*{/);
assert.match(styles, /\.hardware-row\s*{/);
assert.match(html, /Gate \/ 诊断 \/ 验收/);
@@ -198,6 +229,9 @@ assert.match(html, /id="unblock-list"/);
assert.match(app, /DB live readiness/);
assert.match(app, /res_boxsimu_1:DO1 -> res_boxsimu_2:DI1/);
assert.match(app, /patch-panel DO1 -> DI1/);
for (const trustedRecordTerm of requiredTrustedRecordTerms) {
assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`);
}
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u);
@@ -314,7 +348,14 @@ assert.doesNotMatch(
assert.equal(gateSummary.m5DryRunTopology.patchPanel.patchPanelStatusId, "pps_m5-0001");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.operations.length, 2);
assert.ok(gateSummary.operations.every((operation) => operation.dryRun === true));
assert.ok(gateSummary.operations.every((operation) => operation.requestedAt && operation.updatedAt));
assert.equal(gateSummary.auditEvents.length, 7);
assert.ok(gateSummary.auditEvents.every((event) => event.dryRun === true));
assert.ok(gateSummary.auditEvents.every((event) => event.traceId && event.occurredAt));
assert.equal(gateSummary.evidenceRecords.length, 2);
assert.ok(gateSummary.evidenceRecords.every((record) => record.traceId && record.createdAt && record.dryRun === true));
assert.equal(gateSummary.safety.allowNetwork, false);
runCloudWebM3ReadonlyContract();
+34
View File
@@ -448,6 +448,40 @@ h3 {
gap: 8px;
}
.record-group {
min-width: 0;
display: grid;
gap: 7px;
padding: 8px;
background: var(--surface);
border: 1px solid var(--line);
}
.record-group-head {
min-width: 0;
display: grid;
gap: 2px;
}
.record-group-title {
color: var(--text);
font-weight: 800;
overflow-wrap: anywhere;
}
.record-group-meta {
color: var(--dim);
font-family: var(--mono);
font-size: 10px;
overflow-wrap: anywhere;
}
.record-group-list {
min-height: 0;
display: grid;
gap: 7px;
}
.message-card,
.info-card,
.trace-item,