Skip to content

Commit 4514141

Browse files
author
DavidQ
committed
Add V2 tool-to-tool launch actions with session handoff and executable validation - PR 11.213
1 parent b1434b2 commit 4514141

8 files changed

Lines changed: 302 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# PR_11_213 Report — V2 Tool -> Tool Launch Actions (Real UX)
2+
3+
## Actions Added Per Tool
4+
- `asset-browser-v2`
5+
- UI action: `Open in SVG Asset Studio V2`
6+
- Control: `#assetBrowserV2OpenSvgAssetStudioV2Button`
7+
- Target: `tools/svg-asset-studio-v2/index.html?hostContextId=<id>`
8+
9+
- `palette-manager-v2`
10+
- UI action: `Open in Vector Map Editor V2`
11+
- Control: `#paletteManagerOpenVectorMapEditorV2Button`
12+
- Target: `tools/vector-map-editor-v2/index.html?hostContextId=<id>`
13+
14+
- `tilemap-studio-v2`
15+
- UI action: `Open in Asset Browser V2`
16+
- Control: `#tilemapV2OpenAssetBrowserV2Button`
17+
- Target: `tools/asset-browser-v2/index.html?hostContextId=<id>`
18+
19+
All actions preserve `hostContextId`, do not mutate payload, and do not write fallback data.
20+
21+
## Flows Tested
22+
Runtime test: `tests/runtime/V2ToolActionFlow.test.mjs`
23+
24+
Validated:
25+
1. Fixture load for source tool.
26+
2. HostContextId generation and sessionStorage write simulation.
27+
3. Action URL construction to required target V2 path.
28+
4. HostContextId preservation in target URL.
29+
5. Target tool existence (`index.html`, `index.js`).
30+
6. Source and target JS syntax validity.
31+
7. Source payload not mutated during simulated action flow.
32+
33+
Result output:
34+
- `tmp/v2-tool-action-results.json`
35+
- Failures: `0`
36+
37+
## Pass/Fail
38+
- Asset Browser V2 -> SVG Asset Studio V2: **PASS**
39+
- Palette Manager V2 -> Vector Map Editor V2: **PASS**
40+
- Tilemap Studio V2 -> Asset Browser V2: **PASS**
41+
42+
## Files Changed
43+
- `tools/asset-browser-v2/index.html`
44+
- `tools/asset-browser-v2/index.js`
45+
- `tools/palette-manager-v2/index.html`
46+
- `tools/palette-manager-v2/index.js`
47+
- `tools/tilemap-studio-v2/index.html`
48+
- `tools/tilemap-studio-v2/index.js`
49+
- `tests/runtime/V2ToolActionFlow.test.mjs`
50+
- `docs/dev/reports/PR_11_213_report.md`
51+
52+
## Validation Commands Run
53+
1. `node --check tests/runtime/V2ToolActionFlow.test.mjs`
54+
- Result: **PASS**
55+
2. `node tests/runtime/V2ToolActionFlow.test.mjs`
56+
- Result: **PASS**
57+
3. `node --check tools/*-v2/index.js`
58+
- Result: **FAIL** in PowerShell wildcard expansion (`*` passed literally to Node)
59+
4. Equivalent per-tool syntax checks:
60+
- `node --check tools/asset-browser-v2/index.js`**PASS**
61+
- `node --check tools/palette-manager-v2/index.js`**PASS**
62+
- `node --check tools/svg-asset-studio-v2/index.js`**PASS**
63+
- `node --check tools/tilemap-studio-v2/index.js`**PASS**
64+
- `node --check tools/vector-map-editor-v2/index.js`**PASS**
65+
66+
## Fallback Confirmation
67+
- No fallback/default/demo data introduced.
68+
- No hidden sample loading introduced.
69+
- Session payload and key flow remain explicit and unchanged:
70+
- `hostContextId` from URL
71+
- `sessionStorage[hostContextId]`
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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 toolsRoot = path.join(repoRoot, "tools");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-tool-action-results.json");
13+
14+
const FLOWS = [
15+
{
16+
sourceToolId: "asset-browser-v2",
17+
targetToolId: "svg-asset-studio-v2",
18+
buttonId: "assetBrowserV2OpenSvgAssetStudioV2Button",
19+
targetUrlSnippet: "../svg-asset-studio-v2/index.html"
20+
},
21+
{
22+
sourceToolId: "palette-manager-v2",
23+
targetToolId: "vector-map-editor-v2",
24+
buttonId: "paletteManagerOpenVectorMapEditorV2Button",
25+
targetUrlSnippet: "../vector-map-editor-v2/index.html"
26+
},
27+
{
28+
sourceToolId: "tilemap-studio-v2",
29+
targetToolId: "asset-browser-v2",
30+
buttonId: "tilemapV2OpenAssetBrowserV2Button",
31+
targetUrlSnippet: "../asset-browser-v2/index.html"
32+
}
33+
];
34+
35+
class MemorySessionStorage {
36+
constructor() {
37+
this.values = new Map();
38+
}
39+
40+
setItem(key, value) {
41+
this.values.set(String(key), String(value));
42+
}
43+
44+
getItem(key) {
45+
if (!this.values.has(String(key))) {
46+
return null;
47+
}
48+
return this.values.get(String(key));
49+
}
50+
}
51+
52+
function readText(filePath) {
53+
return fs.readFileSync(filePath, "utf8");
54+
}
55+
56+
function readJson(filePath) {
57+
return JSON.parse(readText(filePath));
58+
}
59+
60+
function checkJsSyntax(jsPath) {
61+
try {
62+
execFileSync(process.execPath, ["--check", jsPath], {
63+
cwd: repoRoot,
64+
stdio: ["ignore", "pipe", "pipe"]
65+
});
66+
return { syntaxValid: true, syntaxError: "" };
67+
} catch (error) {
68+
return {
69+
syntaxValid: false,
70+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
71+
};
72+
}
73+
}
74+
75+
function generateHostContextId(sourceToolId) {
76+
const randomPart = Math.random().toString(36).slice(2, 10);
77+
return `${sourceToolId}-action-${Date.now()}-${randomPart}`;
78+
}
79+
80+
function validateFlow(flow) {
81+
const sourceFixturePath = path.join(fixturesRoot, `${flow.sourceToolId}.json`);
82+
const sourceHtmlPath = path.join(toolsRoot, flow.sourceToolId, "index.html");
83+
const sourceJsPath = path.join(toolsRoot, flow.sourceToolId, "index.js");
84+
const targetHtmlPath = path.join(toolsRoot, flow.targetToolId, "index.html");
85+
const targetJsPath = path.join(toolsRoot, flow.targetToolId, "index.js");
86+
const failures = [];
87+
88+
const sourceFixtureExists = fs.existsSync(sourceFixturePath);
89+
const sourceHtmlExists = fs.existsSync(sourceHtmlPath);
90+
const sourceJsExists = fs.existsSync(sourceJsPath);
91+
const targetHtmlExists = fs.existsSync(targetHtmlPath);
92+
const targetJsExists = fs.existsSync(targetJsPath);
93+
94+
let fixtureValid = false;
95+
let sourceSessionContext = null;
96+
if (!sourceFixtureExists) {
97+
failures.push("Source fixture is missing.");
98+
} else {
99+
try {
100+
const fixture = readJson(sourceFixturePath);
101+
fixtureValid = true;
102+
sourceSessionContext = fixture.sessionContext;
103+
} catch {
104+
fixtureValid = false;
105+
}
106+
if (!fixtureValid) failures.push("Source fixture JSON is invalid.");
107+
if (fixtureValid && (!sourceSessionContext || typeof sourceSessionContext !== "object" || Array.isArray(sourceSessionContext))) {
108+
failures.push("Source fixture sessionContext is missing or invalid.");
109+
}
110+
}
111+
112+
const hostContextId = generateHostContextId(flow.sourceToolId);
113+
const sessionStorageLike = new MemorySessionStorage();
114+
if (sourceSessionContext) {
115+
sessionStorageLike.setItem(hostContextId, JSON.stringify(sourceSessionContext));
116+
}
117+
118+
const actionUrl = new URL(`tools/${flow.targetToolId}/index.html`, "http://localhost/");
119+
actionUrl.searchParams.set("hostContextId", hostContextId);
120+
const builtUrl = actionUrl.pathname.slice(1) + actionUrl.search;
121+
const parsedHostContextId = actionUrl.searchParams.get("hostContextId");
122+
const storedValue = sessionStorageLike.getItem(hostContextId);
123+
const expectedStoredValue = sourceSessionContext ? JSON.stringify(sourceSessionContext) : "";
124+
125+
const sourceHtmlText = sourceHtmlExists ? readText(sourceHtmlPath) : "";
126+
const sourceJsText = sourceJsExists ? readText(sourceJsPath) : "";
127+
const { syntaxValid: sourceSyntaxValid, syntaxError: sourceSyntaxError } = checkJsSyntax(sourceJsPath);
128+
const { syntaxValid: targetSyntaxValid, syntaxError: targetSyntaxError } = checkJsSyntax(targetJsPath);
129+
130+
if (!sourceHtmlExists) failures.push("Source tool index.html is missing.");
131+
if (!sourceJsExists) failures.push("Source tool index.js is missing.");
132+
if (!targetHtmlExists) failures.push("Target tool index.html is missing.");
133+
if (!targetJsExists) failures.push("Target tool index.js is missing.");
134+
if (!sourceHtmlText.includes(`id="${flow.buttonId}"`)) failures.push("Required launch action control is missing in source tool HTML.");
135+
if (!sourceJsText.includes(flow.targetUrlSnippet)) failures.push("Source tool JS does not construct the required target route.");
136+
if (!sourceJsText.includes('searchParams.set("hostContextId", this.urlState.hostContextId);')) failures.push("Source tool JS does not preserve hostContextId in action URL.");
137+
if (!builtUrl.startsWith(`tools/${flow.targetToolId}/index.html?hostContextId=`)) failures.push("Built action URL does not target required V2 route.");
138+
if (parsedHostContextId !== hostContextId) failures.push("Action URL hostContextId was not preserved.");
139+
if (!storedValue) failures.push("Session storage entry for hostContextId is missing in simulated action flow.");
140+
if (storedValue !== expectedStoredValue) failures.push("Session payload was mutated during simulated action flow.");
141+
if (!sourceSyntaxValid) failures.push("Source tool index.js failed syntax check.");
142+
if (!targetSyntaxValid) failures.push("Target tool index.js failed syntax check.");
143+
144+
return {
145+
sourceTool: flow.sourceToolId,
146+
targetTool: flow.targetToolId,
147+
buttonId: flow.buttonId,
148+
sourceFixturePath: path.relative(repoRoot, sourceFixturePath).replace(/\\/g, "/"),
149+
sourceHtmlPath: path.relative(repoRoot, sourceHtmlPath).replace(/\\/g, "/"),
150+
sourceJsPath: path.relative(repoRoot, sourceJsPath).replace(/\\/g, "/"),
151+
targetHtmlPath: path.relative(repoRoot, targetHtmlPath).replace(/\\/g, "/"),
152+
targetJsPath: path.relative(repoRoot, targetJsPath).replace(/\\/g, "/"),
153+
sourceFixtureExists,
154+
fixtureValid,
155+
hostContextId,
156+
builtUrl,
157+
parsedHostContextId,
158+
storageEntryExists: Boolean(storedValue),
159+
sourceSyntaxValid,
160+
sourceSyntaxError,
161+
targetSyntaxValid,
162+
targetSyntaxError,
163+
failures
164+
};
165+
}
166+
167+
export function run() {
168+
const rows = FLOWS.map(validateFlow);
169+
const failures = rows.flatMap((row) => row.failures.map((entry) => `${row.sourceTool}->${row.targetTool}: ${entry}`));
170+
171+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
172+
fs.writeFileSync(resultsPath, `${JSON.stringify({
173+
generatedAt: new Date().toISOString(),
174+
flowCount: rows.length,
175+
failures,
176+
rows
177+
}, null, 2)}\n`, "utf8");
178+
179+
console.log(`v2 tool action results: ${resultsPath}`);
180+
assert.equal(failures.length, 0, `V2 tool action failures: ${failures.join(" | ")}`);
181+
return { flowCount: rows.length, failures, rows };
182+
}
183+
184+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
185+
try {
186+
const summary = run();
187+
console.log(JSON.stringify(summary, null, 2));
188+
} catch (error) {
189+
console.error(error);
190+
process.exitCode = 1;
191+
}
192+
}

