Skip to content

Commit e26fa97

Browse files
author
DavidQ
committed
Establish Workspace V2 as default entry and validate end-to-end flow to V2 tools - PR 11.215
1 parent 2576dc5 commit e26fa97

4 files changed

Lines changed: 242 additions & 1 deletion

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# PR_11_215 Report — V2 Entry Landing (Workspace V2 as Default)
2+
3+
## Entry Behavior
4+
- Updated `tools/index.html` to provide a prominent default V2 entry section:
5+
- heading: `Default V2 Entry`
6+
- primary action: `Open Workspace V2`
7+
- route: `./workspace-v2/index.html`
8+
- This establishes Workspace V2 as the clear launch surface for V2 flows.
9+
10+
## Workspace V2 UX Adjustment
11+
- Minor copy update in `tools/workspace-v2/index.html` clarifies Workspace V2 as the default V2 entry and explicit session/URL wiring surface.
12+
13+
## Flow Validation
14+
Runtime test: `tests/runtime/V2EntryFlow.test.mjs`
15+
16+
Validated:
17+
1. `tools/index.html` exists.
18+
2. Workspace V2 route exists from index (`./workspace-v2/index.html`) and explicit `Open Workspace V2` action exists.
19+
3. Workspace producer launch wiring includes:
20+
- `hostContextId` query parameter
21+
- `fromTool=workspace-v2`
22+
4. Simulated `workspace -> tool launch` for all V2 tools:
23+
- route path valid
24+
- `hostContextId` present and preserved
25+
- target tool route exists (`index.html`, `index.js`)
26+
- no broken paths
27+
28+
Tools validated:
29+
- `asset-browser-v2`
30+
- `palette-manager-v2`
31+
- `svg-asset-studio-v2`
32+
- `tilemap-studio-v2`
33+
- `vector-map-editor-v2`
34+
35+
## Pass/Fail
36+
- `node --check tests/runtime/V2EntryFlow.test.mjs` -> **PASS**
37+
- `node tests/runtime/V2EntryFlow.test.mjs` -> **PASS**
38+
- `node --check tools/workspace-v2/index.js` -> **PASS**
39+
40+
Runtime output:
41+
- `tmp/v2-entry-flow-results.json`
42+
- failures: `0`
43+
44+
## Files Changed
45+
- `tools/index.html`
46+
- `tools/workspace-v2/index.html`
47+
- `tests/runtime/V2EntryFlow.test.mjs`
48+
- `docs/dev/reports/PR_11_215_report.md`
49+
50+
## No Fallback Confirmation
51+
- No auto-loaded data introduced.
52+
- No hidden sample loading introduced.
53+
- No legacy routing introduced in this PR scope.
54+
- Launch flow remains explicit and URL/session driven.

