Skip to content

Commit 631b84b

Browse files
author
DavidQ
committed
Refresh Workspace and V2 tools from normalized session data to preserve cross-tool changes - PR_26128_029-workspace-tool-session-refresh
1 parent 2c1ecbb commit 631b84b

8 files changed

Lines changed: 336 additions & 30 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Playwright Workspace Tool Session Refresh
2+
3+
## Commands
4+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "exports manifests and launches tools from fixed Workspace Manager V2 tiles"`
5+
- `npm run test:workspace-v2`
6+
7+
## Results
8+
- Focused Workspace Manager V2 launch/session refresh flow: passed 1/1.
9+
- Workspace Manager V2 suite: passed 16/16.
10+
11+
## Targeted Coverage
12+
- Verified Palette Manager V2 edit updates `workspace.tools.palette-manager-v2.data.swatches`.
13+
- Verified Palette Manager V2 edit marks `workspace.tools.palette-manager-v2.dirty.isDirty` true with `reason: "palette-updated"`.
14+
- Verified returning to Workspace Manager V2 refreshes the Palette Manager V2 tile to 12 swatches.
15+
- Verified Workspace Manager V2 reflects Palette Manager V2 dirty status from the normalized session.
16+
- Verified reopening Palette Manager V2 loads the edited palette swatch from session.
17+
- Verified Asset Manager V2 launches with 12 palette colors from `workspace.tools.palette-manager-v2.data`.
18+
- Verified Asset Manager V2 can add a color asset from the edited palette swatch.
19+
- Verified Asset Manager V2 writes the new asset to `workspace.tools.asset-manager-v2.data.assets`.
20+
- Verified Asset Manager V2 marks `workspace.tools.asset-manager-v2.dirty.isDirty` true with `reason: "asset-updated"`.
21+
- Verified returning to Workspace Manager V2 refreshes the Asset Manager V2 tile to 15 managed assets.
22+
- Verified Preview Generator V2 still launches and generates after refreshed tool sessions.
23+
- Verified Session Inspector V2 Delete All/reset behavior still clears shown session entries through the full suite.
24+
25+
## Skipped
26+
- Full samples smoke test was skipped by request. The relevant refresh, launch, persistence, dirty tracking, and reset paths are covered by `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Workspace Tool Session Refresh
2+
3+
## Scope
4+
- Added Workspace Manager V2 refresh-from-session behavior after tool session hydration.
5+
- Workspace Manager V2 now refreshes active tool display data from normalized `workspace.tools.<tool-id>` session objects.
6+
- Palette Manager V2 return refresh updates the Palette Manager V2 tile from `workspace.tools.palette-manager-v2.data.swatches`.
7+
- Tool tiles expose normalized dirty status from each tool session.
8+
- Asset Manager V2 now reads both its asset payload and the active palette from normalized session keys on launch.
9+
- Asset Manager V2 writes asset changes back to `workspace.tools.asset-manager-v2`.
10+
11+
## Session Rules
12+
- Workspace Manager V2 hydrates manifest defaults only when a normalized tool session is missing or invalid for the current game/context.
13+
- Valid dirty sessions are preserved when returning from a tool.
14+
- Workspace Manager V2 refreshes its active context from normalized session `data` before rendering tool counts and before later launches/exports.
15+
- Asset Manager V2 reads:
16+
- `workspace.tools.asset-manager-v2.data.assets`
17+
- `workspace.tools.palette-manager-v2.data.swatches`
18+
- Asset Manager V2 writes:
19+
- `workspace.tools.asset-manager-v2.data.assets`
20+
- `workspace.tools.asset-manager-v2.dirty`
21+
22+
## Dirty Tracking
23+
- Palette Manager V2 dirty data continues to use `reason: "palette-updated"`.
24+
- Asset Manager V2 dirty data now uses `reason: "asset-updated"` when asset data changes.
25+
- Asset Manager V2 dirty updates include:
26+
- `isDirty: true`
27+
- `changedAt`: current ISO timestamp
28+
- `changedKeys`: changed asset data paths.
29+
- Returning to Workspace Manager V2 without asset data changes does not create a new dirty timestamp.
30+
31+
## Validation Notes
32+
- Palette edits update session data and dirty tracking.
33+
- Returning to Workspace Manager V2 updates the Palette Manager V2 tile from 11 to 12 swatches.
34+
- Workspace Manager V2 reflects Palette Manager V2 dirty status from the normalized session.
35+
- Reopening Palette Manager V2 reloads the edited swatch from session.
36+
- Asset Manager V2 loads the edited palette swatch from `workspace.tools.palette-manager-v2.data`.
37+
- Asset Manager V2 writes a new color asset to `workspace.tools.asset-manager-v2.data` and marks its dirty object.
38+
- Returning to Workspace Manager V2 updates Asset Manager V2 from 14 to 15 managed assets.
39+
40+
## Guardrails
41+
- No `game.manifest.json` write path was added.
42+
- No direct cross-tool communication was added.
43+
- Session storage remains the integration boundary.
44+
- No sample JSON was modified.
45+
- No roadmap content was modified.
46+
47+
## Skipped
48+
- Full samples smoke test was skipped by request. The affected Workspace Manager V2, Palette Manager V2, Asset Manager V2, Preview Generator V2, and Session Inspector V2 session paths are covered by `npm run test:workspace-v2`.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17281728
isDirty: true,
17291729
reason: "palette-updated"
17301730
});
1731+
await expect(paletteTile).toContainText("12 palette swatches");
1732+
await expect(paletteTile).toHaveAttribute("data-workspace-tool-dirty", "true");
1733+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Refreshed palette-manager-v2 from workspace\.tools\.palette-manager-v2\.data: 12 palette swatches; Dirty: true\./);
17311734
await expect(page.locator("#statusLog")).toHaveValue(/OK Restored repo destination from workspace\.repo\.reference for HTML-JavaScript-Gaming\./);
17321735
await expect(previewTile).toBeEnabled();
17331736
await expect(previewTile).toContainText("Schema-valid manifest");
@@ -1739,6 +1742,53 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17391742
await page.locator("#returnToWorkspaceButton").click();
17401743
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
17411744
await expectWorkspaceReturnRehydrated(page);
1745+
await expect(paletteTile).toContainText("12 palette swatches");
1746+
await expect(paletteTile).toHaveAttribute("data-workspace-tool-dirty", "true");
1747+
1748+
await assetTile.click();
1749+
await expect(page).toHaveURL(/asset-manager-v2\/index\.html.*launch=workspace/);
1750+
await expect(page.locator("#statusLog")).toHaveValue(/Workspace Manager V2 loaded 14 validated assets from tools\.asset-manager-v2\.assets/);
1751+
await expect(page.locator("#statusLog")).toHaveValue(/Workspace Manager V2 loaded 12 palette colors from active palette context/);
1752+
await page.locator("#assetKindColor").check();
1753+
const sessionPurpleSwatch = page.locator('#assetColorSwatchList button[aria-label*="Workspace Session Purple"]');
1754+
await expect(sessionPurpleSwatch).toBeVisible();
1755+
await sessionPurpleSwatch.click();
1756+
await page.locator("#assetUsageInput").fill("session");
1757+
await expect(page.locator("#assetIdInput")).toHaveValue("assets.color.hud.session.workspace-session-purple");
1758+
await page.locator("#addAssetButton").click();
1759+
await expect(page.locator("#statusLog")).toHaveValue(/OK Added assets\.color\.hud\.session\.workspace-session-purple\./);
1760+
await expect(page.locator("#statusLog")).toHaveValue(/OK workspace\.tools\.asset-manager-v2 now has 15 validated assets\./);
1761+
const editedAssetSession = await page.evaluate(() => JSON.parse(sessionStorage.getItem("workspace.tools.asset-manager-v2")));
1762+
expect(Object.keys(editedAssetSession.data.assets)).toHaveLength(15);
1763+
expect(editedAssetSession.data.assets["assets.color.hud.session.workspace-session-purple"]).toMatchObject({
1764+
color: {
1765+
hex: "#123456",
1766+
name: "Workspace Session Purple",
1767+
source: "User Added",
1768+
symbol: "@"
1769+
},
1770+
kind: "hex",
1771+
path: "palette://workspace/workspace-session-purple",
1772+
role: "hud",
1773+
type: "color"
1774+
});
1775+
expect(editedAssetSession.dirty).toMatchObject({
1776+
isDirty: true,
1777+
reason: "asset-updated"
1778+
});
1779+
expect(Date.parse(editedAssetSession.dirty.changedAt)).not.toBeNaN();
1780+
expect(editedAssetSession.dirty.changedKeys).toEqual(expect.arrayContaining([
1781+
"data.assets",
1782+
'data.assets["assets.color.hud.session.workspace-session-purple"]'
1783+
]));
1784+
await page.locator("#returnToWorkspaceButton").click();
1785+
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
1786+
await expectWorkspaceReturnRehydrated(page);
1787+
await expect(assetTile).toContainText("15 managed assets");
1788+
await expect(assetTile).toHaveAttribute("data-workspace-tool-dirty", "true");
1789+
await expect(paletteTile).toContainText("12 palette swatches");
1790+
await expect(paletteTile).toHaveAttribute("data-workspace-tool-dirty", "true");
1791+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Refreshed asset-manager-v2 from workspace\.tools\.asset-manager-v2\.data: 15 managed assets; Dirty: true\./);
17421792
await page.locator('[data-workspace-tool-id="preview-generator-v2"]').click();
17431793
await expect(page).toHaveURL(/preview-generator-v2\/index\.html.*launch=workspace/);
17441794
await expect(page.locator('[data-launch-mode-nav="tool"]')).toBeHidden();