tools/asset-browser-v2/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<aside class="asset-browser-v2-panel" data-menu-tool>
2020
<h3>menuTool</h3>
2121
<p id="assetBrowserV2ContractReadout">payloadJson.assetCatalog not loaded.</p>
22+
<button id="assetBrowserV2OpenSvgAssetStudioV2Button" type="button">Open in SVG Asset Studio V2</button>
2223
<div id="assetBrowserV2List" aria-label="Asset Browser V2 asset list"></div>
2324
</aside>
2425
<section class="asset-browser-v2-panel" aria-live="polite">

tools/asset-browser-v2/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ class AssetBrowserV2 {
44
document.title = "Asset Browser V2";
55
document.body.dataset.toolId = "asset-browser-v2";
66
this.urlState = this.readUrlState();
7+
this.openSvgAssetStudioV2 = this.openSvgAssetStudioV2.bind(this);
78
this.handleNavigationState = this.handleNavigationState.bind(this);
89
window.addEventListener("popstate", this.handleNavigationState);
910
window.addEventListener("pageshow", this.handleNavigationState);
11+
document.getElementById("assetBrowserV2OpenSvgAssetStudioV2Button").addEventListener("click", this.openSvgAssetStudioV2);
1012
this.readSession();
1113
}
1214

15+
openSvgAssetStudioV2() {
16+
if (!this.urlState.hostContextId) {
17+
this.renderMissing("No hostContextId is available for launch. Re-open Asset Browser V2 from a valid Tool V2 session link.");
18+
return;
19+
}
20+
const targetUrl = new URL("../svg-asset-studio-v2/index.html", window.location.href);
21+
targetUrl.searchParams.set("hostContextId", this.urlState.hostContextId);
22+
window.location.href = targetUrl.toString();
23+
}
24+
1325
handleNavigationState() {
1426
this.urlState = this.readUrlState();
1527
this.readSession();

tools/palette-manager-v2/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<aside class="palette-manager-v2-panel" data-menu-tool>
2020
<h3>menuTool</h3>
2121
<p id="paletteManagerContractReadout" class="palette-manager-v2-readout">paletteJson not loaded.</p>
22+
<button id="paletteManagerOpenVectorMapEditorV2Button" type="button">Open in Vector Map Editor V2</button>
2223
</aside>
2324
<section class="palette-manager-v2-panel" aria-live="polite">
2425
<h3 id="paletteManagerName">No palette loaded</h3>

tools/palette-manager-v2/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ class PaletteManagerV2 {
44
document.title = "Palette Manager V2";
55
document.body.dataset.toolId = "palette-manager-v2";
66
this.urlState = this.readUrlState();
7+
this.openVectorMapEditorV2 = this.openVectorMapEditorV2.bind(this);
78
this.handleNavigationState = this.handleNavigationState.bind(this);
89
window.addEventListener("popstate", this.handleNavigationState);
910
window.addEventListener("pageshow", this.handleNavigationState);
11+
document.getElementById("paletteManagerOpenVectorMapEditorV2Button").addEventListener("click", this.openVectorMapEditorV2);
1012
this.readSession();
1113
}
1214

15+
openVectorMapEditorV2() {
16+
if (!this.urlState.hostContextId) {
17+
this.renderMissing("No hostContextId is available for launch. Re-open Palette Manager V2 from a valid Tool V2 session link.");
18+
return;
19+
}
20+
const targetUrl = new URL("../vector-map-editor-v2/index.html", window.location.href);
21+
targetUrl.searchParams.set("hostContextId", this.urlState.hostContextId);
22+
window.location.href = targetUrl.toString();
23+
}
24+
1325
handleNavigationState() {
1426
this.urlState = this.readUrlState();
1527
this.readSession();

tools/tilemap-studio-v2/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<aside class="tilemap-v2-panel" data-menu-tool>
2020
<h3>menuTool</h3>
2121
<p id="tilemapV2ContractReadout">payloadJson.tileMapDocument not loaded.</p>
22+
<button id="tilemapV2OpenAssetBrowserV2Button" type="button">Open in Asset Browser V2</button>
2223
<ul id="tilemapV2LayerList" aria-label="Tilemap Studio V2 layers"></ul>
2324
</aside>
2425
<section class="tilemap-v2-panel" aria-live="polite">

tools/tilemap-studio-v2/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ class TilemapStudioV2 {
44
document.title = "Tilemap Studio V2";
55
document.body.dataset.toolId = "tilemap-studio-v2";
66
this.urlState = this.readUrlState();
7+
this.openAssetBrowserV2 = this.openAssetBrowserV2.bind(this);
78
this.handleNavigationState = this.handleNavigationState.bind(this);
89
window.addEventListener("popstate", this.handleNavigationState);
910
window.addEventListener("pageshow", this.handleNavigationState);
11+
document.getElementById("tilemapV2OpenAssetBrowserV2Button").addEventListener("click", this.openAssetBrowserV2);
1012
this.readSession();
1113
}
1214

15+
openAssetBrowserV2() {
16+
if (!this.urlState.hostContextId) {
17+
this.renderMissing("No hostContextId is available for launch. Re-open Tilemap Studio V2 from a valid Tool V2 session link.");
18+
return;
19+
}
20+
const targetUrl = new URL("../asset-browser-v2/index.html", window.location.href);
21+
targetUrl.searchParams.set("hostContextId", this.urlState.hostContextId);
22+
window.location.href = targetUrl.toString();
23+
}
24+
1325
handleNavigationState() {
1426
this.urlState = this.readUrlState();
1527
this.readSession();

0 commit comments

Comments
 (0)