fix: render code agent conversation thread

This commit is contained in:
Code Queue Review
2026-05-22 23:39:48 +00:00
parent 3c97791464
commit e647d2bd07
4 changed files with 530 additions and 26 deletions
+350 -12
View File
@@ -132,7 +132,10 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
"openai-responses",
"codex-cli",
"echo/mock/stub",
"untrusted_completion"
"untrusted_completion",
"SOURCE fixture",
"Code Agent SOURCE 回复",
"message-evidence-chip"
]);
const forbiddenWritePatterns = Object.freeze([
@@ -163,7 +166,10 @@ export function runDevCloudWorkbenchStaticSmoke() {
export async function runDevCloudWorkbenchSmoke(argv = []) {
const args = parseSmokeArgs(argv);
return args.mode === "live" ? runLiveSmoke(args) : runStaticSmoke();
if (args.mode === "live") return runLiveSmoke(args);
if (args.mode === "local-agent-fixture") return runDevCloudWorkbenchLocalAgentFixtureSmoke();
if (args.mode === "mobile") return runDevCloudWorkbenchMobileSmoke();
return runStaticSmoke();
}
export function parseSmokeArgs(argv) {
@@ -175,7 +181,10 @@ export function parseSmokeArgs(argv) {
} else if (arg === "--live") {
args.mode = "live";
} else if (arg === "--mobile") {
args.mode = "mobile";
args.mobile = true;
} else if (arg === "--local-agent-fixture") {
args.mode = "local-agent-fixture";
} else if (arg === "--url") {
index += 1;
if (!argv[index]) throw new Error("--url requires a value");
@@ -296,6 +305,17 @@ function runStaticSmoke() {
evidence: requiredCodeAgentEvidenceTerms
});
addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and provider_unavailable states as distinct conversation replies with bounded evidence.", {
blocker: "observability_blocker",
evidence: [
"DEV-LIVE 回复",
"SOURCE 回复",
"发送失败",
"BLOCKED 凭证缺口",
"message-evidence/details/chips"
]
});
addCheck(checks, blockers, "no-hardware-write-api", noForbiddenWriteSurface(source), "Workbench source does not expose hardware write APIs or live mutation commands.", {
blocker: "safety_blocker",
evidence: forbiddenWritePatterns.map(String)
@@ -413,6 +433,7 @@ async function runLiveSmoke(args) {
function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, help = undefined, safety, url = null, runtimeIdentity = null }) {
const sourceIdentity = observeSourceIdentity();
const liveJourneyPassed = mode === "live" && status === "pass";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json",
@@ -465,15 +486,19 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he
"node --check scripts/dev-cloud-workbench-smoke.mjs",
"node --check scripts/src/dev-cloud-workbench-smoke-lib.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --static",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json",
"node scripts/validate-dev-gate-report.mjs reports/dev-gate/dev-cloud-workbench-live.json",
"node scripts/validate-dev-gate-report.mjs"
],
localSmoke: {
status: mode === "static" ? status : "not_run",
commands: ["node scripts/dev-cloud-workbench-smoke.mjs --static"],
evidence: ["Static mode verifies the source contract and same-origin Code Agent wiring only."],
summary: "Static workbench smoke is SOURCE-level evidence and does not prove the deployed browser journey."
commands: [
"node scripts/dev-cloud-workbench-smoke.mjs --static",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture"
],
evidence: ["Static mode verifies the source contract and same-origin Code Agent wiring only.", "Local fixture browser mode verifies send/reply UX at SOURCE level."],
summary: "Static and local fixture workbench smokes are SOURCE-level evidence and do not prove the deployed browser journey."
},
dryRun: {
status: "not_applicable",
@@ -482,7 +507,7 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he
summary: "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
},
devPreconditions: {
status: status === "pass" ? "pass" : "blocked",
status: liveJourneyPassed ? "pass" : "blocked",
requirements: [
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
"Browser DOM exposes the default workbench and core controls.",
@@ -492,9 +517,11 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he
commands: [
"node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json"
],
summary: status === "pass"
summary: liveJourneyPassed
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
: "Deployed browser journey is blocked; see checks and blockers."
: mode === "live"
? "Deployed browser journey is blocked; see checks and blockers."
: "Static/local fixture evidence does not establish the deployed 16666 browser journey."
},
endpoints: {
frontend: runtime.endpoints.frontend,
@@ -862,12 +889,39 @@ function hasCodeAgentCompletedEvidenceVisibility({ app }) {
/UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/u.test(realEvidenceBody) &&
/value\?\.conversationId \|\| value\?\.sessionId/u.test(realEvidenceBody) &&
/isRealCompletedChatResult\(result\)/u.test(app) &&
/realCompleted \? "completed" : "failed"/u.test(app) &&
/classifyCodeAgentCompletion\(result\)/u.test(app) &&
/status:\s*"completed"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/sourceKind:\s*"DEV-LIVE"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/Code Agent 完成证据不足/u.test(app) &&
/本次不会标记为真实完成/u.test(app)
);
}
function hasCodeAgentConversationUxStates({ app, styles }) {
const submitBody = functionBody(app, "initCommandBar");
return (
/function\s+classifyCodeAgentCompletion\s*\(/u.test(app) &&
/function\s+isSourceFixtureChatResult\s*\(/u.test(app) &&
/function\s+isSourceFixtureCompletion\s*\(/u.test(app) &&
/function\s+isSourceFixtureCompletedChatMessage\s*\(/u.test(app) &&
/function\s+messageEvidencePanel\s*\(/u.test(app) &&
/function\s+messageEvidenceSummary\s*\(/u.test(app) &&
/function\s+boundedEvidenceField\s*\(/u.test(app) &&
/Code Agent SOURCE 回复/u.test(app) &&
/SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/u.test(app) &&
/sourceKind:\s*completion\.sourceKind/u.test(submitBody) &&
/completion\.replied/u.test(submitBody) &&
/status-source/u.test(styles) &&
/message-user/u.test(styles) &&
/message-agent/u.test(styles) &&
/message-evidence\s*\{/u.test(styles) &&
/message-evidence-chip/u.test(styles) &&
/width:\s*min\(100%,\s*780px\)/u.test(styles) &&
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : "source"/u.test(app) &&
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(app)
);
}
function noForbiddenWriteSurface(source) {
return forbiddenWritePatterns.every((pattern) => !pattern.test(source));
}
@@ -1286,7 +1340,7 @@ async function inspectLiveUserJourney(url) {
Boolean(responseSummary.provider) &&
Boolean(responseSummary.model) &&
Boolean(responseSummary.traceId);
const uiCompleted = ui.agentChatStatus === "回复" || ui.completedMessageVisible;
const uiCompleted = ui.agentChatStatus === "DEV-LIVE 回复" || ui.completedMessageVisible;
const pass = backendCompleted && uiCompleted && !ui.failedMessageVisible;
return {
@@ -1357,11 +1411,16 @@ async function inspectJourneyUi(page) {
agentChatStatus: document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null,
inputCleared: document.querySelector("#command-input")?.value === "",
completedMessageVisible: Boolean(document.querySelector(".message-card.status-completed")),
sourceMessageVisible: Boolean(document.querySelector(".message-card.status-source")),
failedMessageVisible: Boolean(document.querySelector(".message-card.status-failed")),
messageCount: document.querySelectorAll(".message-card").length,
completedMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
.some((element) => /openai-responses|gpt-5\.5|trc_/u.test(element.textContent ?? ""))
),
sourceMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-source .message-meta")]
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
)
}));
}
@@ -1370,6 +1429,8 @@ function sanitizeAgentChatBody(body) {
if (!body || typeof body !== "object") return null;
return {
status: typeof body.status === "string" ? body.status : null,
sourceKind: typeof body.sourceKind === "string" ? body.sourceKind : null,
evidenceLevel: typeof body.evidenceLevel === "string" ? body.evidenceLevel : null,
provider: typeof body.provider === "string" ? body.provider : null,
model: typeof body.model === "string" ? body.model : null,
backend: typeof body.backend === "string" ? body.backend : null,
@@ -1384,6 +1445,145 @@ function sanitizeAgentChatBody(body) {
};
}
export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
let chromium;
try {
({ chromium } = await import("playwright"));
} catch (error) {
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
evidenceLevel: "SOURCE",
devLive: false,
summary: `Local Code Agent browser fixture smoke skipped because Playwright is unavailable: ${error.message}`,
checks: [],
blockers: [],
safety: staticSafety()
};
}
const server = await startStaticWebServer({ agentFixture: true });
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const agentResponses = [];
page.on("response", async (response) => {
if (!response.url().includes("/v1/agent/chat")) return;
let body = null;
try {
body = await response.json();
} catch {
body = null;
}
agentResponses.push({
method: response.request().method(),
status: response.status(),
urlPath: new URL(response.url()).pathname,
body: sanitizeAgentChatBody(body)
});
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const controls = await inspectJourneyControls(page);
await page.locator("#command-input").fill("请用一句话说明当前 HWLAB 工作台可以做什么。");
const responsePromise = page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
{ timeout: 15000 }
);
await page.locator("#command-send").click();
const response = await responsePromise;
const responseBody = await response.json().catch(() => null);
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
const ui = await inspectJourneyUi(page);
const responseSummary = sanitizeAgentChatBody(responseBody);
const pass =
response.ok() &&
responseSummary?.status === "completed" &&
responseSummary?.sourceKind === "SOURCE" &&
responseSummary?.hasReply === true &&
ui.agentChatStatus === "SOURCE 回复" &&
ui.sourceMessageVisible &&
ui.sourceMessageHasNonSensitiveMeta &&
!ui.completedMessageVisible &&
!ui.failedMessageVisible;
const checks = [
{
id: "local-agent-fixture-controls",
status: Object.values(controls).every(Boolean) ? "pass" : "blocked",
summary: "Local SOURCE fixture browser smoke can locate the Code Agent input, send button, thread, and right-side evidence panels.",
observations: controls
},
{
id: "local-agent-fixture-replied-state",
status: pass ? "pass" : "blocked",
summary: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: {
response: responseSummary,
ui,
networkEvents: agentResponses
}
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "observability_blocker",
scope: check.id,
status: "open",
summary: check.summary
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local browser smoke uses a sanitized SOURCE fixture for /v1/agent/chat; it proves the UI conversation path only and does not claim DEV-LIVE.",
checks,
blockers,
safety: {
...staticSafety(),
localFixtureOnly: true,
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
preservedLiveFields: ["provider", "model", "backend", "conversationId", "traceId", "messageId", "providerTrace"]
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `Local Code Agent browser fixture smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "observability_blocker",
scope: "local-agent-fixture-browser",
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
export async function runDevCloudWorkbenchMobileSmoke() {
let chromium;
try {
@@ -1537,9 +1737,12 @@ export async function runDevCloudWorkbenchMobileSmoke() {
}
}
async function startStaticWebServer() {
async function startStaticWebServer(options = {}) {
const server = http.createServer(async (request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url })) {
return;
}
let pathname = decodeURIComponent(url.pathname);
if (pathname === "/" || pathname === "/workbench" || pathname === "/gate" || pathname === "/diagnostics/gate") {
pathname = "/index.html";
@@ -1570,6 +1773,140 @@ async function startStaticWebServer() {
};
}
async function handleLocalAgentFixtureApi({ request, response, url }) {
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, {
serviceId: "hwlab-cloud-api",
status: "degraded",
sourceKind: "SOURCE",
codeAgent: localSourceFixtureAvailability(),
methods: [...readOnlyRpcMethods],
runtime: {
durable: false,
status: "degraded",
counts: {
gatewaySessions: gateSummary.gatewaySessionCount,
boxResources: gateSummary.boxResourceCount
}
}
});
return true;
}
if (request.method === "POST" && url.pathname === "/json-rpc") {
const body = await readJsonBody(request);
jsonResponse(response, 200, {
jsonrpc: "2.0",
id: body?.id ?? "req_source_fixture",
result: localRpcFixtureResult(body?.method)
});
return true;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
const body = await readJsonBody(request);
const timestamp = "2026-05-22T00:00:00.000Z";
const conversationId = stringOrFallback(body?.conversationId, "cnv_source_fixture_browser");
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId: "msg_source_fixture_browser",
status: "completed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId: stringOrFallback(body?.traceId, "trc_source_fixture_browser"),
provider: "source-fixture",
model: "gpt-source-fixture",
backend: "local-source-fixture",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
reply: {
messageId: "msg_source_fixture_browser",
role: "assistant",
content: "SOURCE fixture 回复:HWLAB 工作台可以整理任务、查看资源,并保持硬件变更受控。",
createdAt: timestamp
},
usage: null,
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
responseId: "rsp_source_fixture_sanitized",
redacted: true
}
});
return true;
}
return false;
}
function localSourceFixtureAvailability() {
return {
endpoint: "POST /v1/agent/chat",
provider: "source-fixture",
model: "gpt-source-fixture",
backend: "local-source-fixture",
mode: "local-source-fixture",
status: "available",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
ready: true,
summary: "Local browser smoke SOURCE fixture; it is not DEV-LIVE provider evidence.",
missingEnv: [],
secretRefs: [],
safety: {
localFixtureOnly: true,
secretsRead: false,
valuesRedacted: true
}
};
}
function localRpcFixtureResult(method) {
if (method === "system.health" || method === "cloud.adapter.describe") {
return {
status: "degraded",
sourceKind: "SOURCE",
methods: [...readOnlyRpcMethods],
runtime: {
durable: false,
counts: {
gatewaySessions: gateSummary.gatewaySessionCount,
boxResources: gateSummary.boxResourceCount
}
}
};
}
if (method === "audit.event.query") {
return { count: 0, events: [] };
}
if (method === "evidence.record.query") {
return { count: 0, records: [] };
}
return { status: "not_implemented", sourceKind: "SOURCE" };
}
async function readJsonBody(request) {
const chunks = [];
for await (const chunk of request) {
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString("utf8");
if (!text.trim()) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
function jsonResponse(response, status, body) {
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify(body));
}
function stringOrFallback(value, fallback) {
return typeof value === "string" && value.trim() ? value.trim() : fallback;
}
function contentTypeFor(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
@@ -1825,10 +2162,11 @@ function escapeRegExp(value) {
export function printSmokeHelp() {
return {
status: "usage",
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --mobile | --live --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-live.json]",
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --mobile | --local-agent-fixture | --live --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-live.json]",
notes: [
"Static mode reads repository files and emits SOURCE-level evidence only.",
"--mobile runs a local static 390x844 browser hit-test and still emits SOURCE-level evidence only.",
"--local-agent-fixture runs a local browser send/reply path with a sanitized SOURCE fixture and does not claim DEV-LIVE.",
"Live mode opens the deployed workbench in a browser, verifies core controls, sends one Code Agent message, and retains only non-sensitive response fields."
]
};