Skip to content

Commit 5d33e9a

Browse files
author
DavidQ
committed
Allow session library save and overwrite from entered recent session IDs - PR 11.245
1 parent c36bbb6 commit 5d33e9a

3 files changed

Lines changed: 366 additions & 6 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# PR_11_245 — Save Session Library Entry From Entered Recent Session ID
2+
3+
## Summary
4+
Updated Workspace V2 Session Library Save/Overwrite behavior so an entered Recent Session `hostContextId` can resolve to a valid session payload (from recent/sessionStorage) and be saved/overwritten in `v2-session-library`, even when no separate active Workspace V2 session exists.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.js`
8+
- `tests/runtime/V2SaveLibraryFromRecentSession.test.mjs`
9+
10+
## Implementation Details
11+
- Added resolver path for Session Library writes:
12+
- `readSessionPayloadFromRecentSessionId(sessionId)`
13+
- `readSessionPayloadForLibraryWrite(sessionId)`
14+
- Save/Overwrite now resolve source payload in this order:
15+
1. Recent Session by entered `hostContextId` + `sessionStorage` (with recent payload fallback via existing resolver),
16+
2. active Workspace V2 session payload fallback.
17+
- If payload cannot be resolved for Save/Overwrite:
18+
- `Session ID does not resolve to a valid Workspace V2 session.`
19+
20+
## Behavior Verified
21+
- Save / Overwrite / Load / Delete empty-input messages are button-specific.
22+
- Saving valid recent-session ID creates library entry.
23+
- Duplicate save is blocked with overwrite guidance.
24+
- Overwrite existing saved recent-session ID succeeds.
25+
- Overwrite missing saved recent-session ID with resolvable payload gives save guidance.
26+
- Unknown non-resolvable ID gives `does not resolve` for Save/Overwrite.
27+
- Load remains library-only.
28+
- Delete Saved Session remains library-only.
29+
- Recent Sessions are not removed by Save/Overwrite/Load/Delete Saved Session.
30+
31+
## Validation Commands Run
32+
```powershell
33+
node --check tools/workspace-v2/index.js
34+
node --check tests/runtime/V2SaveLibraryFromRecentSession.test.mjs
35+
node --check tests/runtime/V2SavedSessionDeleteFeedback.test.mjs
36+
node tests/runtime/V2SaveLibraryFromRecentSession.test.mjs
37+
node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs
38+
```
39+
40+
## Validation Results
41+
- `node --check tools/workspace-v2/index.js` -> PASS
42+
- `node --check tests/runtime/V2SaveLibraryFromRecentSession.test.mjs` -> PASS
43+
- `node --check tests/runtime/V2SavedSessionDeleteFeedback.test.mjs` -> PASS
44+
- `node tests/runtime/V2SaveLibraryFromRecentSession.test.mjs` -> PASS
45+
- output: `tmp/v2-save-library-from-recent-session-results.json`
46+
- failures: `[]`
47+
- `node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs` -> PASS
48+
- output: `tmp/v2-saved-session-delete-feedback-results.json`
49+
- failures: `[]`
50+
51+
## Full Samples Smoke
52+
Skipped.
53+
54+
Reason: this PR is narrowly scoped to Workspace V2 Session Library action behavior and targeted runtime validation. No schemas/samples/games/shared sample infrastructure were changed.
55+
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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 jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
11+
const resultsPath = path.join(repoRoot, "tmp", "v2-save-library-from-recent-session-results.json");
12+
13+
function readText(filePath) {
14+
return fs.readFileSync(filePath, "utf8");
15+
}
16+
17+
function checkSyntax(filePath) {
18+
try {
19+
execFileSync(process.execPath, ["--check", filePath], {
20+
cwd: repoRoot,
21+
stdio: ["ignore", "pipe", "pipe"]
22+
});
23+
return { ok: true, error: "" };
24+
} catch (error) {
25+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
26+
}
27+
}
28+
29+
function isValidPayload(payload) {
30+
return Boolean(payload && typeof payload === "object" && !Array.isArray(payload));
31+
}
32+
33+
function resolvePayloadFromRecentSessionId(sessionId, history, storage, activePayload) {
34+
const id = typeof sessionId === "string" ? sessionId.trim() : "";
35+
if (id) {
36+
const recent = history.find((entry) => entry.hostContextId === id);
37+
if (recent) {
38+
const raw = storage[id];
39+
if (typeof raw === "string") {
40+
try {
41+
const parsed = JSON.parse(raw);
42+
if (isValidPayload(parsed)) {
43+
return parsed;
44+
}
45+
} catch {
46+
return null;
47+
}
48+
}
49+
if (isValidPayload(recent.payload)) {
50+
return recent.payload;
51+
}
52+
return null;
53+
}
54+
}
55+
if (isValidPayload(activePayload)) {
56+
return activePayload;
57+
}
58+
return null;
59+
}
60+
61+
function simulateSave(overwrite, inputId, library, history, storage, activePayload) {
62+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
63+
const nextLibrary = { ...library };
64+
if (!sessionId) {
65+
return { message: overwrite ? "Enter a session ID before overwriting." : "Enter a session ID before saving.", library: nextLibrary };
66+
}
67+
const payload = resolvePayloadFromRecentSessionId(sessionId, history, storage, activePayload);
68+
if (!isValidPayload(payload)) {
69+
return { message: "Session ID does not resolve to a valid Workspace V2 session.", library: nextLibrary };
70+
}
71+
const exists = Object.prototype.hasOwnProperty.call(nextLibrary, sessionId);
72+
if (!overwrite && exists) {
73+
return { message: "Saved session already exists. Use Overwrite Session.", library: nextLibrary };
74+
}
75+
if (overwrite && !exists) {
76+
return { message: "Saved session not found. Use Save Session to create it first.", library: nextLibrary };
77+
}
78+
nextLibrary[sessionId] = payload;
79+
return { message: overwrite ? "Saved session overwritten." : "Saved session created.", library: nextLibrary };
80+
}
81+
82+
function simulateLoad(inputId, library) {
83+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
84+
if (!sessionId) {
85+
return { message: "Enter a saved session ID before loading.", loaded: false };
86+
}
87+
if (!Object.prototype.hasOwnProperty.call(library, sessionId)) {
88+
return { message: "Saved session not found.", loaded: false };
89+
}
90+
return { message: "Saved session loaded.", loaded: true, payload: library[sessionId] };
91+
}
92+
93+
function simulateDelete(inputId, library, history, storage) {
94+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
95+
const nextLibrary = { ...library };
96+
const nextHistory = [...history];
97+
const nextStorage = { ...storage };
98+
if (!sessionId) {
99+
return { message: "Enter a saved session ID before deleting.", library: nextLibrary, history: nextHistory, storage: nextStorage };
100+
}
101+
const recentMatch = nextHistory.some((entry) => entry.hostContextId === sessionId);
102+
if (Object.keys(nextLibrary).length === 0) {
103+
return {
104+
message: recentMatch
105+
? "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions."
106+
: "No saved sessions exist. Use Delete on Recent Sessions to remove temporary sessions.",
107+
library: nextLibrary,
108+
history: nextHistory,
109+
storage: nextStorage
110+
};
111+
}
112+
if (!Object.prototype.hasOwnProperty.call(nextLibrary, sessionId)) {
113+
return {
114+
message: recentMatch
115+
? "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions."
116+
: "Saved session not found.",
117+
library: nextLibrary,
118+
history: nextHistory,
119+
storage: nextStorage
120+
};
121+
}
122+
delete nextLibrary[sessionId];
123+
return { message: "Saved session deleted.", library: nextLibrary, history: nextHistory, storage: nextStorage };
124+
}
125+
126+
export function run() {
127+
const failures = [];
128+
const jsExists = fs.existsSync(jsPath);
129+
const js = jsExists ? readText(jsPath) : "";
130+
const jsSyntax = checkSyntax(jsPath);
131+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2SaveLibraryFromRecentSession.test.mjs"));
132+
133+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
134+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
135+
if (!testSyntax.ok) failures.push("tests/runtime/V2SaveLibraryFromRecentSession.test.mjs failed syntax check.");
136+
137+
const mustContain = [
138+
"readSessionPayloadFromRecentSessionId(sessionId)",
139+
"readSessionPayloadForLibraryWrite(sessionId)",
140+
"Session ID does not resolve to a valid Workspace V2 session.",
141+
"Enter a session ID before saving.",
142+
"Enter a session ID before overwriting.",
143+
"Enter a saved session ID before loading.",
144+
"Enter a saved session ID before deleting.",
145+
"Saved session already exists. Use Overwrite Session.",
146+
"Saved session overwritten.",
147+
"Saved session loaded.",
148+
"Saved session deleted."
149+
];
150+
mustContain.forEach((token) => {
151+
if (!js.includes(token)) {
152+
failures.push(`Missing required implementation token/message: ${token}`);
153+
}
154+
});
155+
156+
const history = [
157+
{
158+
hostContextId: "asset-browser-v2-1777676088718-3eff5h3y",
159+
tool: "asset-browser-v2",
160+
timestamp: "2026-05-01T01:00:00.000Z",
161+
payload: { version: "v2", toolId: "asset-browser-v2", payloadJson: { source: "history-fallback" } }
162+
}
163+
];
164+
const storage = {
165+
"asset-browser-v2-1777676088718-3eff5h3y": JSON.stringify({ version: "v2", toolId: "asset-browser-v2", payloadJson: { source: "storage" } })
166+
};
167+
const activePayload = null;
168+
const initialRecentSnapshot = JSON.stringify(history);
169+
const initialStorageSnapshot = JSON.stringify(storage);
170+
171+
const saveEmpty = simulateSave(false, "", {}, history, storage, activePayload);
172+
const overwriteEmpty = simulateSave(true, "", {}, history, storage, activePayload);
173+
const loadEmpty = simulateLoad("", {});
174+
const deleteEmpty = simulateDelete("", {}, history, storage);
175+
if (saveEmpty.message !== "Enter a session ID before saving.") failures.push("Save empty input message mismatch.");
176+
if (overwriteEmpty.message !== "Enter a session ID before overwriting.") failures.push("Overwrite empty input message mismatch.");
177+
if (loadEmpty.message !== "Enter a saved session ID before loading.") failures.push("Load empty input message mismatch.");
178+
if (deleteEmpty.message !== "Enter a saved session ID before deleting.") failures.push("Delete empty input message mismatch.");
179+
180+
const saveRecent = simulateSave(false, "asset-browser-v2-1777676088718-3eff5h3y", {}, history, storage, activePayload);
181+
if (saveRecent.message !== "Saved session created.") failures.push("Save should create from valid recent-session id.");
182+
if (!Object.prototype.hasOwnProperty.call(saveRecent.library, "asset-browser-v2-1777676088718-3eff5h3y")) {
183+
failures.push("Save from recent-session id did not create library entry.");
184+
}
185+
186+
const duplicateSave = simulateSave(false, "asset-browser-v2-1777676088718-3eff5h3y", saveRecent.library, history, storage, activePayload);
187+
if (duplicateSave.message !== "Saved session already exists. Use Overwrite Session.") {
188+
failures.push("Duplicate save should be blocked with overwrite guidance.");
189+
}
190+
191+
const overwriteExisting = simulateSave(true, "asset-browser-v2-1777676088718-3eff5h3y", saveRecent.library, history, storage, activePayload);
192+
if (overwriteExisting.message !== "Saved session overwritten.") {
193+
failures.push("Overwrite should succeed for existing saved recent-session id.");
194+
}
195+
196+
const overwriteMissing = simulateSave(true, "palette-manager-v2-unknown", saveRecent.library, history, storage, activePayload);
197+
if (overwriteMissing.message !== "Session ID does not resolve to a valid Workspace V2 session.") {
198+
failures.push("Overwrite unknown id should show does-not-resolve message.");
199+
}
200+
201+
const overwriteMissingSavedButResolvable = simulateSave(
202+
true,
203+
"asset-browser-v2-1777676088718-3eff5h3y",
204+
{},
205+
history,
206+
storage,
207+
activePayload
208+
);
209+
if (overwriteMissingSavedButResolvable.message !== "Saved session not found. Use Save Session to create it first.") {
210+
failures.push("Overwrite missing saved entry for resolvable id should show save guidance.");
211+
}
212+
213+
const saveUnknown = simulateSave(false, "unknown-id", {}, history, storage, activePayload);
214+
if (saveUnknown.message !== "Session ID does not resolve to a valid Workspace V2 session.") {
215+
failures.push("Save unknown id should show does-not-resolve message.");
216+
}
217+
218+
const loadMissing = simulateLoad("missing-id", saveRecent.library);
219+
if (loadMissing.message !== "Saved session not found.") {
220+
failures.push("Load should remain library-only and block missing entry.");
221+
}
222+
const loadExisting = simulateLoad("asset-browser-v2-1777676088718-3eff5h3y", saveRecent.library);
223+
if (loadExisting.message !== "Saved session loaded." || !loadExisting.loaded) {
224+
failures.push("Load existing saved entry should succeed.");
225+
}
226+
227+
const deleteRecentOnly = simulateDelete("asset-browser-v2-1777676088718-3eff5h3y", {}, history, storage);
228+
if (deleteRecentOnly.message !== "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.") {
229+
failures.push("Delete Saved Session recent-only guidance message mismatch.");
230+
}
231+
const deleteSaved = simulateDelete("asset-browser-v2-1777676088718-3eff5h3y", saveRecent.library, history, storage);
232+
if (deleteSaved.message !== "Saved session deleted.") {
233+
failures.push("Delete saved entry should succeed.");
234+
}
235+
236+
if (JSON.stringify(history) !== initialRecentSnapshot) {
237+
failures.push("Save/Overwrite/Load/Delete Saved Session must not remove recent sessions.");
238+
}
239+
if (JSON.stringify(storage) !== initialStorageSnapshot) {
240+
failures.push("Save/Overwrite/Load/Delete Saved Session must not remove sessionStorage payloads.");
241+
}
242+
243+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
244+
fs.writeFileSync(resultsPath, `${JSON.stringify({
245+
generatedAt: new Date().toISOString(),
246+
failures,
247+
checks: {
248+
jsExists,
249+
jsSyntax,
250+
testSyntax
251+
},
252+
scenarios: {
253+
saveEmpty,
254+
overwriteEmpty,
255+
loadEmpty,
256+
deleteEmpty,
257+
saveRecent,
258+
duplicateSave,
259+
overwriteExisting,
260+
overwriteMissing,
261+
overwriteMissingSavedButResolvable,
262+
saveUnknown,
263+
loadMissing,
264+
loadExisting,
265+
deleteRecentOnly,
266+
deleteSaved
267+
}
268+
}, null, 2)}\n`, "utf8");
269+
270+
console.log(`v2 save-library-from-recent-session results: ${resultsPath}`);
271+
assert.equal(failures.length, 0, `V2 save-library-from-recent-session failures: ${failures.join(" | ")}`);
272+
return { failures };
273+
}
274+
275+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
276+
try {
277+
const summary = run();
278+
console.log(JSON.stringify(summary, null, 2));
279+
} catch (error) {
280+
console.error(error);
281+
process.exitCode = 1;
282+
}
283+
}

tools/workspace-v2/index.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,30 @@ class WorkspaceV2SessionProducer {
210210
return parsed.value;
211211
}
212212

213+
readSessionPayloadFromRecentSessionId(sessionId) {
214+
if (typeof sessionId !== "string" || !sessionId.trim()) {
215+
return null;
216+
}
217+
const history = this.readSessionHistory();
218+
const recentEntry = history.find((entry) => entry.hostContextId === sessionId.trim());
219+
if (!recentEntry) {
220+
return null;
221+
}
222+
return this.resolveSessionPayloadFromContextId(sessionId.trim(), recentEntry.payload);
223+
}
224+
225+
readSessionPayloadForLibraryWrite(sessionId) {
226+
const recentPayload = this.readSessionPayloadFromRecentSessionId(sessionId);
227+
if (this.isValidSessionPayload(recentPayload)) {
228+
return recentPayload;
229+
}
230+
const activePayload = this.readActiveSessionPayloadForLibraryActions();
231+
if (this.isValidSessionPayload(activePayload)) {
232+
return activePayload;
233+
}
234+
return null;
235+
}
236+
213237
fixturePathForTool(toolId) {
214238
return `../../tests/fixtures/v2-tools/${toolId}.json`;
215239
}
@@ -1704,11 +1728,9 @@ class WorkspaceV2SessionProducer {
17041728
this.setLibraryStatus(overwriteExisting ? "Enter a session ID before overwriting." : "Enter a session ID before saving.");
17051729
return;
17061730
}
1707-
const activePayload = this.readActiveSessionPayloadForLibraryActions();
1708-
if (!this.isValidSessionPayload(activePayload)) {
1709-
this.setLibraryStatus(overwriteExisting
1710-
? "No active Workspace V2 session is available to overwrite from."
1711-
: "No active Workspace V2 session is available to save.");
1731+
const payloadForWrite = this.readSessionPayloadForLibraryWrite(sessionName);
1732+
if (!this.isValidSessionPayload(payloadForWrite)) {
1733+
this.setLibraryStatus("Session ID does not resolve to a valid Workspace V2 session.");
17121734
return;
17131735
}
17141736
const library = this.readSessionLibrary();
@@ -1724,7 +1746,7 @@ class WorkspaceV2SessionProducer {
17241746
this.setLibraryStatus("Saved session not found. Use Save Session to create it first.");
17251747
return;
17261748
}
1727-
library[sessionName] = activePayload;
1749+
library[sessionName] = payloadForWrite;
17281750
this.writeSessionLibrary(library);
17291751
this.renderSessionLibrary();
17301752
this.setLibraryStatus(overwriteExisting ? "Saved session overwritten." : "Saved session created.");

0 commit comments

Comments
 (0)