Skip to content

Commit a175ccc

Browse files
author
DavidQ
committed
Add version tag to V2 session payloads with strict validation and forward compatibility guard - PR 11.226
1 parent 6fcdcba commit a175ccc

13 files changed

Lines changed: 345 additions & 4 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# PR_11_226 Report - V2 Session Version Tag (Forward Compatibility)
2+
3+
## Files Changed
4+
- `tests/fixtures/v2-tools/asset-browser-v2.json`
5+
- `tests/fixtures/v2-tools/palette-manager-v2.json`
6+
- `tests/fixtures/v2-tools/svg-asset-studio-v2.json`
7+
- `tests/fixtures/v2-tools/tilemap-studio-v2.json`
8+
- `tests/fixtures/v2-tools/vector-map-editor-v2.json`
9+
- `tools/workspace-v2/index.js`
10+
- `tools/asset-browser-v2/index.js`
11+
- `tools/palette-manager-v2/index.js`
12+
- `tools/svg-asset-studio-v2/index.js`
13+
- `tools/tilemap-studio-v2/index.js`
14+
- `tools/vector-map-editor-v2/index.js`
15+
- `tests/runtime/V2SessionVersion.test.mjs`
16+
- `docs/dev/reports/PR_11_226_report.md`
17+
18+
## Version Checks Implemented
19+
Producer behavior:
20+
- Workspace V2 now tags produced sessions with:
21+
- `version: "v2"`
22+
- Version tagging is applied before storage/write in session creation flows.
23+
24+
Tool behavior:
25+
- All V2 tools now validate:
26+
- `sessionContext.version === "v2"`
27+
- On mismatch:
28+
- tool enters INVALID path
29+
- message includes:
30+
- `Unsupported session version`
31+
32+
Fixtures:
33+
- Updated all V2 tool fixtures under `tests/fixtures/v2-tools/*` to include:
34+
- `sessionContext.version: "v2"`
35+
36+
## Runtime Test Coverage
37+
`tests/runtime/V2SessionVersion.test.mjs` validates per tool:
38+
1. valid version (`"v2"`) -> `VALID`
39+
2. missing version -> `INVALID`
40+
3. wrong version (`"v3"`) -> `INVALID`
41+
42+
Output:
43+
- `tmp/v2-session-version-results.json`
44+
45+
## Validation Results
46+
Commands run:
47+
1. `node --check tests/runtime/V2SessionVersion.test.mjs`
48+
Result: **PASS**
49+
2. `node tests/runtime/V2SessionVersion.test.mjs`
50+
Result: **PASS**
51+
3. `node --check tools/*-v2/index.js`
52+
Result: **FAIL** on Windows/Node wildcard resolution (`MODULE_NOT_FOUND` for literal `tools\\*-v2\\index.js`)
53+
4. Equivalent explicit per-file `node --check` sweep for `tools/*-v2/index.js`
54+
Result: **PASS** for all detected V2 tool JS files
55+
56+
## No Fallback Confirmation
57+
- No auto-upgrade path in tool readers.
58+
- No silent version ignore path.
59+
- No fallback/default/demo data introduced.

