Skip to content

Commit 329544a

Browse files
author
DavidQ
committed
Add workspace-based session producer to launch V2 tools with real session data - PR 11.211
1 parent 67a82e6 commit 329544a

4 files changed

Lines changed: 379 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# PR_11_211 Report — V2 Session Producer (Workspace → Tools)
2+
3+
## Producer Behavior
4+
Added a new minimal V2 producer at `tools/workspace-v2/`:
5+
- UI allows selecting one of the five V2 tools.
6+
- Fixture load step reads `tests/fixtures/v2-tools/<tool>.json`.
7+
- Launch step:
8+
1. generates a new `hostContextId`
9+
2. writes `sessionStorage.setItem(hostContextId, JSON.stringify(payload))`
10+
3. navigates to `tools/<tool>-v2/index.html?hostContextId=<id>` (tool-relative URL generated from workspace path)
11+
12+
## Tools Launched
13+
- `asset-browser-v2`
14+
- `palette-manager-v2`
15+
- `svg-asset-studio-v2`
16+
- `tilemap-studio-v2`
17+
- `vector-map-editor-v2`
18+
19+
## Session Creation Verification
20+
Runtime test: `tests/runtime/V2SessionProducer.test.mjs`
21+
- Generates `hostContextId` per tool.
22+
- Writes fixture `sessionContext` to storage with generated key.
23+
- Builds launch URL with `hostContextId` query.
24+
- Verifies:
25+
- URL contains expected `hostContextId`
26+
- storage entry exists for generated key
27+
- stored payload parses as JSON object
28+
- target tool `index.js` syntax is valid
29+
- producer script syntax is valid
30+
31+
Output generated:
32+
- `tmp/v2-session-producer-results.json`
33+
- failures: `0`
34+
35+
## Files Changed
36+
- `tools/workspace-v2/index.html`
37+
- `tools/workspace-v2/index.js`
38+
- `tests/runtime/V2SessionProducer.test.mjs`
39+
- `docs/dev/reports/PR_11_211_report.md`
40+
41+
## Validation Commands Run
42+
1. `node --check tests/runtime/V2SessionProducer.test.mjs`
43+
- Result: **PASS**
44+
2. `node tests/runtime/V2SessionProducer.test.mjs`
45+
- Result: **PASS**
46+
3. `node --check tools/workspace-v2/index.js`
47+
- Result: **PASS**
48+
49+
## Fallback Confirmation
50+
- No fallback/default/demo data added.
51+
- Producer uses explicit fixture payload + generated `hostContextId` only.
52+
- No alternate session source guessing was introduced.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 fixturesRoot = path.join(repoRoot, "tests", "fixtures", "v2-tools");
11+
const workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-session-producer-results.json");
13+
14+
const TOOLS = [
15+
"asset-browser-v2",
16+
"palette-manager-v2",
17+
"svg-asset-studio-v2",
18+
"tilemap-studio-v2",
19+
"vector-map-editor-v2"
20+
];
21+
22+
class MemorySessionStorage {
23+
constructor() {
24+
this.values = new Map();
25+
}
26+
27+
setItem(key, value) {
28+
this.values.set(String(key), String(value));
29+
}
30+
31+
getItem(key) {
32+
if (!this.values.has(String(key))) {
33+
return null;
34+
}
35+
return this.values.get(String(key));
36+
}
37+
}
38+
39+
function readText(filePath) {
40+
return fs.readFileSync(filePath, "utf8");
41+
}
42+
43+
function readJson(filePath) {
44+
return JSON.parse(readText(filePath));
45+
}
46+
47+
function checkJsSyntax(jsPath) {
48+
try {
49+
execFileSync(process.execPath, ["--check", jsPath], {
50+
cwd: repoRoot,
51+
stdio: ["ignore", "pipe", "pipe"]
52+
});
53+
return { syntaxValid: true, syntaxError: "" };
54+
} catch (error) {
55+
return {
56+
syntaxValid: false,
57+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
58+
};
59+
}
60+
}
61+
62+
function generateHostContextId(toolId) {
63+
const randomPart = Math.random().toString(36).slice(2, 10);
64+
return `${toolId}-producer-${Date.now()}-${randomPart}`;
65+
}
66+
67+
function buildToolUrl(toolId, hostContextId) {
68+
return `tools/${toolId}/index.html?hostContextId=${encodeURIComponent(hostContextId)}`;
69+
}
70+
71+
function validateTool(toolId) {
72+
const fixturePath = path.join(fixturesRoot, `${toolId}.json`);
73+
const toolJsPath = path.join(repoRoot, "tools", toolId, "index.js");
74+
const toolHtmlPath = path.join(repoRoot, "tools", toolId, "index.html");
75+
const failures = [];
76+
const fixtureExists = fs.existsSync(fixturePath);
77+
const toolJsExists = fs.existsSync(toolJsPath);
78+
const toolHtmlExists = fs.existsSync(toolHtmlPath);
79+
let fixtureValid = false;
80+
let fixtureSessionContext = null;
81+
82+
if (!fixtureExists) {
83+
failures.push("Fixture file is missing.");
84+
} else {
85+
try {
86+
const fixture = readJson(fixturePath);
87+
fixtureValid = true;
88+
fixtureSessionContext = fixture.sessionContext;
89+
} catch {
90+
fixtureValid = false;
91+
}
92+
if (!fixtureValid) failures.push("Fixture JSON is invalid.");
93+
if (fixtureValid && (!fixtureSessionContext || typeof fixtureSessionContext !== "object" || Array.isArray(fixtureSessionContext))) {
94+
failures.push("Fixture sessionContext is missing or invalid.");
95+
}
96+
}
97+
98+
const hostContextId = generateHostContextId(toolId);
99+
const sessionStorageLike = new MemorySessionStorage();
100+
if (fixtureSessionContext) {
101+
sessionStorageLike.setItem(hostContextId, JSON.stringify(fixtureSessionContext));
102+
}
103+
104+
const launchUrl = buildToolUrl(toolId, hostContextId);
105+
const parsedLaunchUrl = new URL(launchUrl, "http://localhost/");
106+
const parsedHostContextId = parsedLaunchUrl.searchParams.get("hostContextId");
107+
const storedValue = sessionStorageLike.getItem(hostContextId);
108+
const storedPayloadParseable = (() => {
109+
if (!storedValue) return false;
110+
try {
111+
const parsed = JSON.parse(storedValue);
112+
return Boolean(parsed && typeof parsed === "object" && !Array.isArray(parsed));
113+
} catch {
114+
return false;
115+
}
116+
})();
117+
118+
const { syntaxValid: toolSyntaxValid, syntaxError: toolSyntaxError } = checkJsSyntax(toolJsPath);
119+
120+
if (!toolHtmlExists) failures.push("Target tool index.html is missing.");
121+
if (!toolJsExists) failures.push("Target tool index.js is missing.");
122+
if (!launchUrl.includes(`tools/${toolId}/index.html?hostContextId=`)) failures.push("Launch URL does not match expected V2 path format.");
123+
if (parsedHostContextId !== hostContextId) failures.push("Launch URL hostContextId does not match generated value.");
124+
if (!storedValue) failures.push("Session storage entry was not written.");
125+
if (!storedPayloadParseable) failures.push("Session storage entry is not valid serialized JSON payload.");
126+
if (!toolSyntaxValid) failures.push("Target tool index.js failed syntax check.");
127+
128+
return {
129+
tool: toolId,
130+
fixturePath: path.relative(repoRoot, fixturePath).replace(/\\/g, "/"),
131+
toolHtmlPath: path.relative(repoRoot, toolHtmlPath).replace(/\\/g, "/"),
132+
toolJsPath: path.relative(repoRoot, toolJsPath).replace(/\\/g, "/"),
133+
fixtureExists,
134+
fixtureValid,
135+
hostContextId,
136+
launchUrl,
137+
parsedHostContextId,
138+
storageEntryExists: Boolean(storedValue),
139+
storedPayloadParseable,
140+
toolSyntaxValid,
141+
toolSyntaxError,
142+
failures
143+
};
144+
}
145+
146+
export function run() {
147+
const workspaceJsText = fs.existsSync(workspaceJsPath) ? readText(workspaceJsPath) : "";
148+
const { syntaxValid: workspaceSyntaxValid, syntaxError: workspaceSyntaxError } = checkJsSyntax(workspaceJsPath);
149+
const producerChecks = {
150+
workspaceJsExists: fs.existsSync(workspaceJsPath),
151+
usesSessionStorageSetItem: workspaceJsText.includes("sessionStorage.setItem(hostContextId, JSON.stringify(payload));"),
152+
setsHostContextIdInUrl: workspaceJsText.includes('toolUrl.searchParams.set("hostContextId", hostContextId);'),
153+
hasFixtureLoaderPath: workspaceJsText.includes("../../tests/fixtures/v2-tools/"),
154+
workspaceSyntaxValid,
155+
workspaceSyntaxError
156+
};
157+
158+
const rows = TOOLS.map(validateTool);
159+
const failures = [];
160+
if (!producerChecks.workspaceJsExists) failures.push("workspace-v2/index.js is missing.");
161+
if (!producerChecks.usesSessionStorageSetItem) failures.push("workspace-v2/index.js does not write session storage with hostContextId key.");
162+
if (!producerChecks.setsHostContextIdInUrl) failures.push("workspace-v2/index.js does not add hostContextId query parameter.");
163+
if (!producerChecks.hasFixtureLoaderPath) failures.push("workspace-v2/index.js does not reference v2 fixture path.");
164+
if (!producerChecks.workspaceSyntaxValid) failures.push("workspace-v2/index.js failed syntax check.");
165+
rows.forEach((row) => {
166+
row.failures.forEach((entry) => failures.push(`${row.tool}: ${entry}`));
167+
});
168+
169+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
170+
fs.writeFileSync(resultsPath, `${JSON.stringify({
171+
generatedAt: new Date().toISOString(),
172+
toolCount: rows.length,
173+
producerChecks,
174+
failures,
175+
rows
176+
}, null, 2)}\n`, "utf8");
177+
178+
console.log(`v2 session producer results: ${resultsPath}`);
179+
assert.equal(failures.length, 0, `V2 session producer failures: ${failures.join(" | ")}`);
180+
return { toolCount: rows.length, producerChecks, failures, rows };
181+
}
182+
183+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
184+
try {
185+
const summary = run();
186+
console.log(JSON.stringify(summary, null, 2));
187+
} catch (error) {
188+
console.error(error);
189+
process.exitCode = 1;
190+
}
191+
}

