Skip to content

Commit c36bbb6

Browse files
author
DavidQ
committed
Wire session library actions with explicit status feedback - PR 11.244
1 parent 297da66 commit c36bbb6

4 files changed

Lines changed: 387 additions & 24 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# PR_11_244 — Workspace V2 Session Library Actions + Status Feedback
2+
3+
## Summary
4+
Implemented explicit, non-silent Session Library action feedback for `Save Session`, `Overwrite Session`, `Load Session`, and `Delete Saved Session` in Workspace V2.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.html`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2SessionLibraryActions.test.mjs`
10+
11+
## Implementation Details
12+
- Added dedicated Session Library status output:
13+
- `#workspaceV2LibraryStatus`
14+
- Wired Session Library actions to write explicit status through one path:
15+
- `setLibraryStatus(message)`
16+
17+
### Save Session
18+
- Empty ID -> `Enter a session ID before saving.`
19+
- Missing/invalid active Workspace V2 session -> `No active Workspace V2 session is available to save.`
20+
- Duplicate ID -> `Saved session already exists. Use Overwrite Session.`
21+
- Success -> saves into `localStorage['v2-session-library']` and shows `Saved session created.`
22+
23+
### Overwrite Session
24+
- Empty ID -> `Enter a session ID before overwriting.`
25+
- Missing/invalid active Workspace V2 session -> `No active Workspace V2 session is available to overwrite from.`
26+
- Missing saved entry -> `Saved session not found. Use Save Session to create it first.`
27+
- Success -> overwrites saved entry and shows `Saved session overwritten.`
28+
29+
### Load Session
30+
- Empty ID -> `Enter a saved session ID before loading.`
31+
- Missing saved entry -> `Saved session not found.`
32+
- Success -> loads saved payload into active state and shows `Saved session loaded.`
33+
34+
### Delete Saved Session
35+
- Empty ID -> `Enter a saved session ID before deleting.`
36+
- Empty library -> `No saved sessions exist. Use Delete on Recent Sessions to remove temporary sessions.`
37+
- Recent-only ID -> `Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.`
38+
- Unknown ID -> `Saved session not found.`
39+
- Success -> deletes only from Session Library and shows `Saved session deleted.`
40+
41+
## Storage + Scope Guarantees
42+
- Session Library actions operate on `localStorage` key `v2-session-library`.
43+
- `Delete Saved Session` does not delete Recent Sessions.
44+
- `Delete Saved Session` does not remove `sessionStorage` payloads.
45+
- Save/Overwrite do not create fallback/default payloads.
46+
- Save/Overwrite use only active valid Workspace V2 payload.
47+
- Successful Save/Overwrite/Delete refresh Session Library rendering.
48+
- Library mutations trigger Diff/Merge inventory recompute via existing library write path.
49+
50+
## Validation Commands Run
51+
```powershell
52+
node --check tools/workspace-v2/index.js
53+
node --check tests/runtime/V2SessionLibraryActions.test.mjs
54+
node --check tests/runtime/V2SavedSessionDeleteFeedback.test.mjs
55+
node tests/runtime/V2SessionLibraryActions.test.mjs
56+
node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs
57+
```
58+
59+
## Validation Results
60+
- `node --check tools/workspace-v2/index.js` -> PASS
61+
- `node --check tests/runtime/V2SessionLibraryActions.test.mjs` -> PASS
62+
- `node --check tests/runtime/V2SavedSessionDeleteFeedback.test.mjs` -> PASS
63+
- `node tests/runtime/V2SessionLibraryActions.test.mjs` -> PASS
64+
- output: `tmp/v2-session-library-actions-results.json`
65+
- failures: `[]`
66+
- `node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs` -> PASS
67+
- output: `tmp/v2-saved-session-delete-feedback-results.json`
68+
- failures: `[]`
69+
70+
## Full Samples Smoke
71+
Skipped.
72+
73+
Reason: PR scope is limited to Workspace V2 Session Library UI/actions and targeted runtime checks. No schemas, samples, games, or shared sample infrastructure were changed.
74+
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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 htmlPath = path.join(repoRoot, "tools", "workspace-v2", "index.html");
11+
const jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-session-library-actions-results.json");
13+
14+
function readText(filePath) {
15+
return fs.readFileSync(filePath, "utf8");
16+
}
17+
18+
function checkSyntax(filePath) {
19+
try {
20+
execFileSync(process.execPath, ["--check", filePath], {
21+
cwd: repoRoot,
22+
stdio: ["ignore", "pipe", "pipe"]
23+
});
24+
return { ok: true, error: "" };
25+
} catch (error) {
26+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
27+
}
28+
}
29+
30+
function evaluateSaveAction(inputId, activePayload, library) {
31+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
32+
const libraryMap = { ...library };
33+
if (!sessionId) {
34+
return { message: "Enter a session ID before saving.", library: libraryMap };
35+
}
36+
if (!activePayload || typeof activePayload !== "object" || Array.isArray(activePayload)) {
37+
return { message: "No active Workspace V2 session is available to save.", library: libraryMap };
38+
}
39+
if (Object.prototype.hasOwnProperty.call(libraryMap, sessionId)) {
40+
return { message: "Saved session already exists. Use Overwrite Session.", library: libraryMap };
41+
}
42+
libraryMap[sessionId] = activePayload;
43+
return { message: "Saved session created.", library: libraryMap };
44+
}
45+
46+
function evaluateOverwriteAction(inputId, activePayload, library) {
47+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
48+
const libraryMap = { ...library };
49+
if (!sessionId) {
50+
return { message: "Enter a session ID before overwriting.", library: libraryMap };
51+
}
52+
if (!activePayload || typeof activePayload !== "object" || Array.isArray(activePayload)) {
53+
return { message: "No active Workspace V2 session is available to overwrite from.", library: libraryMap };
54+
}
55+
if (!Object.prototype.hasOwnProperty.call(libraryMap, sessionId)) {
56+
return { message: "Saved session not found. Use Save Session to create it first.", library: libraryMap };
57+
}
58+
libraryMap[sessionId] = activePayload;
59+
return { message: "Saved session overwritten.", library: libraryMap };
60+
}
61+
62+
function evaluateLoadAction(inputId, library) {
63+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
64+
if (!sessionId) {
65+
return { message: "Enter a saved session ID before loading.", loaded: false };
66+
}
67+
if (!Object.prototype.hasOwnProperty.call(library, sessionId)) {
68+
return { message: "Saved session not found.", loaded: false };
69+
}
70+
return { message: "Saved session loaded.", loaded: true, payload: library[sessionId] };
71+
}
72+
73+
function evaluateDeleteSavedAction(inputId, library, historyEntries, sessionStorageMap) {
74+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
75+
const libraryMap = { ...library };
76+
const history = [...historyEntries];
77+
const storage = { ...sessionStorageMap };
78+
if (!sessionId) {
79+
return { message: "Enter a saved session ID before deleting.", library: libraryMap, history, storage };
80+
}
81+
const recentMatch = history.some((entry) => entry.hostContextId === sessionId);
82+
const libraryKeys = Object.keys(libraryMap);
83+
if (libraryKeys.length === 0) {
84+
return {
85+
message: recentMatch
86+
? "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions."
87+
: "No saved sessions exist. Use Delete on Recent Sessions to remove temporary sessions.",
88+
library: libraryMap,
89+
history,
90+
storage
91+
};
92+
}
93+
if (!Object.prototype.hasOwnProperty.call(libraryMap, sessionId)) {
94+
return {
95+
message: recentMatch
96+
? "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions."
97+
: "Saved session not found.",
98+
library: libraryMap,
99+
history,
100+
storage
101+
};
102+
}
103+
delete libraryMap[sessionId];
104+
return { message: "Saved session deleted.", library: libraryMap, history, storage };
105+
}
106+
107+
export function run() {
108+
const failures = [];
109+
const htmlExists = fs.existsSync(htmlPath);
110+
const jsExists = fs.existsSync(jsPath);
111+
const html = htmlExists ? readText(htmlPath) : "";
112+
const js = jsExists ? readText(jsPath) : "";
113+
const jsSyntax = checkSyntax(jsPath);
114+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2SessionLibraryActions.test.mjs"));
115+
116+
if (!htmlExists) failures.push("Missing tools/workspace-v2/index.html.");
117+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
118+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
119+
if (!testSyntax.ok) failures.push("tests/runtime/V2SessionLibraryActions.test.mjs failed syntax check.");
120+
121+
if (!html.includes("id=\"workspaceV2LibraryStatus\"")) {
122+
failures.push("Missing explicit Session Library status output area.");
123+
}
124+
if (!js.includes("this.libraryStatusNode = document.getElementById(\"workspaceV2LibraryStatus\");")) {
125+
failures.push("Session Library status node is not wired in JS.");
126+
}
127+
if (!js.includes("setLibraryStatus(message)")) {
128+
failures.push("setLibraryStatus(message) helper is missing.");
129+
}
130+
131+
const requiredMessages = [
132+
"Enter a session ID before saving.",
133+
"No active Workspace V2 session is available to save.",
134+
"Saved session already exists. Use Overwrite Session.",
135+
"Saved session created.",
136+
"Enter a session ID before overwriting.",
137+
"No active Workspace V2 session is available to overwrite from.",
138+
"Saved session not found. Use Save Session to create it first.",
139+
"Saved session overwritten.",
140+
"Enter a saved session ID before loading.",
141+
"Saved session loaded.",
142+
"Enter a saved session ID before deleting.",
143+
"No saved sessions exist. Use Delete on Recent Sessions to remove temporary sessions.",
144+
"Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.",
145+
"Saved session not found.",
146+
"Saved session deleted."
147+
];
148+
requiredMessages.forEach((message) => {
149+
if (!js.includes(message)) {
150+
failures.push(`Missing required message: ${message}`);
151+
}
152+
});
153+
154+
if (!js.includes("this.writeSessionLibrary(library);") || !js.includes("this.renderSessionLibrary();")) {
155+
failures.push("Session Library refresh path missing after mutations.");
156+
}
157+
if (!js.includes("this.renderSessionDiffInputs();") || !js.includes("this.renderSessionMergeInputs();")) {
158+
failures.push("Diff/Merge selector recompute hooks are missing after library changes.");
159+
}
160+
161+
const recentHistory = [
162+
{ hostContextId: "recent-1", tool: "asset-browser-v2", timestamp: "2026-05-01T00:00:00.000Z", payload: { toolId: "asset-browser-v2", version: "v2" } }
163+
];
164+
const storageMap = { "recent-1": "{\"toolId\":\"asset-browser-v2\",\"version\":\"v2\"}" };
165+
const activePayload = { toolId: "asset-browser-v2", version: "v2", payloadJson: { ok: true } };
166+
167+
const emptySave = evaluateSaveAction("", activePayload, {});
168+
const emptyOverwrite = evaluateOverwriteAction("", activePayload, {});
169+
const emptyLoad = evaluateLoadAction("", {});
170+
const emptyDelete = evaluateDeleteSavedAction("", {}, recentHistory, storageMap);
171+
if (emptySave.message !== "Enter a session ID before saving.") failures.push("Save empty input message mismatch.");
172+
if (emptyOverwrite.message !== "Enter a session ID before overwriting.") failures.push("Overwrite empty input message mismatch.");
173+
if (emptyLoad.message !== "Enter a saved session ID before loading.") failures.push("Load empty input message mismatch.");
174+
if (emptyDelete.message !== "Enter a saved session ID before deleting.") failures.push("Delete empty input message mismatch.");
175+
176+
const saveCreated = evaluateSaveAction("saved-a", activePayload, {});
177+
if (saveCreated.message !== "Saved session created." || !Object.prototype.hasOwnProperty.call(saveCreated.library, "saved-a")) {
178+
failures.push("Save with valid active payload should create library entry.");
179+
}
180+
181+
const duplicateSave = evaluateSaveAction("saved-a", activePayload, { "saved-a": { toolId: "palette-manager-v2", version: "v2" } });
182+
if (duplicateSave.message !== "Saved session already exists. Use Overwrite Session.") {
183+
failures.push("Duplicate save should be blocked with overwrite guidance.");
184+
}
185+
186+
const overwriteMissing = evaluateOverwriteAction("missing", activePayload, {});
187+
if (overwriteMissing.message !== "Saved session not found. Use Save Session to create it first.") {
188+
failures.push("Overwrite missing entry should be blocked with save guidance.");
189+
}
190+
191+
const overwriteExisting = evaluateOverwriteAction("saved-a", activePayload, { "saved-a": { toolId: "palette-manager-v2", version: "v2", payloadJson: { old: true } } });
192+
if (overwriteExisting.message !== "Saved session overwritten." || overwriteExisting.library["saved-a"].payloadJson?.ok !== true) {
193+
failures.push("Overwrite existing entry should succeed.");
194+
}
195+
196+
const loadMissing = evaluateLoadAction("missing", { "saved-a": activePayload });
197+
if (loadMissing.message !== "Saved session not found.") {
198+
failures.push("Load missing entry should be blocked.");
199+
}
200+
201+
const loadExisting = evaluateLoadAction("saved-a", { "saved-a": activePayload });
202+
if (loadExisting.message !== "Saved session loaded." || !loadExisting.loaded) {
203+
failures.push("Load existing entry should succeed.");
204+
}
205+
206+
const deleteEmptyLibraryWithInput = evaluateDeleteSavedAction("unknown", {}, recentHistory, storageMap);
207+
if (deleteEmptyLibraryWithInput.message !== "No saved sessions exist. Use Delete on Recent Sessions to remove temporary sessions.") {
208+
failures.push("Delete with empty library should show explicit empty-library message.");
209+
}
210+
211+
const deleteRecentOnly = evaluateDeleteSavedAction("recent-1", {}, recentHistory, storageMap);
212+
if (deleteRecentOnly.message !== "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.") {
213+
failures.push("Delete recent-only ID should show explicit guidance.");
214+
}
215+
if (deleteRecentOnly.history.length !== recentHistory.length) {
216+
failures.push("Delete Saved Session must not delete Recent Sessions.");
217+
}
218+
if (!Object.prototype.hasOwnProperty.call(deleteRecentOnly.storage, "recent-1")) {
219+
failures.push("Delete Saved Session must not remove sessionStorage payloads.");
220+
}
221+
222+
const deleteSaved = evaluateDeleteSavedAction("saved-a", { "saved-a": activePayload, "saved-b": { toolId: "tilemap-studio-v2", version: "v2" } }, recentHistory, storageMap);
223+
if (deleteSaved.message !== "Saved session deleted." || Object.prototype.hasOwnProperty.call(deleteSaved.library, "saved-a")) {
224+
failures.push("Delete saved session should succeed.");
225+
}
226+
227+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
228+
fs.writeFileSync(resultsPath, `${JSON.stringify({
229+
generatedAt: new Date().toISOString(),
230+
failures,
231+
checks: {
232+
htmlExists,
233+
jsExists,
234+
jsSyntax,
235+
testSyntax
236+
},
237+
scenarios: {
238+
emptySave,
239+
emptyOverwrite,
240+
emptyLoad,
241+
emptyDelete,
242+
saveCreated,
243+
duplicateSave,
244+
overwriteMissing,
245+
overwriteExisting,
246+
loadMissing,
247+
loadExisting,
248+
deleteEmptyLibraryWithInput,
249+
deleteRecentOnly,
250+
deleteSaved
251+
}
252+
}, null, 2)}\n`, "utf8");
253+
254+
console.log(`v2 session-library actions results: ${resultsPath}`);
255+
assert.equal(failures.length, 0, `V2 session-library actions failures: ${failures.join(" | ")}`);
256+
return { failures };
257+
}
258+
259+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
260+
try {
261+
const summary = run();
262+
console.log(JSON.stringify(summary, null, 2));
263+
} catch (error) {
264+
console.error(error);
265+
process.exitCode = 1;
266+
}
267+
}

tools/workspace-v2/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ <h2>Session Library</h2>
6666
<button id="workspaceV2LoadSessionButton" type="button">Load Session</button>
6767
<button id="workspaceV2DeleteSessionButton" type="button">Delete Saved Session</button>
6868
</div>
69+
<p id="workspaceV2LibraryStatus">No Session Library action yet.</p>
6970
<p id="workspaceV2LibraryEmptyState">No saved sessions in library.</p>
7071
<ul id="workspaceV2SessionList" aria-label="Workspace V2 saved sessions"></ul>
7172
</section>

0 commit comments

Comments
 (0)