Skip to content

Commit 9b6602e

Browse files
author
DavidQ
committed
Rehydrate Workspace Manager V2 repo and active game after returning from launched tools - PR_26128_027-workspace-return-repo-rehydrate
1 parent c4bf667 commit 9b6602e

5 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Playwright Workspace Return Repo Rehydrate
2+
3+
## Targeted Coverage
4+
- Verified return from Asset Manager V2 repopulates Repo Destination from `workspace.repo.reference`.
5+
- Verified return from Palette Manager V2 repopulates Repo Destination from `workspace.repo.reference`.
6+
- Verified return from Preview Generator V2 repopulates Repo Destination from `workspace.repo.reference`.
7+
- Verified Active Game remains selected after valid returns.
8+
- Verified tool buttons remain enabled after valid repo/game return hydration.
9+
- Verified missing repo session reference shows an actionable Workspace Manager V2 restore failure.
10+
- Verified malformed repo session reference also blocks restore and keeps controls disabled.
11+
12+
## Commands
13+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
14+
- `node --check tools/workspace-manager-v2/js/WorkspaceManagerV2App.js`
15+
- `node --check tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
16+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "blocks Workspace Manager V2 return restore"`
17+
- `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"`
18+
- `npm run test:workspace-v2`
19+
20+
## Result
21+
- Focused restore failure validation passed.
22+
- Focused return rehydration flow passed.
23+
- Full workspace-v2 Playwright validation passed: 16 tests.
24+
25+
## Full Samples Smoke
26+
- Skipped per PR instructions because the requested change is scoped to Workspace Manager V2 return-from-tool session restoration.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# PR_26128_027 Workspace Return Repo Rehydrate
2+
3+
## Scope
4+
- Updated Workspace Manager V2 return-from-tool restore behavior.
5+
- Kept session storage as the integration boundary.
6+
- Preserved normalized `workspace.tools.<tool-id>` session keys.
7+
8+
## Changes
9+
- Workspace Manager V2 now reads `workspace.repo.reference` before restoring a returned `hostContextId`.
10+
- Repo Destination display is repopulated from the session repo reference on valid returns.
11+
- Active Game and tool buttons remain restored only when both the session workspace context and repo reference are valid.
12+
- Missing or invalid repo session references now show an actionable restore failure and keep repo/game/tool controls disabled.
13+
- Added validation for repo reference JSON shape, `source`, `kind`, optional `storageKey`, display name, and manifest repo root compatibility.
14+
15+
## Guardrails
16+
- No sample JSON files modified.
17+
- No roadmap files modified.
18+
- No cross-tool communication added.
19+
- No schema or normalized tool session key shape changes.
20+
21+
## Validation
22+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs` passed.
23+
- `node --check tools/workspace-manager-v2/js/WorkspaceManagerV2App.js` passed.
24+
- `node --check tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js` passed.
25+
- Focused missing/invalid repo reference restore test passed.
26+
- Focused Asset/Palette/Preview return flow test passed.
27+
- `npm run test:workspace-v2` passed: 16 tests.
28+
29+
## Full Samples Smoke
30+
- Skipped per PR instructions. The change is limited to Workspace Manager V2 session return restore behavior and targeted Workspace Manager V2 Playwright coverage validates the affected flows.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ async function expectWorkspaceToolsDisabled(page) {
147147
expect(await page.locator("#workspaceToolTiles [data-workspace-tool-id]").evaluateAll((tiles) => tiles.every((tile) => tile.disabled))).toBe(true);
148148
}
149149

150+
async function expectWorkspaceReturnRehydrated(page, { gameId = "Asteroids", repoName = "HTML-JavaScript-Gaming" } = {}) {
151+
await expect(page.locator("#repoSelectedValue")).toHaveText(repoName);
152+
await expect(page.locator("#activeGameSelect")).toHaveValue(gameId);
153+
await expect(page.locator("#exportManifestButton")).toBeEnabled();
154+
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
155+
await expect(page.locator('[data-workspace-tool-id="palette-manager-v2"]')).toBeEnabled();
156+
await expect(page.locator('[data-workspace-tool-id="preview-generator-v2"]')).toBeEnabled();
157+
await expect(page.locator('[data-workspace-tool-id="session-inspector-v2"]')).toBeEnabled();
158+
}
159+
150160
async function readWorkspaceSessionHydration(page) {
151161
return await page.evaluate(() => {
152162
const parseJson = (key) => {
@@ -1662,10 +1672,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16621672
expect(JSON.stringify(storedContext)).not.toMatch(/samples\//i);
16631673
await page.locator("#returnToWorkspaceButton").click();
16641674
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
1675+
await expectWorkspaceReturnRehydrated(page);
16651676
await expect(page.locator("#activeGameSelect")).toHaveValue("Asteroids");
16661677
await expect(page.locator("#activeAssetRegistrySummary")).toHaveCount(0);
16671678
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
16681679
await expect(page.locator("#exportManifestButton")).toBeEnabled();
1680+
await expect(page.locator("#statusLog")).toHaveValue(/OK Restored repo destination from workspace\.repo\.reference for HTML-JavaScript-Gaming\./);
16691681
await expect(page.locator("#statusLog")).toHaveValue(/OK Restored Asteroids workspace from session context workspace-manager-v2-/);
16701682

16711683
await page.locator('[data-workspace-tool-id="palette-manager-v2"]').click();
@@ -1679,6 +1691,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16791691
await expect(page.locator("#paletteStatus")).toHaveText("Loaded active workspace palette Asteroids Palette.");
16801692
await page.locator("#returnToWorkspaceButton").click();
16811693
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
1694+
await expectWorkspaceReturnRehydrated(page);
1695+
await expect(page.locator("#statusLog")).toHaveValue(/OK Restored repo destination from workspace\.repo\.reference for HTML-JavaScript-Gaming\./);
16821696
await expect(previewTile).toBeEnabled();
16831697
await expect(previewTile).toContainText("Schema-valid manifest");
16841698
await page.locator('[data-workspace-tool-id="preview-generator-v2"]').click();
@@ -1740,6 +1754,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17401754
expect(previewWrites[0].contents).not.toContain("Capture timeout");
17411755
await page.locator("#returnToWorkspaceButton").click();
17421756
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
1757+
await expectWorkspaceReturnRehydrated(page);
1758+
await expect(page.locator("#statusLog")).toHaveValue(/OK Restored repo destination from workspace\.repo\.reference for HTML-JavaScript-Gaming\./);
17431759
expect(pageErrors).toEqual([]);
17441760
} finally {
17451761
await coverageReporter.stop(page);
@@ -1790,6 +1806,50 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17901806
}
17911807
});
17921808

1809+
test("blocks Workspace Manager V2 return restore when repo session reference is missing or invalid", async ({ page }) => {
1810+
const pageErrors = [];
1811+
const hostContextId = "workspace-manager-v2-missing-return-repo-reference";
1812+
const gameManifest = JSON.parse(await readFile("games/Asteroids/game.manifest.json", "utf8"));
1813+
const manifest = gameManifest.game.workspace;
1814+
await page.addInitScript(({ contextId, workspaceManifest }) => {
1815+
window.sessionStorage.setItem(contextId, JSON.stringify(workspaceManifest));
1816+
}, { contextId: hostContextId, workspaceManifest: manifest });
1817+
const server = await openWorkspaceManagerV2(page, `?hostContextId=${hostContextId}`);
1818+
1819+
page.on("pageerror", (error) => {
1820+
pageErrors.push(error.message);
1821+
});
1822+
1823+
try {
1824+
await expect(page.locator("#repoSelectedValue")).toHaveText("not selected");
1825+
await expect(page.locator("#activeGameSelect")).toBeDisabled();
1826+
await expect(page.locator("#activeGameSelect option")).toHaveCount(0);
1827+
await expect(page.locator("#exportManifestButton")).toBeDisabled();
1828+
await expect(page.locator("#workspaceContextOutput")).toHaveValue("{}");
1829+
await expectWorkspaceToolsDisabled(page);
1830+
await expect(page.locator("#activeGameSummary")).toHaveText("workspace.repo.reference was not found in sessionStorage. Pick Repo Folder to reselect repo before launching tools.");
1831+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Workspace restore failed: workspace\.repo\.reference was not found in sessionStorage\. Pick Repo Folder to reselect repo before launching tools\./);
1832+
expect(await readWorkspaceSessionHydration(page)).toMatchObject({
1833+
repoReference: null,
1834+
toolKeys: []
1835+
});
1836+
1837+
await page.evaluate(({ contextId, workspaceManifest }) => {
1838+
window.sessionStorage.setItem(contextId, JSON.stringify(workspaceManifest));
1839+
window.sessionStorage.setItem("workspace.repo.reference", "not-json");
1840+
}, { contextId: hostContextId, workspaceManifest: manifest });
1841+
await page.goto(`${server.baseUrl}/tools/workspace-manager-v2/index.html?hostContextId=${hostContextId}`, { waitUntil: "networkidle" });
1842+
await expect(page.locator("#repoSelectedValue")).toHaveText("not selected");
1843+
await expect(page.locator("#activeGameSelect")).toBeDisabled();
1844+
await expectWorkspaceToolsDisabled(page);
1845+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Workspace restore failed: workspace\.repo\.reference contains invalid JSON:/);
1846+
expect(pageErrors).toEqual([]);
1847+
} finally {
1848+
await coverageReporter.stop(page);
1849+
await server.close();
1850+
}
1851+
});
1852+
17931853
test("opens Preview Generator V2 workspace launch with actionable missing repo session status", async ({ page }) => {
17941854
const server = await startRepoServer();
17951855
const pageErrors = [];

tools/workspace-manager-v2/js/WorkspaceManagerV2App.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,24 @@ export class WorkspaceManagerV2App {
190190
this.statusLog.fail(`Workspace restore failed: ${result.message}`);
191191
return;
192192
}
193+
const repoReferenceResult = this.contextService.readWorkspaceRepoReference({
194+
expectedRepoRoot: result.context.repoRoot || result.game.repoRoot || ""
195+
});
196+
if (!repoReferenceResult.ok) {
197+
const message = `${repoReferenceResult.message} Pick Repo Folder to reselect repo before launching tools.`;
198+
this.repoDestination.setRepoDestinationDisplayName("not selected");
199+
this.clearActiveWorkspace(message);
200+
this.statusLog.fail(`Workspace restore failed: ${message}`);
201+
return;
202+
}
203+
this.repoDestination.setRepoDestinationDisplayName(repoReferenceResult.reference.displayName);
193204
this.gameSelector.setValue(result.game.id, result.game.name);
194205
this.applyContextResult(result);
195206
if (result.assetWarning) {
196207
this.statusLog.info(`Warning: ${result.assetWarning}`);
197208
}
198209
this.reportSessionHydration();
210+
this.statusLog.ok(`Restored repo destination from workspace.repo.reference for ${repoReferenceResult.reference.displayName}.`);
199211
this.statusLog.ok(`Restored ${result.game.name} workspace from session context ${result.hostContextId}.`);
200212
}
201213

tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ function makeHostContextId() {
6565
return `workspace-manager-v2-${Date.now().toString(36)}`;
6666
}
6767

68+
function repoRootNameMatches(selectedRepoName, expectedRepoRoot) {
69+
const expected = String(expectedRepoRoot || "").trim();
70+
if (!expected) {
71+
return true;
72+
}
73+
if (selectedRepoName === expected) {
74+
return true;
75+
}
76+
const expectedFolderName = expected
77+
.replaceAll("\\", "/")
78+
.split("/")
79+
.filter(Boolean)
80+
.at(-1);
81+
return selectedRepoName === expectedFolderName;
82+
}
83+
6884
function toolSessionKey(toolId) {
6985
return `${WORKSPACE_TOOL_SESSION_KEY_PREFIX}${toolId}`;
7086
}
@@ -368,6 +384,49 @@ export class WorkspaceManagerV2ContextService {
368384
this.sessionStorage.removeItem(WORKSPACE_REPO_REFERENCE_SESSION_KEY);
369385
}
370386

387+
readSessionJson(key) {
388+
const rawValue = this.sessionStorage.getItem(key);
389+
if (!rawValue) {
390+
return { ok: false, message: `${key} was not found in sessionStorage.` };
391+
}
392+
try {
393+
const value = JSON.parse(rawValue);
394+
return isPlainObject(value)
395+
? { ok: true, value }
396+
: { ok: false, message: `${key} must contain a JSON object.` };
397+
} catch (error) {
398+
return { ok: false, message: `${key} contains invalid JSON: ${error.message}` };
399+
}
400+
}
401+
402+
readWorkspaceRepoReference({ expectedRepoRoot = "" } = {}) {
403+
const result = this.readSessionJson(WORKSPACE_REPO_REFERENCE_SESSION_KEY);
404+
if (!result.ok) {
405+
return result;
406+
}
407+
const reference = result.value;
408+
if (reference.source !== "workspace-manager-v2") {
409+
return { ok: false, message: `${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.source must be workspace-manager-v2.` };
410+
}
411+
if (reference.kind !== "file-system-directory-handle-reference") {
412+
return { ok: false, message: `${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.kind must be file-system-directory-handle-reference.` };
413+
}
414+
if (reference.storageKey && reference.storageKey !== WORKSPACE_REPO_REFERENCE_SESSION_KEY) {
415+
return { ok: false, message: `${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.storageKey must be ${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.` };
416+
}
417+
const displayName = String(reference.displayName || reference.handleName || "").trim();
418+
if (!displayName) {
419+
return { ok: false, message: `${WORKSPACE_REPO_REFERENCE_SESSION_KEY} must include displayName or handleName.` };
420+
}
421+
if (!repoRootNameMatches(displayName, expectedRepoRoot)) {
422+
return {
423+
ok: false,
424+
message: `${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.displayName ${displayName} does not match manifest repoRoot ${expectedRepoRoot}.`
425+
};
426+
}
427+
return { ok: true, reference: { ...reference, displayName } };
428+
}
429+
371430
hydrateRepoReference(repoHandle, displayName = "") {
372431
const repoName = String(displayName || repoHandle?.name || "selected").trim() || "selected";
373432
const reference = {

0 commit comments

Comments
 (0)