tools/workspace-v2/index.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>Workspace V2</title>
7+
<link rel="stylesheet" href="../../src/engine/theme/main.css" />
8+
<link rel="stylesheet" href="../../src/engine/ui/hubCommon.css" />
9+
</head>
10+
<body data-tool-id="workspace-v2">
11+
<div id="shared-theme-header"></div>
12+
<main class="hub-page">
13+
<section class="hub-panel">
14+
<h1>Workspace V2 Session Producer</h1>
15+
<p>Create a hostContext session from a V2 fixture and launch a V2 tool with that context.</p>
16+
</section>
17+
18+
<section class="hub-panel">
19+
<h2>Producer</h2>
20+
<label for="workspaceV2ToolSelect">Tool</label>
21+
<select id="workspaceV2ToolSelect">
22+
<option value="asset-browser-v2">Asset Browser V2</option>
23+
<option value="palette-manager-v2">Palette Manager V2</option>
24+
<option value="svg-asset-studio-v2">SVG Asset Studio V2</option>
25+
<option value="tilemap-studio-v2">Tilemap Studio V2</option>
26+
<option value="vector-map-editor-v2">Vector Map Editor V2</option>
27+
</select>
28+
<div>
29+
<button id="workspaceV2LoadFixtureButton" type="button">Load Fixture</button>
30+
<button id="workspaceV2LaunchButton" type="button">Create Session + Launch</button>
31+
</div>
32+
</section>
33+
34+
<section class="hub-panel">
35+
<h2>Session Output</h2>
36+
<pre id="workspaceV2Status">No fixture loaded.</pre>
37+
</section>
38+
</main>
39+
<script type="module" src="../../src/engine/theme/mount-shared-header.js"></script>
40+
<script type="module" src="./index.js"></script>
41+
</body>
42+
</html>

