diff --git a/editors/vscode-prototype/package.json b/editors/vscode-prototype/package.json index 8d4d713..56cbfff 100644 --- a/editors/vscode-prototype/package.json +++ b/editors/vscode-prototype/package.json @@ -241,6 +241,11 @@ ], "default": "module", "description": "Experimental local prototype default declaration kind used by navigation; not a stable API." + }, + "pccxSystemVerilog.kv260.preflightTranscriptPath": { + "type": "string", + "default": "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + "description": "Experimental local prototype path for the read-only KV260 preflight transcript summary; not a stable API." } } } diff --git a/editors/vscode-prototype/src/config.mjs b/editors/vscode-prototype/src/config.mjs index 0f0ae2c..01435c7 100644 --- a/editors/vscode-prototype/src/config.mjs +++ b/editors/vscode-prototype/src/config.mjs @@ -20,6 +20,7 @@ export const CONFIG_KEYS = Object.freeze([ "defaultNavigationRoot", "defaultModule", "defaultDeclarationKind", + "kv260.preflightTranscriptPath", ]); export const FACADE_COMMAND_IDS = Object.freeze([ @@ -105,6 +106,9 @@ const DEFAULT_CONFIG = Object.freeze({ defaultNavigationRoot: "fixtures/modules", defaultModule: "simple_mod", defaultDeclarationKind: "module", + kv260: Object.freeze({ + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }), }); const SHELL_CONTROL_PATTERN = /(?:&&|\|\||;|`|\$\()/; @@ -197,6 +201,7 @@ export function defaultConfig() { pccxLab: { ...DEFAULT_CONFIG.pccxLab }, workflowBoundary: { ...DEFAULT_CONFIG.workflowBoundary }, validationRunner: { ...DEFAULT_CONFIG.validationRunner }, + kv260: { ...DEFAULT_CONFIG.kv260 }, }; } @@ -272,6 +277,13 @@ export function normalizeConfig(rawConfig = {}) { DEFAULT_CONFIG.defaultDeclarationKind, DECLARATION_KINDS, ), + kv260: { + preflightTranscriptPath: stringSetting( + rawConfig, + "kv260.preflightTranscriptPath", + DEFAULT_CONFIG.kv260.preflightTranscriptPath, + ), + }, }; } diff --git a/editors/vscode-prototype/src/extension.mjs b/editors/vscode-prototype/src/extension.mjs index 7190611..4fc0531 100644 --- a/editors/vscode-prototype/src/extension.mjs +++ b/editors/vscode-prototype/src/extension.mjs @@ -2,6 +2,8 @@ // Copyright 2026 pccxai import { execFile } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; import { dirname, isAbsolute, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -75,6 +77,7 @@ import { import { createKv260StatusPanel, formatKv260StatusPanel, + parseKv260PreflightTranscript, renderKv260StatusPanelHtml, } from "./kv260-status-panel.mjs"; @@ -496,6 +499,35 @@ function diagnosticFileForUri(file, root = DEFAULT_DIAGNOSTIC_FILE_ROOT) { return resolve(root, filePath); } +function resolveConfiguredPath(path, runtime = {}) { + const value = typeof path === "string" ? path : ""; + if (value === "~") { + return runtime.homeDir ?? homedir(); + } + if (value.startsWith("~/")) { + return resolve(runtime.homeDir ?? homedir(), value.slice(2)); + } + if (isAbsolute(value)) { + return value; + } + return resolve(runtime.repoRoot ?? DEFAULT_DIAGNOSTIC_FILE_ROOT, value); +} + +export async function readKv260PreflightTranscriptSummary(rawConfig = {}, runtime = {}) { + if (typeof runtime.kv260PreflightTranscriptText === "string") { + return parseKv260PreflightTranscript(runtime.kv260PreflightTranscriptText); + } + const config = normalizeConfig(rawConfig); + const configuredPath = config.kv260.preflightTranscriptPath; + const transcriptPath = resolveConfiguredPath(configuredPath, runtime); + const reader = runtime.kv260PreflightTranscriptReader ?? readFile; + try { + return parseKv260PreflightTranscript(await reader(transcriptPath, "utf8")); + } catch { + return parseKv260PreflightTranscript(""); + } +} + export function createPresenterDeps(vscodeApi, runtime = {}) { const diagnosticSeverity = { Error: vscodeApi?.DiagnosticSeverity?.Error ?? "Error", @@ -1139,7 +1171,11 @@ export function createCommandHandler(commandId, vscodeApi, runtime = {}) { if (commandId === SHOW_KV260_STATUS_PANEL_COMMAND) { let result; try { - const panel = createKv260StatusPanel(runtime.kv260StatusInputs); + const preflightTranscript = await readKv260PreflightTranscriptSummary(rawConfig, runtime); + const panel = createKv260StatusPanel({ + ...(runtime.kv260StatusInputs ?? {}), + preflightTranscript, + }); result = { ok: true, commandId, diff --git a/editors/vscode-prototype/src/kv260-status-panel.mjs b/editors/vscode-prototype/src/kv260-status-panel.mjs index 257bf39..dd1f7be 100644 --- a/editors/vscode-prototype/src/kv260-status-panel.mjs +++ b/editors/vscode-prototype/src/kv260-status-panel.mjs @@ -18,6 +18,19 @@ export const DEFAULT_SERIAL_PREFLIGHT_STATUS = Object.freeze({ last_preflight_at: null, }); +export const DEFAULT_PREFLIGHT_TRANSCRIPT_SUMMARY = Object.freeze({ + status: "not_captured", + captured: false, + winningPort: null, + baud: null, + loginOk: null, + uname: null, + unameDisplay: "no preflight captured yet", + xrtVersion: null, + xmutilAppCount: null, + message: "no preflight captured yet", +}); + export const DEFAULT_LAUNCHER_NPU_STATUS = Object.freeze({ bitstream_loaded: false, bitstream_uuid: null, @@ -178,6 +191,13 @@ function optionalIntegerField(value, path, errors) { return value; } +function optionalNonNegativeIntegerField(value, path, errors) { + if (value == null) { + return null; + } + return integerField(value, path, errors); +} + function integerField(value, path, errors) { if (!Number.isSafeInteger(value) || value < 0) { addError(errors, path, "must be a non-negative integer"); @@ -195,6 +215,181 @@ function firstPresent(value, keys) { return undefined; } +function compactWhitespace(value) { + return String(value ?? "") + .replace(/\x1b\[[0-9;]*m/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +function boundedSingleLine(value, maxCharacters = 160) { + const text = compactWhitespace(value); + if (text.length <= maxCharacters) { + return text; + } + return `${text.slice(0, maxCharacters - 3)}...`; +} + +function parseBooleanText(value) { + const text = compactWhitespace(value).toLowerCase(); + if (/^(?:ok|pass|passed|true|yes|success|successful)$/.test(text)) { + return true; + } + if (/^(?:fail|failed|false|no|not ok|blocked)$/.test(text)) { + return false; + } + return null; +} + +function parseIntegerText(value) { + const text = compactWhitespace(value); + if (!/^\d+$/.test(text)) { + return null; + } + const parsed = Number.parseInt(text, 10); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +function firstMatch(text, patterns, map = (match) => match[1]) { + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const value = map(match); + if (value != null && value !== "") { + return value; + } + } + } + return null; +} + +function parseWinningPort(text) { + return firstMatch(text, [ + /\b(?:winning|selected|chosen)\s+(?:serial\s+)?port\s*[:=]\s*([^\s,;@]+)/i, + /\bport\s*[:=]\s*([^\s,;@]+)[^\n\r]{0,80}\bbaud\b/i, + /\b([/A-Za-z0-9_.:-]*(?:tty|cu|COM)[A-Za-z0-9_.:-]*)\s*@\s*\d{4,7}\b/i, + ], (match) => boundedSingleLine(match[1], 80)); +} + +function parseBaud(text) { + return firstMatch(text, [ + /\b(?:winning|selected|chosen)\s+(?:serial\s+)?port\s*[:=]\s*[^\s,;@]+(?:\s*(?:@|,|\bbaud\b)\s*(?:baud\s*)?)(\d{4,7})\b/i, + /\bbaud(?:rate)?\s*[:=]?\s*(\d{4,7})\b/i, + /\b[/A-Za-z0-9_.:-]*(?:tty|cu|COM)[A-Za-z0-9_.:-]*\s*@\s*(\d{4,7})\b/i, + ], (match) => parseIntegerText(match[1])); +} + +function parseLoginOk(text) { + return firstMatch(text, [ + /\blogin[_\s-]*ok\s*[:=]\s*(ok|pass|passed|true|yes|success|successful|fail|failed|false|no|not ok|blocked)\b/i, + /\blogin\s*[:=]\s*(ok|pass|passed|true|yes|success|successful|fail|failed|false|no|not ok|blocked)\b/i, + /\blogin\s+(ok|successful|failed)\b/i, + ], (match) => parseBooleanText(match[1])); +} + +function parseUname(text) { + return firstMatch(text, [ + /\b(?:kernel[_\s-]*)?uname(?:\s+-a)?\s*[:=]\s*([^\n\r]+)/i, + /(?:^|\n)\s*\$?\s*uname\s+-a\s*\r?\n\s*([^\n\r]+)/i, + ], (match) => boundedSingleLine(match[1], 240)); +} + +function parseXrtVersion(text) { + return firstMatch(text, [ + /\bxrt(?:[_\s-]*(?:version|build version))?\s*[:=]\s*([^\n\r]+)/i, + /\bxrt-smi[^\n\r]{0,80}\bversion\s*[:=]\s*([^\n\r]+)/i, + ], (match) => boundedSingleLine(match[1], 120)); +} + +function parseXmutilAppCount(text) { + return firstMatch(text, [ + /\bxmutil(?:\s+(?:app|apps|application|applications))?\s+count\s*[:=]\s*(\d+)\b/i, + /\bxmutil[^\n\r]{0,80}\b(?:app|apps|application|applications)\b[^\n\r]{0,40}\bcount\s*[:=]\s*(\d+)\b/i, + /\bxmutil[^\n\r]{0,80}\b(\d+)\s+(?:app|apps|application|applications)\b/i, + ], (match) => parseIntegerText(match[1])); +} + +function normalizePreflightTranscriptSummary(summary = DEFAULT_PREFLIGHT_TRANSCRIPT_SUMMARY) { + const errors = []; + if (!isObject(summary)) { + throw new Error("preflight transcript summary must be an object"); + } + const captured = summary.captured === true || summary.status === "captured"; + const normalized = { + status: captured ? "captured" : "not_captured", + captured, + winningPort: optionalStringField( + summary.winningPort, + "preflightTranscript.winningPort", + errors, + 80, + ), + baud: optionalNonNegativeIntegerField(summary.baud, "preflightTranscript.baud", errors), + loginOk: optionalBoolField(summary.loginOk, "preflightTranscript.loginOk", errors), + uname: optionalStringField(summary.uname, "preflightTranscript.uname", errors, 240), + xrtVersion: optionalStringField( + summary.xrtVersion, + "preflightTranscript.xrtVersion", + errors, + 120, + ), + xmutilAppCount: optionalNonNegativeIntegerField( + summary.xmutilAppCount, + "preflightTranscript.xmutilAppCount", + errors, + ), + message: optionalStringField(summary.message, "preflightTranscript.message", errors, 160), + }; + normalized.unameDisplay = captured + ? truncateText(normalized.uname, 80) + : DEFAULT_PREFLIGHT_TRANSCRIPT_SUMMARY.unameDisplay; + normalized.message = normalized.message ?? + (captured ? "preflight transcript captured" : DEFAULT_PREFLIGHT_TRANSCRIPT_SUMMARY.message); + assertSafeText(normalized, errors); + if (errors.length > 0) { + throw new Error(`invalid preflight transcript summary: ${errors.join("; ")}`); + } + return Object.freeze(normalized); +} + +export function parseKv260PreflightTranscript(text = "") { + const transcript = typeof text === "string" ? text : ""; + if (transcript.trim().length === 0) { + return normalizePreflightTranscriptSummary(); + } + const summary = { + status: "captured", + captured: true, + winningPort: parseWinningPort(transcript), + baud: parseBaud(transcript), + loginOk: parseLoginOk(transcript), + uname: parseUname(transcript), + xrtVersion: parseXrtVersion(transcript), + xmutilAppCount: parseXmutilAppCount(transcript), + message: "preflight transcript captured", + }; + if ( + summary.winningPort == null && + summary.baud == null && + summary.loginOk == null && + summary.uname == null && + summary.xrtVersion == null && + summary.xmutilAppCount == null + ) { + return normalizePreflightTranscriptSummary(); + } + return normalizePreflightTranscriptSummary(summary); +} + +function preflightTranscriptFromInput(inputs) { + if (typeof inputs.preflightTranscriptText === "string") { + return parseKv260PreflightTranscript(inputs.preflightTranscriptText); + } + return normalizePreflightTranscriptSummary( + inputs.preflightTranscript ?? inputs.preflightTranscriptSummary, + ); +} + function arrayField(value, path, errors) { if (!Array.isArray(value)) { addError(errors, path, "must be an array"); @@ -436,6 +631,7 @@ export function createPreflightProposal(launcherStatus, _traceManifest) { export function createKv260StatusPanel(inputs = {}) { const launcherStatus = LauncherStatusReader.consume(inputs.launcherStatus); const traceManifest = LabTraceReader.consume(inputs.traceManifest); + const preflightTranscript = preflightTranscriptFromInput(inputs); return Object.freeze({ version: KV260_STATUS_PANEL_VERSION, kind: "kv260-status-panel", @@ -468,6 +664,7 @@ export function createKv260StatusPanel(inputs = {}) { runbookRef: traceManifest.runbook_ref, }), preflight: createPreflightProposal(launcherStatus, traceManifest), + preflightTranscript, safety: Object.freeze({ dataOnly: true, readOnly: true, @@ -492,6 +689,10 @@ export function kv260StatusPanelJson(inputs = {}) { export function renderKv260StatusPanelHtml(panel = createKv260StatusPanel()) { const emptyState = panelEmptyState(panel); + const transcript = panel.preflightTranscript ?? DEFAULT_PREFLIGHT_TRANSCRIPT_SUMMARY; + const transcriptWinning = transcript.winningPort + ? `${transcript.winningPort}${transcript.baud == null ? "" : ` @ ${transcript.baud}`}` + : "not captured"; const checklistItems = panel.preflight.items.map((item) => { const status = statusPresentationForItem(item); const evidencePath = evidencePathForItem(item); @@ -604,6 +805,22 @@ export function renderKv260StatusPanelHtml(panel = createKv260StatusPanel()) { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); overflow-wrap: anywhere; } + .summary-card { + margin: 14px 0 18px; + padding: 12px; + border: 1px solid var(--panel-border); + background: var(--panel-soft); + } + .summary-card h2 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 650; + letter-spacing: 0; + } + .summary-card .summary-state { + margin: 0 0 10px; + color: var(--panel-muted); + } .checklist { list-style: none; margin: 0; @@ -702,6 +919,22 @@ export function renderKv260StatusPanelHtml(panel = createKv260StatusPanel()) { ${escapeHtml(`${panel.lab.sourceKind}; ${panel.lab.frameCount} frame(s)`)} +
+

Preflight Transcript

+

${escapeHtml(transcript.message)}

+
+
Winning port
+
${escapeHtml(transcriptWinning)}
+
Login OK
+
${escapeHtml(transcript.loginOk == null ? "not captured" : yesNo(transcript.loginOk))}
+
uname
+
${escapeHtml(transcript.unameDisplay)}
+
XRT version
+
${escapeHtml(transcript.xrtVersion || "not captured")}
+
xmutil apps
+
${escapeHtml(transcript.xmutilAppCount == null ? "not captured" : String(transcript.xmutilAppCount))}
+
+
    ${checklistItems}
@@ -731,6 +964,24 @@ export function formatKv260StatusPanel(panel = createKv260StatusPanel()) { `lab.frames: ${panel.lab.frameCount}`, `lab.axiBase: ${panel.lab.axiBase}`, `lab.isaVersion: ${panel.lab.isaVersion}`, + `preflightTranscript: ${panel.preflightTranscript.message}`, + `preflightTranscript.winning: ${ + panel.preflightTranscript.winningPort + ? `${panel.preflightTranscript.winningPort}${ + panel.preflightTranscript.baud == null ? "" : ` @ ${panel.preflightTranscript.baud}` + }` + : "not captured" + }`, + `preflightTranscript.loginOk: ${ + panel.preflightTranscript.loginOk == null ? "not captured" : yesNo(panel.preflightTranscript.loginOk) + }`, + `preflightTranscript.uname: ${panel.preflightTranscript.unameDisplay}`, + `preflightTranscript.xrtVersion: ${panel.preflightTranscript.xrtVersion || "not captured"}`, + `preflightTranscript.xmutilAppCount: ${ + panel.preflightTranscript.xmutilAppCount == null + ? "not captured" + : panel.preflightTranscript.xmutilAppCount + }`, "preflight:", ]; for (const item of panel.preflight.items) { diff --git a/editors/vscode-prototype/test/extension-config.test.mjs b/editors/vscode-prototype/test/extension-config.test.mjs index 6cabaa3..999c545 100644 --- a/editors/vscode-prototype/test/extension-config.test.mjs +++ b/editors/vscode-prototype/test/extension-config.test.mjs @@ -38,6 +38,13 @@ const REQUIRED_SETTINGS = new Map([ ["pccxSystemVerilog.defaultNavigationRoot", { type: "string", default: "fixtures/modules" }], ["pccxSystemVerilog.defaultModule", { type: "string", default: "simple_mod" }], ["pccxSystemVerilog.defaultDeclarationKind", { type: "string", default: "module" }], + [ + "pccxSystemVerilog.kv260.preflightTranscriptPath", + { + type: "string", + default: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, + ], ]); const LIVE_WORKSPACE_CONFIG = { @@ -114,6 +121,9 @@ function testDefaultConfig() { defaultNavigationRoot: "fixtures/modules", defaultModule: "simple_mod", defaultDeclarationKind: "module", + kv260: { + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, }); } @@ -170,6 +180,10 @@ function testNormalizeConfigRejectsInvalidSettings() { () => normalizeConfig({ defaultNavigationRoot: "fixtures/modules && whoami" }), /pccxSystemVerilog\.defaultNavigationRoot must not contain shell control syntax/, ); + assert.throws( + () => normalizeConfig({ kv260: { preflightTranscriptPath: "preflight.md; cat secret" } }), + /pccxSystemVerilog\.kv260\.preflightTranscriptPath must not contain shell control syntax/, + ); } function testFacadeArgsForKnownCommands() { diff --git a/editors/vscode-prototype/test/extension-entrypoint.test.mjs b/editors/vscode-prototype/test/extension-entrypoint.test.mjs index 29323e2..3f67d9f 100644 --- a/editors/vscode-prototype/test/extension-entrypoint.test.mjs +++ b/editors/vscode-prototype/test/extension-entrypoint.test.mjs @@ -30,6 +30,7 @@ import { createPresenterDeps, createCommandHandler, deactivate, + readKv260PreflightTranscriptSummary, readExtensionConfig, resolveCommandRequest, } from "../src/extension.mjs"; @@ -282,6 +283,9 @@ function testResolveCommandRequestUsesVsCodeSettings() { defaultNavigationRoot: "configured/modules", defaultModule: "pkg_defs", defaultDeclarationKind: "package", + kv260: { + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, }); assert.deepEqual( resolveCommandRequest("pccxSystemVerilog.runDiagnosticsLive", undefined, vscodeApi), @@ -310,6 +314,9 @@ function testResolveCommandRequestUsesVsCodeSettings() { defaultNavigationRoot: "configured/modules", defaultModule: "pkg_defs", defaultDeclarationKind: "package", + kv260: { + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, }, ); assert.deepEqual( @@ -339,6 +346,9 @@ function testResolveCommandRequestUsesVsCodeSettings() { defaultNavigationRoot: "configured/modules", defaultModule: "pkg_defs", defaultDeclarationKind: "package", + kv260: { + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, }, ); assert.deepEqual( @@ -368,6 +378,9 @@ function testResolveCommandRequestUsesVsCodeSettings() { defaultNavigationRoot: "configured/modules", defaultModule: "pkg_defs", defaultDeclarationKind: "package", + kv260: { + preflightTranscriptPath: "~/.codex/private-state/board-preflight/preflight-tty-2026-05-06.md", + }, }, ); } @@ -1550,7 +1563,13 @@ async function testPccxLabBackendStatusCommandReturnsStatusOnly() { } async function testKv260StatusPanelCommandReturnsDataOnlySurface() { - const handler = createCommandHandler(SHOW_KV260_STATUS_PANEL_COMMAND, null, {}); + const handler = createCommandHandler(SHOW_KV260_STATUS_PANEL_COMMAND, null, { + async kv260PreflightTranscriptReader() { + const error = new Error("missing"); + error.code = "ENOENT"; + throw error; + }, + }); const result = await handler(); @@ -1564,6 +1583,7 @@ async function testKv260StatusPanelCommandReturnsDataOnlySurface() { assert.equal(result.panel.safety.pccxLabExecution, false); assert.equal(result.panel.safety.shellExecution, false); assert.equal(result.panel.safety.sshExecution, false); + assert.equal(result.panel.preflightTranscript.message, "no preflight captured yet"); } async function testKv260StatusPanelCommandRendersExistingWebviewOnly() { @@ -1581,7 +1601,15 @@ async function testKv260StatusPanelCommandRendersExistingWebviewOnly() { showInformationMessage() {}, }, }; - const handler = createCommandHandler(SHOW_KV260_STATUS_PANEL_COMMAND, vscodeApi, {}); + const handler = createCommandHandler(SHOW_KV260_STATUS_PANEL_COMMAND, vscodeApi, { + kv260PreflightTranscriptText: [ + "selected port: /dev/ttyUSB2 baud 115200", + "login: ok", + "uname: Linux kv260 6.6.0-xilinx aarch64 GNU/Linux", + "xrt version: 2.16.204", + "xmutil app count: 4", + ].join("\n"), + }); const result = await handler(); @@ -1590,6 +1618,62 @@ async function testKv260StatusPanelCommandRendersExistingWebviewOnly() { assert.match(webviewPanel.webview.html, /class="aperture-mark"/); assert.match(webviewPanel.webview.html, /status-pill status-pending">PENDING/); assert.match(webviewPanel.webview.html, /
/); + assert.match(webviewPanel.webview.html, /\/dev\/ttyUSB2 @ 115200/); + assert.equal(result.panel.preflightTranscript.xmutilAppCount, 4); +} + +async function testKv260PreflightTranscriptPathIsConfigurableAndGraceful() { + const settings = new Map([ + ["kv260.preflightTranscriptPath", "~/private/preflight.md"], + ]); + const vscodeApi = { + workspace: { + getConfiguration() { + return { + get(key) { + return settings.get(key); + }, + }; + }, + }, + }; + const readCalls = []; + const summary = await readKv260PreflightTranscriptSummary( + readExtensionConfig(vscodeApi), + { + homeDir: "/tmp/home", + async kv260PreflightTranscriptReader(path, encoding) { + readCalls.push([path, encoding]); + return [ + "winning port: /dev/ttyUSB3 @ 921600", + "login_ok: yes", + "uname -a: Linux kv260 6.6.0-test aarch64 GNU/Linux", + "xrt version: 2.17.0", + "xmutil app count: 2", + ].join("\n"); + }, + }, + ); + const missing = await readKv260PreflightTranscriptSummary( + readExtensionConfig(vscodeApi), + { + homeDir: "/tmp/home", + async kv260PreflightTranscriptReader() { + const error = new Error("missing"); + error.code = "ENOENT"; + throw error; + }, + }, + ); + + assert.deepEqual(readCalls, [["/tmp/home/private/preflight.md", "utf8"]]); + assert.equal(summary.winningPort, "/dev/ttyUSB3"); + assert.equal(summary.baud, 921600); + assert.equal(summary.loginOk, true); + assert.equal(summary.xrtVersion, "2.17.0"); + assert.equal(summary.xmutilAppCount, 2); + assert.equal(missing.captured, false); + assert.equal(missing.message, "no preflight captured yet"); } testKnownFacadeArgs(); @@ -1622,5 +1706,6 @@ await testDiagnosticsHandoffSummaryCommandReturnsDataOnlySurface(); await testPccxLabBackendStatusCommandReturnsStatusOnly(); await testKv260StatusPanelCommandReturnsDataOnlySurface(); await testKv260StatusPanelCommandRendersExistingWebviewOnly(); +await testKv260PreflightTranscriptPathIsConfigurableAndGraceful(); console.log("vscode extension entrypoint tests ok"); diff --git a/editors/vscode-prototype/test/kv260-status-panel.test.mjs b/editors/vscode-prototype/test/kv260-status-panel.test.mjs index a9c1c49..86e4725 100644 --- a/editors/vscode-prototype/test/kv260-status-panel.test.mjs +++ b/editors/vscode-prototype/test/kv260-status-panel.test.mjs @@ -15,6 +15,7 @@ import { createKv260StatusPanel, formatKv260StatusPanel, kv260StatusPanelJson, + parseKv260PreflightTranscript, renderKv260StatusPanelHtml, } from "../src/kv260-status-panel.mjs"; @@ -75,6 +76,8 @@ async function testPanelRendersPreflightAndSafety() { assert.equal(panel.serialProbe.ttyPort, "/dev/ttyUSB0"); assert.equal(panel.serialProbe.xrtPresent, true); assert.equal(panel.serialProbe.lastPreflightAt, "2026-05-06T09:00:00Z"); + assert.equal(panel.preflightTranscript.captured, false); + assert.equal(panel.preflightTranscript.message, "no preflight captured yet"); assert.deepEqual(panel.preflight.items.map((item) => item.itemId), [ "serial_tty_port", "serial_login", @@ -99,10 +102,50 @@ async function testPreflightNotRunIsGracefulDefault() { const text = formatKv260StatusPanel(panel); assert.equal(panel.serialProbe.status, "not_run"); + assert.equal(panel.preflightTranscript.status, "not_captured"); assert.ok(panel.preflight.items.every((item) => item.state === "not_run")); assert.match(text, /serial\.ttyPort: preflight not run/); assert.match(text, /serial\.kernelUname: preflight not run/); assert.match(text, /serial\.xrtPresent: preflight not run/); + assert.match(text, /preflightTranscript: no preflight captured yet/); +} + +async function testParsesAndRendersPreflightTranscriptSummaryCard() { + const longUname = + "Linux kv260 6.6.0-xilinx-v2024.2 #1 SMP PREEMPT_DYNAMIC Wed May 6 09:00:00 UTC 2026 aarch64 GNU/Linux"; + const transcript = [ + "# KV260 board preflight", + "winning port: /dev/ttyUSB1 @ 115200", + "login_ok: true", + `uname -a: ${longUname}`, + "xrt version: 2.16.204", + "xmutil app count: 3", + "workspace note: /home/user/private-state/raw-capture.md", + ].join("\n"); + const parsed = parseKv260PreflightTranscript(transcript); + const panel = createKv260StatusPanel({ + launcherStatus: await readJson(LAUNCHER_FIXTURE), + traceManifest: await readJson(TRACE_FIXTURE), + preflightTranscriptText: transcript, + }); + const text = formatKv260StatusPanel(panel); + const html = renderKv260StatusPanelHtml(panel); + + assert.equal(parsed.captured, true); + assert.equal(parsed.winningPort, "/dev/ttyUSB1"); + assert.equal(parsed.baud, 115200); + assert.equal(parsed.loginOk, true); + assert.equal(parsed.xrtVersion, "2.16.204"); + assert.equal(parsed.xmutilAppCount, 3); + assert.equal(panel.preflightTranscript.unameDisplay.length, 80); + assert.match(text, /preflightTranscript\.winning: \/dev\/ttyUSB1 @ 115200/); + assert.match(text, /preflightTranscript\.loginOk: yes/); + assert.match(text, /preflightTranscript\.xrtVersion: 2\.16\.204/); + assert.match(text, /preflightTranscript\.xmutilAppCount: 3/); + assert.match(html, /Preflight Transcript/); + assert.match(html, /\/dev\/ttyUSB1 @ 115200/); + assert.match(html, /xmutil apps/); + assert.doesNotMatch(JSON.stringify(panel), /\/home\/user/); } async function testRendererUsesAperturePillsEvidenceAndEmptyState() { @@ -119,6 +162,7 @@ async function testRendererUsesAperturePillsEvidenceAndEmptyState() { assert.match(pendingHtml, /launcher serial preflight snapshot/); assert.match(pendingHtml, /launcher\.serial_probe\.tty_port/); assert.match(pendingHtml, /Launcher status input is not configured/); + assert.match(pendingHtml, /no preflight captured yet/); assert.doesNotMatch(pendingHtml, /\bAI\b|artificial intelligence/i); const blockedStatus = await readJson(LAUNCHER_FIXTURE); @@ -232,6 +276,7 @@ async function testModuleSourceHasNoExecutionTerms() { await testReadersParseFixtures(); await testPanelRendersPreflightAndSafety(); await testPreflightNotRunIsGracefulDefault(); +await testParsesAndRendersPreflightTranscriptSummaryCard(); await testRendererUsesAperturePillsEvidenceAndEmptyState(); await testRendererEscapesArtifactEvidence(); await testLiveSerialProbeTypeOnlySkipWithoutData();