Skip to content

Commit 9ae8f49

Browse files
author
DavidQ
committed
Add Workspace V2 diagnostics panel for session, URL, and storage inspection with executable validation - PR 11.223
1 parent 3dda241 commit 9ae8f49

4 files changed

Lines changed: 507 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# PR_11_223 Report - V2 Diagnostics Panel (Session + URL + Storage)
2+
3+
## Files Changed
4+
- `tools/workspace-v2/index.html`
5+
- `tools/workspace-v2/index.js`
6+
- `tests/runtime/V2Diagnostics.test.mjs`
7+
- `docs/dev/reports/PR_11_223_report.md`
8+
9+
## Diagnostics Panel Behavior
10+
Added a read-only **Diagnostics** panel in Workspace V2 with:
11+
- URL param display (`hostContextId` + additional params)
12+
- active `hostContextId`
13+
- active state classification (`EMPTY | INVALID | VALID`)
14+
- filtered `sessionStorage` entry preview for matching `hostContextId`
15+
- `localStorage` snapshots for:
16+
- `v2-session-library`
17+
- `v2-error-logs`
18+
- current loaded payload preview (truncated for safety)
19+
20+
Safety behavior:
21+
- diagnostics JSON parsing is guarded (`safeParseJson`)
22+
- malformed storage values are rendered as parse errors, never crash the panel
23+
- rendering is read-only and does not mutate session/system state
24+
25+
## Diagnostics Output Samples
26+
From `tmp/v2-diagnostics-results.json`:
27+
28+
Valid snapshot excerpt:
29+
```json
30+
{
31+
"activeHostContextId": "diag-host-1",
32+
"activeState": "VALID",
33+
"urlParams": {
34+
"hostContextId": "diag-host-1",
35+
"view": "inspector",
36+
"panel": "diagnostics"
37+
}
38+
}
39+
```
40+
41+
Malformed storage snapshot excerpt:
42+
```json
43+
{
44+
"activeState": "INVALID",
45+
"sessionMatches": [
46+
{
47+
"key": "diag-host-1",
48+
"parseOk": false
49+
}
50+
],
51+
"localStorage": {
52+
"errorLogs": {
53+
"parseOk": false
54+
}
55+
}
56+
}
57+
```
58+
59+
## Validation Results
60+
Commands run:
61+
1. `node --check tests/runtime/V2Diagnostics.test.mjs`
62+
Result: **PASS**
63+
2. `node tests/runtime/V2Diagnostics.test.mjs`
64+
Result: **PASS** (writes `tmp/v2-diagnostics-results.json`)
65+
3. `node --check tools/workspace-v2/index.js`
66+
Result: **PASS**
67+
68+
Runtime test coverage:
69+
- simulated URL with `hostContextId` and additional params
70+
- populated `sessionStorage` + `localStorage`
71+
- validated extraction/grouping/preview behavior
72+
- validated malformed JSON handling does not crash
73+
74+
## Behavior/Scope Confirmation
75+
- No Workspace V2 tool launch/render behavior changed.
76+
- Diagnostics are read-only and non-mutating.
77+
- No fallback/default/demo data introduced.
78+
- No schemas/samples/games/workspace-v1/platformShell/tools/shared files changed.
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
11+
const workspaceHtmlPath = path.join(repoRoot, "tools", "workspace-v2", "index.html");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-diagnostics-results.json");
13+
14+
class MemoryStorage {
15+
constructor() {
16+
this.values = new Map();
17+
}
18+
19+
getItem(key) {
20+
if (!this.values.has(String(key))) {
21+
return null;
22+
}
23+
return this.values.get(String(key));
24+
}
25+
26+
setItem(key, value) {
27+
this.values.set(String(key), String(value));
28+
}
29+
}
30+
31+
function readText(filePath) {
32+
return fs.readFileSync(filePath, "utf8");
33+
}
34+
35+
function checkJsSyntax(jsPath) {
36+
try {
37+
execFileSync(process.execPath, ["--check", jsPath], {
38+
cwd: repoRoot,
39+
stdio: ["ignore", "pipe", "pipe"]
40+
});
41+
return { syntaxValid: true, syntaxError: "" };
42+
} catch (error) {
43+
return {
44+
syntaxValid: false,
45+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
46+
};
47+
}
48+
}
49+
50+
function safeParseJson(rawValue) {
51+
if (typeof rawValue !== "string") {
52+
return { ok: false, value: null, error: "value is not a string" };
53+
}
54+
try {
55+
return { ok: true, value: JSON.parse(rawValue), error: "" };
56+
} catch (error) {
57+
return { ok: false, value: null, error: error instanceof Error ? error.message : "unknown error" };
58+
}
59+
}
60+
61+
function truncatePreview(value, maxLength) {
62+
const text = typeof value === "string" ? value : String(value);
63+
if (text.length <= maxLength) {
64+
return text;
65+
}
66+
return `${text.slice(0, maxLength)} ...truncated (${text.length - maxLength} more chars)`;
67+
}
68+
69+
function isValidSessionPayload(sessionPayload) {
70+
return Boolean(sessionPayload && typeof sessionPayload === "object" && !Array.isArray(sessionPayload));
71+
}
72+
73+
function diagnosticsActiveHostContextId(url, currentHostContextId) {
74+
const params = new URL(url).searchParams;
75+
const urlHostContextId = typeof params.get("hostContextId") === "string" ? params.get("hostContextId").trim() : "";
76+
if (urlHostContextId) {
77+
return urlHostContextId;
78+
}
79+
if (typeof currentHostContextId === "string" && currentHostContextId.trim()) {
80+
return currentHostContextId.trim();
81+
}
82+
return "";
83+
}
84+
85+
function diagnosticsActiveState(activeHostContextId, sessionStorageLike, currentSessionPayload) {
86+
if (activeHostContextId) {
87+
const stored = sessionStorageLike.getItem(activeHostContextId);
88+
if (!stored) {
89+
return "EMPTY";
90+
}
91+
const parsed = safeParseJson(stored);
92+
if (!parsed.ok || !isValidSessionPayload(parsed.value)) {
93+
return "INVALID";
94+
}
95+
return "VALID";
96+
}
97+
if (isValidSessionPayload(currentSessionPayload)) {
98+
return "VALID";
99+
}
100+
return "EMPTY";
101+
}
102+
103+
function readDiagnosticsSnapshot(url, currentHostContextId, currentSessionPayload, sessionStorageLike, localStorageLike) {
104+
const params = new URL(url).searchParams;
105+
const urlParams = {};
106+
params.forEach((value, key) => {
107+
urlParams[key] = value;
108+
});
109+
110+
const activeHostContextId = diagnosticsActiveHostContextId(url, currentHostContextId);
111+
const sessionMatches = [];
112+
if (activeHostContextId) {
113+
const rawSessionValue = sessionStorageLike.getItem(activeHostContextId);
114+
if (typeof rawSessionValue === "string") {
115+
const parsedSession = safeParseJson(rawSessionValue);
116+
sessionMatches.push({
117+
key: activeHostContextId,
118+
parseOk: parsedSession.ok,
119+
error: parsedSession.ok ? "" : parsedSession.error,
120+
preview: truncatePreview(rawSessionValue, 500)
121+
});
122+
}
123+
}
124+
125+
const sessionLibraryRaw = localStorageLike.getItem("v2-session-library");
126+
const sessionLibraryParsed = safeParseJson(typeof sessionLibraryRaw === "string" ? sessionLibraryRaw : "");
127+
const errorLogsRaw = localStorageLike.getItem("v2-error-logs");
128+
const errorLogsParsed = safeParseJson(typeof errorLogsRaw === "string" ? errorLogsRaw : "");
129+
const payloadPreview = isValidSessionPayload(currentSessionPayload)
130+
? truncatePreview(JSON.stringify(currentSessionPayload, null, 2), 800)
131+
: "No payload loaded.";
132+
133+
return {
134+
urlParams,
135+
activeHostContextId,
136+
activeState: diagnosticsActiveState(activeHostContextId, sessionStorageLike, currentSessionPayload),
137+
sessionMatches,
138+
localStorage: {
139+
sessionLibrary: {
140+
exists: typeof sessionLibraryRaw === "string",
141+
parseOk: typeof sessionLibraryRaw === "string" ? sessionLibraryParsed.ok : false,
142+
error: typeof sessionLibraryRaw === "string" && !sessionLibraryParsed.ok ? sessionLibraryParsed.error : "",
143+
preview: typeof sessionLibraryRaw === "string" ? truncatePreview(sessionLibraryRaw, 800) : "missing"
144+
},
145+
errorLogs: {
146+
exists: typeof errorLogsRaw === "string",
147+
parseOk: typeof errorLogsRaw === "string" ? errorLogsParsed.ok : false,
148+
error: typeof errorLogsRaw === "string" && !errorLogsParsed.ok ? errorLogsParsed.error : "",
149+
preview: typeof errorLogsRaw === "string" ? truncatePreview(errorLogsRaw, 800) : "missing"
150+
}
151+
},
152+
payloadPreview
153+
};
154+
}
155+
156+
export function run() {
157+
const failures = [];
158+
const workspaceJsExists = fs.existsSync(workspaceJsPath);
159+
const workspaceHtmlExists = fs.existsSync(workspaceHtmlPath);
160+
const workspaceJsText = workspaceJsExists ? readText(workspaceJsPath) : "";
161+
const workspaceHtmlText = workspaceHtmlExists ? readText(workspaceHtmlPath) : "";
162+
const { syntaxValid, syntaxError } = checkJsSyntax(workspaceJsPath);
163+
164+
const htmlHasDiagnosticsPanel = workspaceHtmlText.includes("workspaceV2RefreshDiagnosticsButton") &&
165+
workspaceHtmlText.includes("workspaceV2DiagnosticsUrlParams") &&
166+
workspaceHtmlText.includes("workspaceV2DiagnosticsSessionStorage") &&
167+
workspaceHtmlText.includes("workspaceV2DiagnosticsSessionLibrary") &&
168+
workspaceHtmlText.includes("workspaceV2DiagnosticsErrorLogs") &&
169+
workspaceHtmlText.includes("workspaceV2DiagnosticsPayload");
170+
171+
const jsHasReadDiagnostics = workspaceJsText.includes("readDiagnosticsSnapshot()");
172+
const jsHasRenderDiagnostics = workspaceJsText.includes("renderDiagnosticsPanel()");
173+
const jsHasActiveState = workspaceJsText.includes("diagnosticsActiveState(activeHostContextId)");
174+
const jsHasSafeParse = workspaceJsText.includes("safeParseJson(rawValue)");
175+
const jsHasTruncate = workspaceJsText.includes("truncatePreview(value, maxLength)");
176+
177+
if (!workspaceJsExists) failures.push("Missing tools/workspace-v2/index.js.");
178+
if (!workspaceHtmlExists) failures.push("Missing tools/workspace-v2/index.html.");
179+
if (!syntaxValid) failures.push("tools/workspace-v2/index.js failed syntax check.");
180+
if (!htmlHasDiagnosticsPanel) failures.push("Diagnostics panel markup is missing required nodes.");
181+
if (!jsHasReadDiagnostics) failures.push("Workspace V2 is missing readDiagnosticsSnapshot().");
182+
if (!jsHasRenderDiagnostics) failures.push("Workspace V2 is missing renderDiagnosticsPanel().");
183+
if (!jsHasActiveState) failures.push("Workspace V2 is missing diagnosticsActiveState(activeHostContextId).");
184+
if (!jsHasSafeParse) failures.push("Workspace V2 is missing safeParseJson(rawValue).");
185+
if (!jsHasTruncate) failures.push("Workspace V2 is missing truncatePreview(value, maxLength).");
186+
187+
const sessionStorageLike = new MemoryStorage();
188+
const localStorageLike = new MemoryStorage();
189+
const hostContextId = "diag-host-1";
190+
const url = `https://example.test/tools/workspace-v2/index.html?hostContextId=${encodeURIComponent(hostContextId)}&view=inspector&panel=diagnostics`;
191+
const payload = {
192+
toolId: "asset-browser-v2",
193+
payloadJson: {
194+
assetCatalog: {
195+
name: "Diagnostics Fixture",
196+
entries: [{ id: "asset-1", label: "Hero", kind: "svg", path: "assets/hero.svg" }]
197+
}
198+
}
199+
};
200+
201+
sessionStorageLike.setItem(hostContextId, JSON.stringify(payload));
202+
localStorageLike.setItem("v2-session-library", JSON.stringify({ baseline: payload }));
203+
localStorageLike.setItem("v2-error-logs", "{not-valid-json");
204+
205+
let validSnapshot = null;
206+
let invalidSnapshot = null;
207+
let emptySnapshot = null;
208+
try {
209+
validSnapshot = readDiagnosticsSnapshot(url, "", payload, sessionStorageLike, localStorageLike);
210+
sessionStorageLike.setItem(hostContextId, "{bad-json");
211+
invalidSnapshot = readDiagnosticsSnapshot(url, "", payload, sessionStorageLike, localStorageLike);
212+
sessionStorageLike.setItem(hostContextId, "");
213+
emptySnapshot = readDiagnosticsSnapshot(url, "", null, sessionStorageLike, localStorageLike);
214+
} catch (error) {
215+
failures.push(`Diagnostics snapshot logic crashed: ${error instanceof Error ? error.message : "unknown error"}`);
216+
}
217+
218+
if (validSnapshot) {
219+
if (validSnapshot.urlParams.hostContextId !== hostContextId) failures.push("hostContextId URL param was not extracted correctly.");
220+
if (validSnapshot.urlParams.view !== "inspector") failures.push("Additional URL params were not extracted correctly.");
221+
if (validSnapshot.activeHostContextId !== hostContextId) failures.push("Active hostContextId was not detected.");
222+
if (validSnapshot.activeState !== "VALID") failures.push(`Expected VALID active state, got ${validSnapshot.activeState}.`);
223+
if (validSnapshot.sessionMatches.length !== 1) failures.push(`Expected one matching sessionStorage entry, got ${validSnapshot.sessionMatches.length}.`);
224+
if (!validSnapshot.localStorage.sessionLibrary.parseOk) failures.push("Expected valid parse for v2-session-library.");
225+
if (validSnapshot.localStorage.errorLogs.parseOk) failures.push("Expected malformed v2-error-logs to report parse failure.");
226+
if (!validSnapshot.payloadPreview.includes("assetCatalog")) failures.push("Payload preview is missing expected payload content.");
227+
}
228+
229+
if (invalidSnapshot) {
230+
if (invalidSnapshot.activeState !== "INVALID") failures.push(`Expected INVALID active state for malformed session JSON, got ${invalidSnapshot.activeState}.`);
231+
if (invalidSnapshot.sessionMatches.length !== 1 || invalidSnapshot.sessionMatches[0].parseOk !== false) {
232+
failures.push("Expected malformed session entry preview to report parseOk=false.");
233+
}
234+
}
235+
236+
if (emptySnapshot) {
237+
if (emptySnapshot.activeState !== "EMPTY") failures.push(`Expected EMPTY active state for missing session entry, got ${emptySnapshot.activeState}.`);
238+
}
239+
240+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
241+
fs.writeFileSync(resultsPath, `${JSON.stringify({
242+
generatedAt: new Date().toISOString(),
243+
failures,
244+
workspaceChecks: {
245+
workspaceJsExists,
246+
workspaceHtmlExists,
247+
syntaxValid,
248+
syntaxError,
249+
htmlHasDiagnosticsPanel,
250+
jsHasReadDiagnostics,
251+
jsHasRenderDiagnostics,
252+
jsHasActiveState,
253+
jsHasSafeParse,
254+
jsHasTruncate
255+
},
256+
snapshots: {
257+
valid: validSnapshot,
258+
invalid: invalidSnapshot,
259+
empty: emptySnapshot
260+
}
261+
}, null, 2)}\n`, "utf8");
262+
263+
console.log(`v2 diagnostics results: ${resultsPath}`);
264+
assert.equal(failures.length, 0, `V2 diagnostics failures: ${failures.join(" | ")}`);
265+
return { failures, validSnapshot, invalidSnapshot, emptySnapshot };
266+
}
267+
268+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
269+
try {
270+
const summary = run();
271+
console.log(JSON.stringify(summary, null, 2));
272+
} catch (error) {
273+
console.error(error);
274+
process.exitCode = 1;
275+
}
276+
}