tools/workspace-v2/index.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
class WorkspaceV2SessionProducer {
2+
constructor() {
3+
document.title = "Workspace V2";
4+
document.body.dataset.toolId = "workspace-v2";
5+
this.toolSelect = document.getElementById("workspaceV2ToolSelect");
6+
this.loadFixtureButton = document.getElementById("workspaceV2LoadFixtureButton");
7+
this.launchButton = document.getElementById("workspaceV2LaunchButton");
8+
this.statusNode = document.getElementById("workspaceV2Status");
9+
this.loadedFixture = null;
10+
this.loadedToolId = "";
11+
this.loadFixtureButton.addEventListener("click", () => {
12+
this.loadSelectedFixture();
13+
});
14+
this.launchButton.addEventListener("click", () => {
15+
this.createSessionAndLaunch();
16+
});
17+
}
18+
19+
selectedToolId() {
20+
return typeof this.toolSelect.value === "string" ? this.toolSelect.value.trim() : "";
21+
}
22+
23+
fixturePathForTool(toolId) {
24+
return `../../tests/fixtures/v2-tools/${toolId}.json`;
25+
}
26+
27+
async loadSelectedFixture() {
28+
const toolId = this.selectedToolId();
29+
if (!toolId) {
30+
this.statusNode.textContent = "Select a V2 tool before loading a fixture.";
31+
return;
32+
}
33+
try {
34+
const response = await fetch(this.fixturePathForTool(toolId), { cache: "no-store" });
35+
if (!response.ok) {
36+
this.statusNode.textContent = `Fixture read failed (${response.status}). Expected ${this.fixturePathForTool(toolId)}.`;
37+
this.loadedFixture = null;
38+
this.loadedToolId = "";
39+
return;
40+
}
41+
const fixture = await response.json();
42+
if (!fixture || typeof fixture !== "object" || Array.isArray(fixture)) {
43+
this.statusNode.textContent = "Fixture is invalid. Expected a JSON object with hostContextId and sessionContext.";
44+
this.loadedFixture = null;
45+
this.loadedToolId = "";
46+
return;
47+
}
48+
if (typeof fixture.sessionContext !== "object" || !fixture.sessionContext || Array.isArray(fixture.sessionContext)) {
49+
this.statusNode.textContent = "Fixture is invalid. Missing sessionContext object.";
50+
this.loadedFixture = null;
51+
this.loadedToolId = "";
52+
return;
53+
}
54+
this.loadedFixture = fixture;
55+
this.loadedToolId = toolId;
56+
this.statusNode.textContent = `Fixture loaded for ${toolId}.\nReady to create session and launch.`;
57+
} catch (error) {
58+
this.loadedFixture = null;
59+
this.loadedToolId = "";
60+
this.statusNode.textContent = `Fixture read failed: ${error instanceof Error ? error.message : "unknown error"}`;
61+
}
62+
}
63+
64+
createHostContextId(toolId) {
65+
const randomPart = Math.random().toString(36).slice(2, 10);
66+
return `${toolId}-${Date.now()}-${randomPart}`;
67+
}
68+
69+
buildToolLaunchUrl(toolId, hostContextId) {
70+
const toolUrl = new URL(`../${toolId}/index.html`, window.location.href);
71+
toolUrl.searchParams.set("hostContextId", hostContextId);
72+
return toolUrl.toString();
73+
}
74+
75+
createSessionAndLaunch() {
76+
const toolId = this.selectedToolId();
77+
if (!toolId) {
78+
this.statusNode.textContent = "Select a V2 tool before launch.";
79+
return;
80+
}
81+
if (!this.loadedFixture || this.loadedToolId !== toolId) {
82+
this.statusNode.textContent = "Load fixture for the selected tool before launch.";
83+
return;
84+
}
85+
const hostContextId = this.createHostContextId(toolId);
86+
const payload = this.loadedFixture.sessionContext;
87+
sessionStorage.setItem(hostContextId, JSON.stringify(payload));
88+
const launchUrl = this.buildToolLaunchUrl(toolId, hostContextId);
89+
this.statusNode.textContent = `Session created.\nTool: ${toolId}\nHostContextId: ${hostContextId}\nURL: tools/${toolId}/index.html?hostContextId=${hostContextId}`;
90+
window.location.href = launchUrl;
91+
}
92+
}
93+
94+
new WorkspaceV2SessionProducer();

0 commit comments

Comments
 (0)