tools/asset-manager-v2/js/AssetManagerV2App.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,12 @@ export class AssetManagerV2App {
273273
this.selectedAssetId = formValue.assetId;
274274
this.assetForm.clearEditableFields();
275275
this.statusLog.ok(`Added ${formValue.assetId}.`);
276-
this.syncWorkspaceSessionManifest();
276+
this.syncWorkspaceSessionManifest({
277+
changedKeys: [
278+
"data.assets",
279+
`data.assets["${formValue.assetId}"]`
280+
]
281+
});
277282
this.render();
278283
this.refreshActions();
279284
}
@@ -315,7 +320,12 @@ export class AssetManagerV2App {
315320
this.selectedAssetId = formValue.assetId;
316321
this.assetForm.loadAssetForEdit(formValue.assetId, entryResult.entry);
317322
this.statusLog.ok(`Updated ${formValue.assetId}.`);
318-
this.syncWorkspaceSessionManifest();
323+
this.syncWorkspaceSessionManifest({
324+
changedKeys: [
325+
"data.assets",
326+
`data.assets["${this.selectedAssetId}"]`
327+
]
328+
});
319329
this.render();
320330
this.refreshActions();
321331
}
@@ -341,7 +351,12 @@ export class AssetManagerV2App {
341351
this.assetForm.clearEditableFields();
342352
}
343353
this.statusLog.ok(`Deleted ${assetId}.`);
344-
this.syncWorkspaceSessionManifest();
354+
this.syncWorkspaceSessionManifest({
355+
changedKeys: [
356+
"data.assets",
357+
`data.assets["${assetId}"]`
358+
]
359+
});
345360
this.render();
346361
this.refreshActions();
347362
}
@@ -373,7 +388,7 @@ export class AssetManagerV2App {
373388
} else {
374389
this.assetForm.clearEditableFields();
375390
}
376-
this.syncWorkspaceSessionManifest();
391+
this.syncWorkspaceSessionManifest({ changedKeys: ["data.assets"] });
377392
this.render();
378393
this.refreshActions();
379394
}
@@ -501,7 +516,7 @@ export class AssetManagerV2App {
501516
this.redoStack = [];
502517
this.statusLog.ok(`Imported JSON with ${Object.keys(this.assets).length} validated assets.`);
503518
this.missingFileAssetIds = await this.logMissingReferencedFiles(this.assets);
504-
this.syncWorkspaceSessionManifest();
519+
this.syncWorkspaceSessionManifest({ changedKeys: ["data.assets"] });
505520
this.render();
506521
this.refreshActions();
507522
} catch (error) {
@@ -565,22 +580,22 @@ export class AssetManagerV2App {
565580
this.window.location.href = this.workspaceBridge.workspaceManagerUrl();
566581
}
567582

568-
syncWorkspaceSessionManifest({ quiet = false } = {}) {
583+
syncWorkspaceSessionManifest({ changedKeys = ["data.assets"], quiet = false } = {}) {
569584
if (!this.workspaceBridge.isWorkspaceMode()) {
570585
return { ok: true, skipped: true };
571586
}
572587
const validation = this.validateCurrentPayload({ showInspector: false });
573588
if (!validation.ok) {
574589
return validation;
575590
}
576-
const result = this.workspaceBridge.writeAssetsPayload(validation.payload);
591+
const result = this.workspaceBridge.writeAssetsPayload(validation.payload, changedKeys);
577592
if (!result.ok) {
578-
this.statusLog.fail(`Workspace session manifest update failed: ${result.message}`);
593+
this.statusLog.fail(`Workspace tool session update failed: ${result.message}`);
579594
return result;
580595
}
581596
this.lastWorkspaceManifest = result.workspaceManifest;
582597
if (!quiet) {
583-
this.statusLog.ok(`Workspace Manager V2 session manifest now has ${Object.keys(validation.payload.assets).length} validated assets.`);
598+
this.statusLog.ok(`workspace.tools.asset-manager-v2 now has ${Object.keys(validation.payload.assets).length} validated assets.`);
584599
}
585600
return result;
586601
}

tools/asset-manager-v2/js/services/WorkspaceBridge.js

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ function isWorkspaceManifest(value) {
3333
}
3434

3535
const PALETTE_MANAGER_V2_TOOL_KEY = "palette-manager-v2";
36+
const ASSET_MANAGER_V2_TOOL_KEY = "asset-manager-v2";
37+
const WORKSPACE_TOOL_SESSION_KEY_PREFIX = "workspace.tools.";
38+
39+
function toolSessionKey(toolId) {
40+
return `${WORKSPACE_TOOL_SESSION_KEY_PREFIX}${toolId}`;
41+
}
42+
43+
function normalizedToolSessionError(key) {
44+
return `${key} must use the normalized schema/workspace/data/dirty object shape.`;
45+
}
3646

3747
export class WorkspaceBridge {
3848
constructor({ windowRef = window }) {
@@ -135,6 +145,53 @@ export class WorkspaceBridge {
135145
}
136146
}
137147

148+
readSessionJson(key) {
149+
const rawValue = this.window.sessionStorage.getItem(key);
150+
if (!rawValue) {
151+
return { ok: false, message: `${key} was not found in sessionStorage.` };
152+
}
153+
try {
154+
const value = JSON.parse(rawValue);
155+
return isPlainObject(value)
156+
? { ok: true, value }
157+
: { ok: false, message: `${key} must contain a JSON object.` };
158+
} catch (error) {
159+
return { ok: false, message: `${key} contains invalid JSON: ${error.message}` };
160+
}
161+
}
162+
163+
readWorkspaceToolSession(toolId, contextResult = null) {
164+
const context = contextResult || this.readContext();
165+
if (!context.ok) {
166+
return context;
167+
}
168+
const key = toolSessionKey(toolId);
169+
const sessionResult = this.readSessionJson(key);
170+
if (!sessionResult.ok) {
171+
return sessionResult;
172+
}
173+
const session = sessionResult.value;
174+
if (!isPlainObject(session.schema)
175+
|| !isPlainObject(session.workspace)
176+
|| !Object.prototype.hasOwnProperty.call(session, "data")
177+
|| !isPlainObject(session.dirty)) {
178+
return { ok: false, message: normalizedToolSessionError(key) };
179+
}
180+
if (session.workspace.source !== "workspace-manager-v2") {
181+
return { ok: false, message: `${key}.workspace.source must be workspace-manager-v2.` };
182+
}
183+
if (session.workspace.toolId !== toolId) {
184+
return { ok: false, message: `${key}.workspace.toolId must match ${toolId}.` };
185+
}
186+
if (session.workspace.workspaceManifestId !== context.context.id) {
187+
return { ok: false, message: `${key}.workspace.workspaceManifestId must match ${context.context.id}.` };
188+
}
189+
if (session.workspace.gameId !== context.gameId) {
190+
return { ok: false, message: `${key}.workspace.gameId must match ${context.gameId}.` };
191+
}
192+
return { ok: true, context, key, session };
193+
}
194+
138195
workspaceManifestFromContext(context) {
139196
if (isWorkspaceManifest(context)) {
140197
return context;
@@ -147,13 +204,13 @@ export class WorkspaceBridge {
147204
if (!contextResult.ok) {
148205
return contextResult;
149206
}
150-
const workspaceManifest = this.workspaceManifestFromContext(contextResult.context);
151-
if (!isPlainObject(workspaceManifest)) {
152-
return { ok: false, message: "Workspace Manager V2 manifest context is invalid." };
207+
const sessionResult = this.readWorkspaceToolSession(ASSET_MANAGER_V2_TOOL_KEY, contextResult);
208+
if (!sessionResult.ok) {
209+
return sessionResult;
153210
}
154-
const assetPayload = workspaceManifest.tools?.["asset-manager-v2"];
211+
const assetPayload = sessionResult.session.data;
155212
if (!isPlainObject(assetPayload) || !isPlainObject(assetPayload.assets)) {
156-
return { ok: false, message: "Workspace Manager V2 manifest is missing tools.asset-manager-v2.assets." };
213+
return { ok: false, message: `${sessionResult.key}.data.assets must contain the active workspace assets.` };
157214
}
158215
return {
159216
ok: true,
@@ -168,9 +225,13 @@ export class WorkspaceBridge {
168225
if (!contextResult.ok) {
169226
return contextResult;
170227
}
171-
const activePalette = contextResult.activePalette;
228+
const sessionResult = this.readWorkspaceToolSession(PALETTE_MANAGER_V2_TOOL_KEY, contextResult);
229+
if (!sessionResult.ok) {
230+
return sessionResult;
231+
}
232+
const activePalette = sessionResult.session.data;
172233
if (!isPlainObject(activePalette) || !Array.isArray(activePalette.swatches)) {
173-
return { ok: false, message: "Workspace Manager V2 session context is missing active palette swatches." };
234+
return { ok: false, message: `${sessionResult.key}.data.swatches must contain the active workspace palette.` };
174235
}
175236
return { ok: true, swatches: clone(activePalette.swatches) };
176237
}
@@ -201,27 +262,57 @@ export class WorkspaceBridge {
201262
};
202263
}
203264

204-
writeAssetsPayload(payload) {
265+
writeAssetsPayload(payload, changedKeys = ["data.assets"]) {
205266
if (!this.isWorkspaceMode()) {
206267
return { ok: false, message: "Asset insertion is only available when launched from Workspace Manager V2." };
207268
}
208269
const contextResult = this.readContext();
209270
if (!contextResult.ok) {
210271
return contextResult;
211272
}
273+
const sessionResult = this.readWorkspaceToolSession(ASSET_MANAGER_V2_TOOL_KEY, contextResult);
274+
if (!sessionResult.ok) {
275+
return sessionResult;
276+
}
212277
const workspaceManifest = clone(contextResult.context);
213278
if (!isPlainObject(workspaceManifest)) {
214279
return { ok: false, message: "Workspace Manager V2 manifest context is invalid." };
215280
}
216281
if (!isPlainObject(workspaceManifest.tools)) {
217282
return { ok: false, message: "Workspace Manager V2 session context is missing tools." };
218283
}
219-
if (!isPlainObject(workspaceManifest.tools["asset-manager-v2"]) || !isPlainObject(workspaceManifest.tools["asset-manager-v2"].assets)) {
220-
return { ok: false, message: "Workspace Manager V2 manifest is missing tools.asset-manager-v2.assets." };
284+
if (!isPlainObject(payload?.assets)) {
285+
return { ok: false, message: "Asset Manager V2 payload must include assets." };
221286
}
222287

223-
workspaceManifest.tools["asset-manager-v2"].assets = clone(payload.assets);
224-
this.window.sessionStorage.setItem(contextResult.hostContextId, JSON.stringify(workspaceManifest));
225-
return { ok: true, workspaceManifest: clone(workspaceManifest) };
288+
const session = sessionResult.session;
289+
const currentAssets = isPlainObject(session.data?.assets) ? session.data.assets : {};
290+
const nextData = {
291+
...(isPlainObject(session.data) ? session.data : {}),
292+
assets: clone(payload.assets)
293+
};
294+
const assetsChanged = JSON.stringify(currentAssets) !== JSON.stringify(nextData.assets);
295+
const nextSession = {
296+
...session,
297+
data: nextData,
298+
dirty: assetsChanged
299+
? {
300+
isDirty: true,
301+
reason: "asset-updated",
302+
changedAt: new Date().toISOString(),
303+
changedKeys: Array.from(new Set((Array.isArray(changedKeys) ? changedKeys : ["data.assets"])
304+
.map((key) => String(key || "").trim())
305+
.filter(Boolean)))
306+
}
307+
: session.dirty
308+
};
309+
this.window.sessionStorage.setItem(sessionResult.key, JSON.stringify(nextSession));
310+
workspaceManifest.tools[ASSET_MANAGER_V2_TOOL_KEY] = clone(nextData);
311+
return {
312+
ok: true,
313+
changed: assetsChanged,
314+
session: clone(nextSession),
315+
workspaceManifest: clone(workspaceManifest)
316+
};
226317
}
227318
}

0 commit comments

Comments
 (0)