fix: render code agent conversation thread
This commit is contained in:
@@ -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."
|
||||
]
|
||||
};
|
||||
|
||||
+101
-13
@@ -283,12 +283,12 @@ function initCommandBar() {
|
||||
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 realCompleted = isRealCompletedChatResult(result);
|
||||
const status = realCompleted ? "completed" : "failed";
|
||||
const completion = classifyCodeAgentCompletion(result);
|
||||
const status = completion.status;
|
||||
state.chatMessages[index] = {
|
||||
...pendingMessage,
|
||||
title: realCompleted ? sourceTitle(result) : result.status === "completed" ? "Code Agent 完成证据不足" : "Code Agent 返回失败",
|
||||
text: realCompleted
|
||||
title: completion.title,
|
||||
text: completion.replied
|
||||
? result.reply?.content || "Code Agent 没有返回文本。"
|
||||
: result.status === "completed"
|
||||
? untrustedCompletionMessage(result)
|
||||
@@ -300,9 +300,10 @@ function initCommandBar() {
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
backend: result.backend,
|
||||
sourceKind: completion.sourceKind,
|
||||
providerTrace: result.providerTrace,
|
||||
updatedAt: result.updatedAt,
|
||||
error: result.error ?? (result.status === "completed" && !realCompleted
|
||||
error: result.error ?? (result.status === "completed" && !completion.replied
|
||||
? {
|
||||
code: "untrusted_completion",
|
||||
message: "completed 回复缺少真实 provider/model/trace/conversation 证据,或 provider 属于 echo/mock/stub。"
|
||||
@@ -313,7 +314,7 @@ function initCommandBar() {
|
||||
if (result.availability) {
|
||||
state.codeAgentAvailability = result.availability;
|
||||
}
|
||||
if (!realCompleted) {
|
||||
if (!completion.replied) {
|
||||
el.commandInput.value = value;
|
||||
}
|
||||
renderAgentChatStatus(status, result);
|
||||
@@ -1289,6 +1290,7 @@ function renderGateDiagnostics() {
|
||||
"node --check web/hwlab-cloud-web/app.mjs",
|
||||
"node --check web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"node web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
|
||||
"node scripts/l6-cli-web-smoke.mjs",
|
||||
"git diff --check"
|
||||
].map((command) => {
|
||||
@@ -1307,7 +1309,8 @@ function renderAgentChatStatus(status, result = null) {
|
||||
const labels = {
|
||||
idle: "等待输入",
|
||||
running: "处理中",
|
||||
completed: "已回复",
|
||||
completed: "DEV-LIVE 回复",
|
||||
source: "SOURCE 回复",
|
||||
failed: "发送失败",
|
||||
blocked: "BLOCKED 凭证缺口"
|
||||
};
|
||||
@@ -1327,6 +1330,35 @@ function sourceTitle(result) {
|
||||
return `Code Agent 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
|
||||
}
|
||||
|
||||
function sourceFixtureTitle(result) {
|
||||
return `Code Agent SOURCE 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
|
||||
}
|
||||
|
||||
function classifyCodeAgentCompletion(result) {
|
||||
if (isRealCompletedChatResult(result)) {
|
||||
return {
|
||||
status: "completed",
|
||||
replied: true,
|
||||
sourceKind: "DEV-LIVE",
|
||||
title: sourceTitle(result)
|
||||
};
|
||||
}
|
||||
if (isSourceFixtureChatResult(result)) {
|
||||
return {
|
||||
status: "source",
|
||||
replied: true,
|
||||
sourceKind: "SOURCE",
|
||||
title: sourceFixtureTitle(result)
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "failed",
|
||||
replied: false,
|
||||
sourceKind: "BLOCKED",
|
||||
title: result?.status === "completed" ? "Code Agent 完成证据不足" : "Code Agent 返回失败"
|
||||
};
|
||||
}
|
||||
|
||||
function failureMessage(result) {
|
||||
if (result?.error?.code === "provider_unavailable" || result?.availability?.status === "blocked") {
|
||||
const availability = result.availability ?? codeAgentAvailabilityFromResult(result);
|
||||
@@ -1357,15 +1389,35 @@ function messageCard(message) {
|
||||
article.append(textSpan(roleLabel(message.role), "message-role"));
|
||||
article.append(textSpan(message.title, "message-title"));
|
||||
article.append(textSpan(message.text, "message-copy"));
|
||||
const messageMeta = messageEvidenceFields(message).join(" / ");
|
||||
if (messageMeta) {
|
||||
article.append(textSpan(messageMeta, "message-meta"));
|
||||
}
|
||||
const messageEvidence = messageEvidencePanel(message);
|
||||
if (messageEvidence) article.append(messageEvidence);
|
||||
return article;
|
||||
}
|
||||
|
||||
function messageEvidencePanel(message) {
|
||||
const fields = messageEvidenceFields(message).map(boundedEvidenceField);
|
||||
if (fields.length === 0) return null;
|
||||
const details = document.createElement("details");
|
||||
details.className = "message-evidence";
|
||||
details.open = message.status === "completed" || message.status === "source";
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "message-meta";
|
||||
summary.textContent = messageEvidenceSummary(message, fields);
|
||||
const chips = document.createElement("div");
|
||||
chips.className = "message-evidence-chips";
|
||||
chips.append(...fields.map((field) => textSpan(field, "message-evidence-chip")));
|
||||
details.append(summary, chips);
|
||||
return details;
|
||||
}
|
||||
|
||||
function messageEvidenceSummary(message, fields) {
|
||||
const sourceKind = message.sourceKind ?? (message.status === "completed" ? "DEV-LIVE" : message.status === "failed" ? "BLOCKED" : "SOURCE");
|
||||
return `证据 ${sourceKind} / ${fields.slice(0, 3).join(" / ")}`;
|
||||
}
|
||||
|
||||
function messageEvidenceFields(message) {
|
||||
return [
|
||||
recordField("source", message.sourceKind),
|
||||
recordField("provider", message.provider),
|
||||
recordField("model", message.model),
|
||||
recordField("backend", message.backend),
|
||||
@@ -1376,6 +1428,11 @@ function messageEvidenceFields(message) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function boundedEvidenceField(field) {
|
||||
const text = String(field ?? "").replace(/\s+/gu, " ").trim();
|
||||
return text.length > 96 ? `${text.slice(0, 93)}...` : text;
|
||||
}
|
||||
|
||||
function providerTraceSummary(providerTrace) {
|
||||
if (!providerTrace || typeof providerTrace !== "object") return null;
|
||||
const responseId = providerTrace.responseId ?? providerTrace.id;
|
||||
@@ -1491,6 +1548,14 @@ function codeAgentStatusMessage(availability) {
|
||||
status: "blocked"
|
||||
};
|
||||
}
|
||||
if (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||||
return {
|
||||
role: "system",
|
||||
title: "Code Agent 状态:SOURCE fixture",
|
||||
text: "当前只用于本地浏览器 smoke:可以验证发送、处理中、回复渲染和 evidence 展示,但不会标记为 DEV-LIVE。",
|
||||
status: "source"
|
||||
};
|
||||
}
|
||||
if (availability?.status === "available") {
|
||||
return {
|
||||
role: "system",
|
||||
@@ -1520,6 +1585,9 @@ function codeAgentControlSummary(availability) {
|
||||
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。`
|
||||
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。";
|
||||
}
|
||||
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
|
||||
return "最近一次为 SOURCE fixture 回复;它证明本地浏览器对话 UX,不作为 DEV-LIVE 或 #143 完成证据。";
|
||||
}
|
||||
if (availability?.status === "blocked") {
|
||||
return codeAgentBlockedSummary(availability);
|
||||
}
|
||||
@@ -1532,13 +1600,16 @@ function codeAgentBlockedSummary(availability) {
|
||||
|
||||
function untrustedCompletionMessage(result) {
|
||||
const provider = result?.provider ?? "unknown";
|
||||
return `Code Agent 返回 completed,但缺少真实 provider/model/trace/conversation 证据,或 provider=${provider} 属于 echo/mock/stub;本次不会标记为真实完成。`;
|
||||
return `Code Agent 返回 completed,但缺少真实 provider/model/trace/conversation 证据,或 provider=${provider} 属于 echo/mock/stub;本次不会标记为真实完成。SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE。`;
|
||||
}
|
||||
|
||||
function codeAgentAvailabilitySummary() {
|
||||
if (state.codeAgentAvailability?.status === "blocked") {
|
||||
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
|
||||
}
|
||||
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||||
return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。";
|
||||
}
|
||||
if (state.codeAgentAvailability?.status === "available") {
|
||||
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
|
||||
}
|
||||
@@ -1554,13 +1625,22 @@ function latestCompletedAgentMessage() {
|
||||
|
||||
function isRealCompletedChatResult(result) {
|
||||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||||
return result?.status === "completed" && assistantReply.length > 0 && hasRealCodeAgentEvidence(result);
|
||||
return result?.status === "completed" && assistantReply.length > 0 && !isSourceFixtureCompletion(result) && hasRealCodeAgentEvidence(result);
|
||||
}
|
||||
|
||||
function isSourceFixtureChatResult(result) {
|
||||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||||
return result?.status === "completed" && assistantReply.length > 0 && isSourceFixtureCompletion(result);
|
||||
}
|
||||
|
||||
function isRealCompletedChatMessage(message) {
|
||||
return message?.status === "completed" && !message.error && hasRealCodeAgentEvidence(message);
|
||||
}
|
||||
|
||||
function isSourceFixtureCompletedChatMessage(message) {
|
||||
return message?.status === "source" && !message.error && message.sourceKind === "SOURCE";
|
||||
}
|
||||
|
||||
function hasRealCodeAgentEvidence(value) {
|
||||
return (
|
||||
isTrustedCodeAgentProvider(value?.provider) &&
|
||||
@@ -1577,6 +1657,12 @@ function isTrustedCodeAgentProvider(provider) {
|
||||
return TRUSTED_CODE_AGENT_PROVIDERS.includes(normalized);
|
||||
}
|
||||
|
||||
function isSourceFixtureCompletion(value) {
|
||||
const sourceKind = String(value?.sourceKind ?? value?.evidenceLevel ?? value?.providerTrace?.sourceKind ?? "").trim().toUpperCase();
|
||||
const traceSource = String(value?.providerTrace?.source ?? value?.providerTrace?.mode ?? "").trim().toUpperCase();
|
||||
return sourceKind === "SOURCE" || traceSource.startsWith("SOURCE");
|
||||
}
|
||||
|
||||
function latestChatResult() {
|
||||
return [...state.chatMessages].reverse().find((message) => message.role === "agent") ?? null;
|
||||
}
|
||||
@@ -1585,6 +1671,7 @@ function deriveAgentChatStatus() {
|
||||
if (state.chatPending) return "running";
|
||||
const latest = latestChatResult();
|
||||
if (latest?.status === "completed") return "completed";
|
||||
if (latest?.status === "source") return "source";
|
||||
if (latest?.status === "failed") return latest.error?.code === "provider_unavailable" ? "blocked" : "failed";
|
||||
if (state.codeAgentAvailability?.status === "blocked") return "blocked";
|
||||
return "idle";
|
||||
@@ -1592,6 +1679,7 @@ function deriveAgentChatStatus() {
|
||||
|
||||
function currentConversationTone() {
|
||||
if (latestCompletedAgentMessage()) return "dev-live";
|
||||
if (isSourceFixtureCompletedChatMessage(latestChatResult())) return "source";
|
||||
if (state.codeAgentAvailability?.status === "blocked" || latestChatResult()?.status === "failed") return "blocked";
|
||||
return "source";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
|
||||
import {
|
||||
runDevCloudWorkbenchLocalAgentFixtureSmoke,
|
||||
runDevCloudWorkbenchMobileSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke
|
||||
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
@@ -60,17 +61,23 @@ const requiredTrustedRecordTerms = Object.freeze([
|
||||
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
|
||||
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
|
||||
const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
|
||||
const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states");
|
||||
const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke();
|
||||
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
|
||||
|
||||
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
||||
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(workbenchSmoke.devLive, false);
|
||||
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
|
||||
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
|
||||
assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct");
|
||||
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
|
||||
assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
|
||||
assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(mobileWorkbenchSmoke.devLive, false);
|
||||
assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(localAgentFixtureSmoke.devLive, false);
|
||||
if (mobileWorkbenchSmoke.status === "pass") {
|
||||
assert.equal(mobileWorkbenchSmoke.viewport.width, 390);
|
||||
assert.equal(mobileWorkbenchSmoke.viewport.height, 844);
|
||||
@@ -311,11 +318,27 @@ assert.match(app, /recordField\("provider", message\.provider\)/);
|
||||
assert.match(app, /Code Agent 完成证据不足/);
|
||||
assert.match(app, /本次不会标记为真实完成/);
|
||||
assert.match(app, /untrusted_completion/);
|
||||
assert.match(app, /function classifyCodeAgentCompletion/);
|
||||
assert.match(app, /function isSourceFixtureChatResult/);
|
||||
assert.match(app, /function isSourceFixtureCompletion/);
|
||||
assert.match(app, /function isSourceFixtureCompletedChatMessage/);
|
||||
assert.match(app, /Code Agent SOURCE 回复/);
|
||||
assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/);
|
||||
assert.match(app, /function messageEvidencePanel/);
|
||||
assert.match(app, /function messageEvidenceSummary/);
|
||||
assert.match(app, /function boundedEvidenceField/);
|
||||
assert.match(app, /sourceKind:\s*completion\.sourceKind/);
|
||||
assert.match(app, /status === "completed" \? "dev-live"/);
|
||||
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
|
||||
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
|
||||
assert.doesNotMatch(app, /tone:\s*state\.conversationId\s*\?\s*"dev-live"/);
|
||||
assert.doesNotMatch(`${html}\n${app}`, /sk-[A-Za-z0-9._-]{8,}/u);
|
||||
assert.match(styles, /\.message-card\.status-completed\s*{/);
|
||||
assert.match(styles, /\.message-card\.status-source\s*{/);
|
||||
assert.match(styles, /\.message-user\s*{/);
|
||||
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
|
||||
assert.match(styles, /\.message-evidence\s*{/);
|
||||
assert.match(styles, /\.message-evidence-chip\s*{/);
|
||||
assert.match(app, /state\.conversationId/);
|
||||
assert.match(app, /conversationId/);
|
||||
assert.match(app, /messageId/);
|
||||
|
||||
@@ -507,16 +507,44 @@ h3 {
|
||||
grid-template-columns: 82px minmax(0, 1fr);
|
||||
gap: 6px 12px;
|
||||
padding: 10px;
|
||||
width: min(100%, 780px);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.message-card.status-running {
|
||||
border-color: rgba(223, 170, 85, 0.52);
|
||||
}
|
||||
|
||||
.message-card.status-completed {
|
||||
border-color: rgba(127, 190, 116, 0.62);
|
||||
background: rgba(31, 47, 32, 0.78);
|
||||
}
|
||||
|
||||
.message-card.status-source {
|
||||
border-color: rgba(111, 183, 169, 0.46);
|
||||
background: rgba(24, 39, 37, 0.72);
|
||||
}
|
||||
|
||||
.message-card.status-failed {
|
||||
border-color: rgba(231, 110, 94, 0.58);
|
||||
}
|
||||
|
||||
.message-user {
|
||||
justify-self: end;
|
||||
border-color: rgba(212, 173, 67, 0.46);
|
||||
background: rgba(52, 47, 29, 0.68);
|
||||
}
|
||||
|
||||
.message-agent,
|
||||
.message-system {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.message-system {
|
||||
width: 100%;
|
||||
background: rgba(24, 27, 24, 0.76);
|
||||
}
|
||||
|
||||
.message-role {
|
||||
grid-row: span 2;
|
||||
color: var(--accent);
|
||||
@@ -538,13 +566,40 @@ h3 {
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
grid-column: 2;
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-evidence {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-evidence summary {
|
||||
cursor: pointer;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.message-evidence-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.message-evidence-chip {
|
||||
max-width: 100%;
|
||||
padding: 2px 5px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-blocker {
|
||||
border-color: rgba(231, 110, 94, 0.48);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user