tests/fixtures/v2-tools/asset-browser-v2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"hostContextId": "asset-browser-v2-fixture",
33
"sessionContext": {
4+
"version": "v2",
45
"toolId": "asset-browser-v2",
56
"payloadJson": {
67
"assetCatalog": {

tests/fixtures/v2-tools/palette-manager-v2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"hostContextId": "palette-manager-v2-fixture",
33
"sessionContext": {
4+
"version": "v2",
45
"toolId": "palette-manager-v2",
56
"paletteJson": {
67
"name": "Fixture Palette",

tests/fixtures/v2-tools/svg-asset-studio-v2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"hostContextId": "svg-asset-studio-v2-fixture",
33
"sessionContext": {
4+
"version": "v2",
45
"toolId": "svg-asset-studio-v2",
56
"payloadJson": {
67
"vectorAssetDocument": {

tests/fixtures/v2-tools/tilemap-studio-v2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"hostContextId": "tilemap-studio-v2-fixture",
33
"sessionContext": {
4+
"version": "v2",
45
"toolId": "tilemap-studio-v2",
56
"payloadJson": {
67
"tileMapDocument": {

tests/fixtures/v2-tools/vector-map-editor-v2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"hostContextId": "vector-map-editor-v2-fixture",
33
"sessionContext": {
4+
"version": "v2",
45
"toolId": "vector-map-editor-v2",
56
"payloadJson": {
67
"vectorMapDocument": {
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
13+
const resultsPath = path.join(repoRoot, "tmp", "v2-session-version-results.json");
14+
15+
const TOOLS = [
16+
"asset-browser-v2",
17+
"palette-manager-v2",
18+
"svg-asset-studio-v2",
19+
"tilemap-studio-v2",
20+
"vector-map-editor-v2"
21+
];
22+
23+
function readText(filePath) {
24+
return fs.readFileSync(filePath, "utf8");
25+
}
26+
27+
function readJson(filePath) {
28+
return JSON.parse(readText(filePath));
29+
}
30+
31+
function checkJsSyntax(jsPath) {
32+
try {
33+
execFileSync(process.execPath, ["--check", jsPath], {
34+
cwd: repoRoot,
35+
stdio: ["ignore", "pipe", "pipe"]
36+
});
37+
return { syntaxValid: true, syntaxError: "" };
38+
} catch (error) {
39+
return {
40+
syntaxValid: false,
41+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
42+
};
43+
}
44+
}
45+
46+
function cloneJson(value) {
47+
return JSON.parse(JSON.stringify(value));
48+
}
49+
50+
function hasValidPayload(toolId, sessionContext) {
51+
if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) {
52+
return false;
53+
}
54+
if (sessionContext.version !== "v2") {
55+
return false;
56+
}
57+
58+
if (toolId === "asset-browser-v2") {
59+
const catalog = sessionContext?.payloadJson?.assetCatalog;
60+
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) return false;
61+
if (typeof catalog.name !== "string" || !catalog.name.trim()) return false;
62+
if (!Array.isArray(catalog.entries)) return false;
63+
if (catalog.entries.some((entry) =>
64+
!entry ||
65+
typeof entry !== "object" ||
66+
Array.isArray(entry) ||
67+
typeof entry.id !== "string" ||
68+
!entry.id.trim() ||
69+
typeof entry.label !== "string" ||
70+
!entry.label.trim() ||
71+
typeof entry.kind !== "string" ||
72+
!entry.kind.trim() ||
73+
typeof entry.path !== "string" ||
74+
!entry.path.trim()
75+
)) return false;
76+
return true;
77+
}
78+
79+
if (toolId === "palette-manager-v2") {
80+
const palette = sessionContext?.paletteJson;
81+
if (!palette || typeof palette !== "object" || Array.isArray(palette)) return false;
82+
if (typeof palette.name !== "string" || !palette.name.trim()) return false;
83+
if (!Array.isArray(palette.colors)) return false;
84+
for (const colorEntry of palette.colors) {
85+
let colorValue = "";
86+
if (typeof colorEntry === "string") colorValue = colorEntry.trim().toUpperCase();
87+
if (colorEntry && typeof colorEntry === "object" && !Array.isArray(colorEntry) && typeof colorEntry.hex === "string") colorValue = colorEntry.hex.trim().toUpperCase();
88+
if (colorEntry && typeof colorEntry === "object" && !Array.isArray(colorEntry) && typeof colorEntry.color === "string") colorValue = colorEntry.color.trim().toUpperCase();
89+
if (!/^#([0-9A-F]{6}|[0-9A-F]{8})$/.test(colorValue)) return false;
90+
}
91+
return true;
92+
}
93+
94+
if (toolId === "svg-asset-studio-v2") {
95+
const vectorAsset = sessionContext?.payloadJson?.vectorAssetDocument;
96+
if (!vectorAsset || typeof vectorAsset !== "object" || Array.isArray(vectorAsset)) return false;
97+
if (typeof vectorAsset.sourceName !== "string" || !vectorAsset.sourceName.trim()) return false;
98+
if (typeof vectorAsset.svgText !== "string" || !/^\s*<svg[\s>]/i.test(vectorAsset.svgText)) return false;
99+
return true;
100+
}
101+
102+
if (toolId === "tilemap-studio-v2") {
103+
const tileMap = sessionContext?.payloadJson?.tileMapDocument;
104+
if (!tileMap || typeof tileMap !== "object" || Array.isArray(tileMap)) return false;
105+
if (!tileMap.map || typeof tileMap.map !== "object" || Array.isArray(tileMap.map)) return false;
106+
if (typeof tileMap.map.name !== "string" || !tileMap.map.name.trim()) return false;
107+
if (!Number.isFinite(Number(tileMap.map.width)) || Number(tileMap.map.width) <= 0) return false;
108+
if (!Number.isFinite(Number(tileMap.map.height)) || Number(tileMap.map.height) <= 0) return false;
109+
if (!Array.isArray(tileMap.layers)) return false;
110+
if (tileMap.layers.some((entry) =>
111+
!entry ||
112+
typeof entry !== "object" ||
113+
Array.isArray(entry) ||
114+
typeof entry.name !== "string" ||
115+
!entry.name.trim() ||
116+
typeof entry.kind !== "string" ||
117+
!entry.kind.trim() ||
118+
!Array.isArray(entry.data)
119+
)) return false;
120+
return true;
121+
}
122+
123+
if (toolId === "vector-map-editor-v2") {
124+
const map = sessionContext?.payloadJson?.vectorMapDocument;
125+
if (!map || typeof map !== "object" || Array.isArray(map)) return false;
126+
if (typeof map.name !== "string" || !map.name.trim()) return false;
127+
if (!Number.isFinite(Number(map.width)) || Number(map.width) <= 0) return false;
128+
if (!Number.isFinite(Number(map.height)) || Number(map.height) <= 0) return false;
129+
if (typeof map.background !== "string" || !map.background.trim()) return false;
130+
if (!Array.isArray(map.objects)) return false;
131+
return true;
132+
}
133+
134+
return false;
135+
}
136+
137+
function validateTool(toolId) {
138+
const fixturePath = path.join(fixturesRoot, `${toolId}.json`);
139+
const jsPath = path.join(toolsRoot, toolId, "index.js");
140+
const failures = [];
141+
const fixtureExists = fs.existsSync(fixturePath);
142+
const jsExists = fs.existsSync(jsPath);
143+
const jsText = jsExists ? readText(jsPath) : "";
144+
const { syntaxValid, syntaxError } = checkJsSyntax(jsPath);
145+
146+
let fixtureValid = false;
147+
let validContext = null;
148+
if (!fixtureExists) {
149+
failures.push("Missing fixture file.");
150+
} else {
151+
try {
152+
const fixture = readJson(fixturePath);
153+
fixtureValid = true;
154+
validContext = fixture.sessionContext;
155+
} catch {
156+
fixtureValid = false;
157+
}
158+
}
159+
if (!fixtureValid) {
160+
failures.push("Fixture is invalid JSON.");
161+
}
162+
163+
const hasVersionCheckInTool = jsText.includes('sessionContext.version !== "v2"');
164+
const hasUnsupportedMessage = jsText.includes("Unsupported session version");
165+
if (!jsExists) failures.push("Missing tool index.js.");
166+
if (!syntaxValid) failures.push("Tool index.js failed syntax check.");
167+
if (!hasVersionCheckInTool) failures.push("Tool is missing explicit sessionContext.version check.");
168+
if (!hasUnsupportedMessage) failures.push("Tool is missing 'Unsupported session version' message.");
169+
170+
let validState = "INVALID";
171+
let missingVersionState = "INVALID";
172+
let wrongVersionState = "INVALID";
173+
if (validContext) {
174+
validState = hasValidPayload(toolId, validContext) ? "VALID" : "INVALID";
175+
const missingVersionContext = cloneJson(validContext);
176+
delete missingVersionContext.version;
177+
missingVersionState = hasValidPayload(toolId, missingVersionContext) ? "VALID" : "INVALID";
178+
const wrongVersionContext = cloneJson(validContext);
179+
wrongVersionContext.version = "v3";
180+
wrongVersionState = hasValidPayload(toolId, wrongVersionContext) ? "VALID" : "INVALID";
181+
}
182+
183+
if (validState !== "VALID") failures.push(`Expected VALID with version=v2, got ${validState}.`);
184+
if (missingVersionState !== "INVALID") failures.push(`Expected INVALID with missing version, got ${missingVersionState}.`);
185+
if (wrongVersionState !== "INVALID") failures.push(`Expected INVALID with wrong version, got ${wrongVersionState}.`);
186+
187+
return {
188+
tool: toolId,
189+
fixturePath: path.relative(repoRoot, fixturePath).replace(/\\/g, "/"),
190+
jsPath: path.relative(repoRoot, jsPath).replace(/\\/g, "/"),
191+
fixtureExists,
192+
fixtureValid,
193+
syntaxValid,
194+
syntaxError,
195+
hasVersionCheckInTool,
196+
hasUnsupportedMessage,
197+
cases: {
198+
validVersion: validState,
199+
missingVersion: missingVersionState,
200+
wrongVersion: wrongVersionState
201+
},
202+
failures
203+
};
204+
}
205+
206+
export function run() {
207+
const workspaceJsExists = fs.existsSync(workspaceJsPath);
208+
const workspaceJsText = workspaceJsExists ? readText(workspaceJsPath) : "";
209+
const workspaceSyntax = checkJsSyntax(workspaceJsPath);
210+
const workspaceHasVersionTagMethod = workspaceJsText.includes("withSessionVersion(sessionPayload)");
211+
const workspaceHasVersionTagValue = workspaceJsText.includes('version: "v2"');
212+
213+
const rows = TOOLS.map(validateTool);
214+
const failures = rows.flatMap((row) => row.failures.map((entry) => `${row.tool}: ${entry}`));
215+
if (!workspaceJsExists) failures.push("workspace-v2: Missing index.js.");
216+
if (!workspaceSyntax.syntaxValid) failures.push("workspace-v2: index.js failed syntax check.");
217+
if (!workspaceHasVersionTagMethod) failures.push("workspace-v2: Missing withSessionVersion(sessionPayload) producer tag method.");
218+
if (!workspaceHasVersionTagValue) failures.push("workspace-v2: Missing version='v2' producer tag value.");
219+
220+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
221+
fs.writeFileSync(resultsPath, `${JSON.stringify({
222+
generatedAt: new Date().toISOString(),
223+
failures,
224+
workspaceChecks: {
225+
workspaceJsExists,
226+
syntaxValid: workspaceSyntax.syntaxValid,
227+
syntaxError: workspaceSyntax.syntaxError,
228+
workspaceHasVersionTagMethod,
229+
workspaceHasVersionTagValue
230+
},
231+
rows
232+
}, null, 2)}\n`, "utf8");
233+
234+
console.log(`v2 session version results: ${resultsPath}`);
235+
assert.equal(failures.length, 0, `V2 session version failures: ${failures.join(" | ")}`);
236+
return { failures, rows };
237+
}
238+
239+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
240+
try {
241+
const summary = run();
242+
console.log(JSON.stringify(summary, null, 2));
243+
} catch (error) {
244+
console.error(error);
245+
process.exitCode = 1;
246+
}
247+
}

tools/asset-browser-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ class AssetBrowserV2 {
127127
this.renderError("Session context is invalid. Expected an object containing payloadJson.assetCatalog.");
128128
return;
129129
}
130+
if (sessionContext.version !== "v2") {
131+
this.renderError("Unsupported session version");
132+
return;
133+
}
130134
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
131135
this.renderError("Asset Browser V2 session data is invalid. Expected payloadJson only.");
132136
return;

tools/palette-manager-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ class PaletteManagerV2 {
127127
this.renderError("Session context is invalid. Expected an object containing paletteJson.");
128128
return;
129129
}
130+
if (sessionContext.version !== "v2") {
131+
this.renderError("Unsupported session version");
132+
return;
133+
}
130134
if (!sessionContext.paletteJson || typeof sessionContext.paletteJson !== "object" || Array.isArray(sessionContext.paletteJson)) {
131135
this.renderError("Palette Manager V2 session data is invalid. Expected paletteJson only.");
132136
return;

tools/svg-asset-studio-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ class SvgAssetStudioV2 {
116116
this.renderError("Session context is invalid. Expected an object containing payloadJson.vectorAssetDocument.");
117117
return;
118118
}
119+
if (sessionContext.version !== "v2") {
120+
this.renderError("Unsupported session version");
121+
return;
122+
}
119123
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
120124
this.renderError("SVG Asset Studio V2 session data is invalid. Expected payloadJson only.");
121125
return;

0 commit comments

Comments
 (0)