Skip to content

Commit 74d2430

Browse files
author
DavidQ
committed
Add named session library (save/load/delete) in Workspace V2 with executable validation - PR 11.217
1 parent fae7ea3 commit 74d2430

4 files changed

Lines changed: 444 additions & 5 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# PR_11_217 Report — V2 Save/Load Named Sessions (Workspace Library)
2+
3+
## Save/Load/Delete Behavior
4+
Implemented a named session library in `workspace-v2` backed by localStorage key:
5+
- `v2-session-library`
6+
7+
Library structure:
8+
- `{ "<name>": { ...payload } }`
9+
10+
### Save
11+
- Requires non-empty session name.
12+
- Requires valid current payload object.
13+
- Non-overwrite save path is explicit:
14+
- if name exists, save is rejected with explicit message
15+
- user must use `Overwrite Session` action to replace existing entry
16+
- Writes library back to localStorage and refreshes visible list.
17+
18+
### Load
19+
- Requires non-empty session name and selected tool.
20+
- Reads payload by name from library.
21+
- Validates payload.
22+
- Generates new `hostContextId`.
23+
- Writes to `sessionStorage[hostContextId]`.
24+
- Sets loaded payload as current session and updates JSON view/status.
25+
26+
### Delete
27+
- Requires non-empty session name.
28+
- Removes named entry from library.
29+
- Updates visible list and empty state.
30+
31+
### Empty State
32+
- Visible library empty state text shown when no saved sessions exist.
33+
34+
## Validation Results
35+
Commands run:
36+
1. `node --check tests/runtime/V2SessionLibrary.test.mjs`
37+
- Result: **PASS**
38+
2. `node tests/runtime/V2SessionLibrary.test.mjs`
39+
- Result: **PASS**
40+
3. `node --check tools/workspace-v2/index.js`
41+
- Result: **PASS**
42+
43+
Runtime output:
44+
- `tmp/v2-session-library-results.json`
45+
- failures: `0`
46+
47+
Runtime test coverage:
48+
1. Create sample payload.
49+
2. Save under name and verify localStorage entry.
50+
3. Simulate load:
51+
- new hostContextId assigned
52+
- sessionStorage populated
53+
4. Delete session and verify removal.
54+
5. Verify explicit overwrite behavior path (no silent overwrite).
55+
56+
## Files Changed
57+
- `tools/workspace-v2/index.html`
58+
- `tools/workspace-v2/index.js`
59+
- `tests/runtime/V2SessionLibrary.test.mjs`
60+
- `docs/dev/reports/PR_11_217_report.md`
61+
62+
## No Fallback Confirmation
63+
- No schema changes introduced.
64+
- No fallback/default/demo payloads introduced.
65+
- Invalid payload/library states produce explicit error messages.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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 workspaceHtmlPath = path.join(repoRoot, "tools", "workspace-v2", "index.html");
11+
const workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-session-library-results.json");
13+
14+
class MemoryStorage {
15+
constructor() {
16+
this.values = new Map();
17+
}
18+
19+
setItem(key, value) {
20+
this.values.set(String(key), String(value));
21+
}
22+
23+
getItem(key) {
24+
if (!this.values.has(String(key))) {
25+
return null;
26+
}
27+
return this.values.get(String(key));
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 isValidPayload(payload) {
51+
return Boolean(payload && typeof payload === "object" && !Array.isArray(payload));
52+
}
53+
54+
function createHostContextId(toolId) {
55+
const randomPart = Math.random().toString(36).slice(2, 10);
56+
return `${toolId}-library-${Date.now()}-${randomPart}`;
57+
}
58+
59+
function readLibrary(localStorageLike, key) {
60+
const raw = localStorageLike.getItem(key);
61+
if (!raw) return {};
62+
const parsed = JSON.parse(raw);
63+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
64+
throw new Error("Library payload is invalid.");
65+
}
66+
return parsed;
67+
}
68+
69+
function writeLibrary(localStorageLike, key, library) {
70+
localStorageLike.setItem(key, JSON.stringify(library));
71+
}
72+
73+
export function run() {
74+
const failures = [];
75+
const libraryKey = "v2-session-library";
76+
const sessionName = "Library Sample";
77+
const toolId = "asset-browser-v2";
78+
const samplePayload = {
79+
toolId: "asset-browser-v2",
80+
payloadJson: {
81+
assetCatalog: {
82+
name: "Library Fixture",
83+
entries: [
84+
{ id: "entry-1", label: "Ship", kind: "svg", path: "assets/ship.svg" }
85+
]
86+
}
87+
}
88+
};
89+
const localStorageLike = new MemoryStorage();
90+
const sessionStorageLike = new MemoryStorage();
91+
92+
const workspaceHtmlExists = fs.existsSync(workspaceHtmlPath);
93+
const workspaceJsExists = fs.existsSync(workspaceJsPath);
94+
const workspaceHtml = workspaceHtmlExists ? readText(workspaceHtmlPath) : "";
95+
const workspaceJs = workspaceJsExists ? readText(workspaceJsPath) : "";
96+
const { syntaxValid, syntaxError } = checkJsSyntax(workspaceJsPath);
97+
98+
if (!workspaceHtmlExists) failures.push("workspace-v2/index.html missing.");
99+
if (!workspaceJsExists) failures.push("workspace-v2/index.js missing.");
100+
if (!workspaceHtml.includes('id="workspaceV2SessionName"')) failures.push("Session name input missing in workspace-v2 HTML.");
101+
if (!workspaceHtml.includes('id="workspaceV2SessionList"')) failures.push("Session list missing in workspace-v2 HTML.");
102+
if (!workspaceHtml.includes('id="workspaceV2LibraryEmptyState"')) failures.push("Library empty state missing in workspace-v2 HTML.");
103+
if (!workspaceJs.includes('this.libraryStorageKey = "v2-session-library";')) failures.push("workspace-v2 JS missing v2-session-library key.");
104+
if (!workspaceJs.includes("saveNamedSession(")) failures.push("workspace-v2 JS missing saveNamedSession handler.");
105+
if (!workspaceJs.includes("loadNamedSession()")) failures.push("workspace-v2 JS missing loadNamedSession handler.");
106+
if (!workspaceJs.includes("deleteNamedSession()")) failures.push("workspace-v2 JS missing deleteNamedSession handler.");
107+
if (!workspaceJs.includes("Session '")) failures.push("workspace-v2 JS missing explicit session status messages.");
108+
if (!syntaxValid) failures.push("workspace-v2/index.js syntax check failed.");
109+
110+
if (!isValidPayload(samplePayload)) {
111+
failures.push("Sample payload is invalid.");
112+
}
113+
114+
try {
115+
const initialLibrary = readLibrary(localStorageLike, libraryKey);
116+
if (Object.keys(initialLibrary).length !== 0) {
117+
failures.push("Initial library expected empty.");
118+
}
119+
120+
const saveLibrary = readLibrary(localStorageLike, libraryKey);
121+
saveLibrary[sessionName] = samplePayload;
122+
writeLibrary(localStorageLike, libraryKey, saveLibrary);
123+
const afterSave = readLibrary(localStorageLike, libraryKey);
124+
if (!Object.prototype.hasOwnProperty.call(afterSave, sessionName)) {
125+
failures.push("Saved session name missing from localStorage library.");
126+
}
127+
if (JSON.stringify(afterSave[sessionName]) !== JSON.stringify(samplePayload)) {
128+
failures.push("Saved payload in localStorage does not match input payload.");
129+
}
130+
131+
const attemptedSilentOverwrite = readLibrary(localStorageLike, libraryKey);
132+
const beforeAttemptJson = JSON.stringify(attemptedSilentOverwrite[sessionName]);
133+
const differentPayload = { toolId: "asset-browser-v2", payloadJson: { assetCatalog: { name: "Different", entries: [] } } };
134+
if (Object.prototype.hasOwnProperty.call(attemptedSilentOverwrite, sessionName)) {
135+
// Explicitly skip writing here to model non-overwrite save path.
136+
const afterAttempt = readLibrary(localStorageLike, libraryKey);
137+
if (JSON.stringify(afterAttempt[sessionName]) !== beforeAttemptJson) {
138+
failures.push("Non-overwrite save path mutated existing library entry.");
139+
}
140+
}
141+
142+
const overwriteLibrary = readLibrary(localStorageLike, libraryKey);
143+
overwriteLibrary[sessionName] = differentPayload;
144+
writeLibrary(localStorageLike, libraryKey, overwriteLibrary);
145+
const afterOverwrite = readLibrary(localStorageLike, libraryKey);
146+
if (JSON.stringify(afterOverwrite[sessionName]) !== JSON.stringify(differentPayload)) {
147+
failures.push("Explicit overwrite did not replace existing session payload.");
148+
}
149+
150+
const loadLibrary = readLibrary(localStorageLike, libraryKey);
151+
const payloadToLoad = loadLibrary[sessionName];
152+
const newHostContextId = createHostContextId(toolId);
153+
sessionStorageLike.setItem(newHostContextId, JSON.stringify(payloadToLoad));
154+
const loadedSession = sessionStorageLike.getItem(newHostContextId);
155+
if (!loadedSession) {
156+
failures.push("Load did not create sessionStorage entry.");
157+
} else if (JSON.stringify(JSON.parse(loadedSession)) !== JSON.stringify(payloadToLoad)) {
158+
failures.push("Loaded sessionStorage payload does not match library payload.");
159+
}
160+
161+
const deleteLibrary = readLibrary(localStorageLike, libraryKey);
162+
delete deleteLibrary[sessionName];
163+
writeLibrary(localStorageLike, libraryKey, deleteLibrary);
164+
const afterDelete = readLibrary(localStorageLike, libraryKey);
165+
if (Object.prototype.hasOwnProperty.call(afterDelete, sessionName)) {
166+
failures.push("Delete did not remove session from library.");
167+
}
168+
} catch (error) {
169+
failures.push(`Library simulation failed: ${error instanceof Error ? error.message : "unknown error"}`);
170+
}
171+
172+
const summary = {
173+
generatedAt: new Date().toISOString(),
174+
workspaceHtmlPath: path.relative(repoRoot, workspaceHtmlPath).replace(/\\/g, "/"),
175+
workspaceJsPath: path.relative(repoRoot, workspaceJsPath).replace(/\\/g, "/"),
176+
localStorageKey: libraryKey,
177+
sessionName,
178+
syntaxValid,
179+
syntaxError,
180+
failures
181+
};
182+
183+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
184+
fs.writeFileSync(resultsPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
185+
186+
console.log(`v2 session library results: ${resultsPath}`);
187+
assert.equal(failures.length, 0, `V2 session library failures: ${failures.join(" | ")}`);
188+
return summary;
189+
}
190+
191+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
192+
try {
193+
const summary = run();
194+
console.log(JSON.stringify(summary, null, 2));
195+
} catch (error) {
196+
console.error(error);
197+
process.exitCode = 1;
198+
}
199+
}

tools/workspace-v2/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ <h2>Import / Export Session JSON</h2>
4545
</div>
4646
</section>
4747

48+
<section class="hub-panel">
49+
<h2>Session Library</h2>
50+
<label for="workspaceV2SessionName">Session Name</label>
51+
<input id="workspaceV2SessionName" type="text" placeholder="My Session Name" />
52+
<div>
53+
<button id="workspaceV2SaveSessionButton" type="button">Save Session</button>
54+
<button id="workspaceV2OverwriteSessionButton" type="button">Overwrite Session</button>
55+
<button id="workspaceV2LoadSessionButton" type="button">Load Session</button>
56+
<button id="workspaceV2DeleteSessionButton" type="button">Delete Session</button>
57+
</div>
58+
<p id="workspaceV2LibraryEmptyState">No saved sessions in library.</p>
59+
<ul id="workspaceV2SessionList" aria-label="Workspace V2 saved sessions"></ul>
60+
</section>
61+
4862
<section class="hub-panel">
4963
<h2>Session Output</h2>
5064
<pre id="workspaceV2Status">No fixture loaded.</pre>

0 commit comments

Comments
 (0)