tools/workspace-v2/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ <h2>Error Viewer</h2>
8080
<div id="workspaceV2ErrorLogsList" aria-label="Workspace V2 error logs"></div>
8181
</section>
8282

83+
<section class="hub-panel">
84+
<h2>Diagnostics</h2>
85+
<p>Read-only diagnostics for URL/session/local storage state.</p>
86+
<button id="workspaceV2RefreshDiagnosticsButton" type="button">Refresh Diagnostics</button>
87+
<p><strong>Active State:</strong> <span id="workspaceV2DiagnosticsActiveState">EMPTY</span></p>
88+
<p><strong>Active hostContextId:</strong> <span id="workspaceV2DiagnosticsHostContextId">none</span></p>
89+
<h3>URL Params</h3>
90+
<pre id="workspaceV2DiagnosticsUrlParams">{}</pre>
91+
<h3>Session Storage (hostContextId match)</h3>
92+
<pre id="workspaceV2DiagnosticsSessionStorage">[]</pre>
93+
<h3>Local Storage: v2-session-library</h3>
94+
<pre id="workspaceV2DiagnosticsSessionLibrary">null</pre>
95+
<h3>Local Storage: v2-error-logs</h3>
96+
<pre id="workspaceV2DiagnosticsErrorLogs">null</pre>
97+
<h3>Current Payload Preview</h3>
98+
<pre id="workspaceV2DiagnosticsPayload">No payload loaded.</pre>
99+
</section>
100+
83101
<section class="hub-panel">
84102
<h2>Session Output</h2>
85103
<pre id="workspaceV2Status">No fixture loaded.</pre>

0 commit comments

Comments
 (0)