tests/runtime/V2EntryFlow.test.mjs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 toolsRoot = path.join(repoRoot, "tools");
11+
const fixturesRoot = path.join(repoRoot, "tests", "fixtures", "v2-tools");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-entry-flow-results.json");
13+
14+
const TOOL_IDS = [
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+
function readText(filePath) {
23+
return fs.readFileSync(filePath, "utf8");
24+
}
25+
26+
function readJson(filePath) {
27+
return JSON.parse(readText(filePath));
28+
}
29+
30+
function checkJsSyntax(jsPath) {
31+
try {
32+
execFileSync(process.execPath, ["--check", jsPath], {
33+
cwd: repoRoot,
34+
stdio: ["ignore", "pipe", "pipe"]
35+
});
36+
return { syntaxValid: true, syntaxError: "" };
37+
} catch (error) {
38+
return {
39+
syntaxValid: false,
40+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
41+
};
42+
}
43+
}
44+
45+
function generateHostContextId(toolId) {
46+
const randomPart = Math.random().toString(36).slice(2, 10);
47+
return `${toolId}-entry-flow-${Date.now()}-${randomPart}`;
48+
}
49+
50+
function validateEntrySurface() {
51+
const indexPath = path.join(toolsRoot, "index.html");
52+
const workspacePath = path.join(toolsRoot, "workspace-v2", "index.html");
53+
const workspaceJsPath = path.join(toolsRoot, "workspace-v2", "index.js");
54+
const failures = [];
55+
56+
const indexExists = fs.existsSync(indexPath);
57+
const workspaceExists = fs.existsSync(workspacePath);
58+
const workspaceJsExists = fs.existsSync(workspaceJsPath);
59+
const indexHtml = indexExists ? readText(indexPath) : "";
60+
const workspaceHtml = workspaceExists ? readText(workspacePath) : "";
61+
const workspaceJs = workspaceJsExists ? readText(workspaceJsPath) : "";
62+
const { syntaxValid: workspaceSyntaxValid, syntaxError: workspaceSyntaxError } = checkJsSyntax(workspaceJsPath);
63+
64+
if (!indexExists) failures.push("tools/index.html is missing.");
65+
if (!workspaceExists) failures.push("tools/workspace-v2/index.html is missing.");
66+
if (!workspaceJsExists) failures.push("tools/workspace-v2/index.js is missing.");
67+
if (!indexHtml.includes("Open Workspace V2")) failures.push("tools/index.html does not expose 'Open Workspace V2' entry action.");
68+
if (!indexHtml.includes("./workspace-v2/index.html")) failures.push("tools/index.html does not link to workspace-v2 route.");
69+
if (!workspaceHtml.includes("Workspace V2 Session Producer")) failures.push("workspace-v2 landing title is missing.");
70+
if (!workspaceJs.includes("sessionStorage.setItem(hostContextId, JSON.stringify(payload));")) failures.push("workspace-v2 does not create session storage entry.");
71+
if (!workspaceJs.includes('toolUrl.searchParams.set("hostContextId", hostContextId);')) failures.push("workspace-v2 launch URL does not include hostContextId.");
72+
if (!workspaceJs.includes('toolUrl.searchParams.set("fromTool", "workspace-v2");')) failures.push("workspace-v2 launch URL does not include fromTool=workspace-v2.");
73+
if (!workspaceSyntaxValid) failures.push("workspace-v2/index.js failed syntax check.");
74+
75+
return {
76+
indexPath: path.relative(repoRoot, indexPath).replace(/\\/g, "/"),
77+
workspacePath: path.relative(repoRoot, workspacePath).replace(/\\/g, "/"),
78+
workspaceJsPath: path.relative(repoRoot, workspaceJsPath).replace(/\\/g, "/"),
79+
indexExists,
80+
workspaceExists,
81+
workspaceJsExists,
82+
workspaceSyntaxValid,
83+
workspaceSyntaxError,
84+
failures
85+
};
86+
}
87+
88+
function validateWorkspaceToToolRoute(toolId) {
89+
const fixturePath = path.join(fixturesRoot, `${toolId}.json`);
90+
const toolHtmlPath = path.join(toolsRoot, toolId, "index.html");
91+
const toolJsPath = path.join(toolsRoot, toolId, "index.js");
92+
const failures = [];
93+
94+
const fixtureExists = fs.existsSync(fixturePath);
95+
const toolHtmlExists = fs.existsSync(toolHtmlPath);
96+
const toolJsExists = fs.existsSync(toolJsPath);
97+
let fixtureValid = false;
98+
let sessionContext = null;
99+
100+
if (!fixtureExists) {
101+
failures.push("Fixture missing.");
102+
} else {
103+
try {
104+
const fixture = readJson(fixturePath);
105+
fixtureValid = true;
106+
sessionContext = fixture.sessionContext;
107+
} catch {
108+
fixtureValid = false;
109+
}
110+
if (!fixtureValid) failures.push("Fixture JSON invalid.");
111+
if (fixtureValid && (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext))) {
112+
failures.push("Fixture sessionContext missing/invalid.");
113+
}
114+
}
115+
116+
const hostContextId = generateHostContextId(toolId);
117+
const launchUrl = new URL(`tools/${toolId}/index.html`, "http://localhost/");
118+
launchUrl.searchParams.set("hostContextId", hostContextId);
119+
launchUrl.searchParams.set("fromTool", "workspace-v2");
120+
const parsedHostContextId = launchUrl.searchParams.get("hostContextId");
121+
const launchHref = launchUrl.pathname.slice(1) + launchUrl.search;
122+
const toolRouteExists = toolHtmlExists && toolJsExists;
123+
const { syntaxValid, syntaxError } = checkJsSyntax(toolJsPath);
124+
125+
if (!toolRouteExists) failures.push("Tool route is broken (missing index.html or index.js).");
126+
if (!launchHref.startsWith(`tools/${toolId}/index.html?hostContextId=`)) failures.push("Launch route format is invalid.");
127+
if (parsedHostContextId !== hostContextId) failures.push("hostContextId was not preserved in launch URL.");
128+
if (!syntaxValid) failures.push("Tool index.js failed syntax check.");
129+
130+
return {
131+
tool: toolId,
132+
fixturePath: path.relative(repoRoot, fixturePath).replace(/\\/g, "/"),
133+
toolHtmlPath: path.relative(repoRoot, toolHtmlPath).replace(/\\/g, "/"),
134+
toolJsPath: path.relative(repoRoot, toolJsPath).replace(/\\/g, "/"),
135+
fixtureExists,
136+
fixtureValid,
137+
toolRouteExists,
138+
hostContextId,
139+
launchHref,
140+
parsedHostContextId,
141+
syntaxValid,
142+
syntaxError,
143+
failures
144+
};
145+
}
146+
147+
export function run() {
148+
const entrySurface = validateEntrySurface();
149+
const toolRows = TOOL_IDS.map(validateWorkspaceToToolRoute);
150+
const failures = [...entrySurface.failures];
151+
toolRows.forEach((row) => row.failures.forEach((entry) => failures.push(`${row.tool}: ${entry}`)));
152+
153+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
154+
fs.writeFileSync(resultsPath, `${JSON.stringify({
155+
generatedAt: new Date().toISOString(),
156+
toolCount: toolRows.length,
157+
failures,
158+
entrySurface,
159+
toolRows
160+
}, null, 2)}\n`, "utf8");
161+
162+
console.log(`v2 entry flow results: ${resultsPath}`);
163+
assert.equal(failures.length, 0, `V2 entry flow failures: ${failures.join(" | ")}`);
164+
return { toolCount: toolRows.length, failures, entrySurface, toolRows };
165+
}
166+
167+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
168+
try {
169+
const summary = run();
170+
console.log(JSON.stringify(summary, null, 2));
171+
} catch (error) {
172+
console.error(error);
173+
process.exitCode = 1;
174+
}
175+
}

tools/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@
2828
<p class="subtitle">* Use Open Tool for normal day-to-day use, cleanest behavior, and quickest direct debugging.</p>
2929
</section>
3030

31+
<section>
32+
<h2>Default V2 Entry</h2>
33+
<div class="card">
34+
<h3><a href="./workspace-v2/index.html">Workspace V2</a></h3>
35+
<p>Default launch surface for V2 flows. Create a host session, pick a V2 tool, and open with explicit hostContextId wiring.</p>
36+
<div class="meta">
37+
<a class="tools-platform-card__action" href="./workspace-v2/index.html">Open Workspace V2</a>
38+
</div>
39+
</div>
40+
<hr />
41+
</section>
42+
3143
<section>
3244
<h2>Workspace Manager</h2>
3345
<div class="grid" data-workspace-manager-grid></div>

tools/workspace-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<main class="hub-page">
1313
<section class="hub-panel">
1414
<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>
15+
<p>Default V2 entry: create a hostContext session from a fixture and launch a V2 tool with explicit URL/session wiring.</p>
1616
<p id="workspaceV2Breadcrumb">Workspace V2</p>
1717
<button id="workspaceV2BackButton" type="button">Back to Tools Index</button>
1818
</section>

0 commit comments

Comments
 (0)