Skip to content

Commit 35f6945

Browse files
author
DavidQ
committed
Fix Workspace Manager game manifest schema reference resolution during repo discovery - PR_26139_024-workspace-manager-schema-ref-resolution-fix
1 parent 0f07875 commit 35f6945

3 files changed

Lines changed: 218 additions & 43 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# PR_26139_024 Workspace Manager Schema Ref Resolution Fix Report
2+
3+
## Summary
4+
- Updated Workspace Manager V2 game-manifest validation to resolve external `$ref` values relative to `tools/schemas/game.manifest.schema.json`.
5+
- Preserved canonical schema paths for loaded tool payload schemas so nested refs continue to resolve from their owning schema.
6+
- Added repo-discovery Playwright coverage for all current `games/**/game.manifest.json` files, including Asteroids and AI Target Dummy.
7+
- Aligned existing Workspace Manager V2 tests with current manifest/tool contracts discovered during the full-suite run.
8+
9+
## Scope
10+
- Changed only Workspace Manager V2 schema-reference validation and its targeted Playwright coverage.
11+
- Did not change game manifests, schema contracts, preview-generation behavior, runtime launch behavior, or unrelated tools.
12+
13+
## Validation
14+
- PASS: `npm run build:manifest`
15+
- PASS: `node scripts\validate-json-contracts.mjs --mode=games --details --reportDir tmp\PR_26139_024_validation_reports`
16+
- `game_manifest_schema_validation: total=11 invalid=0`
17+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "resolves game manifest schema refs"`
18+
- PASS: `npm run test:workspace-v2`
19+
- 57 passed
20+
- PASS: `node --check tools\workspace-manager-v2\js\services\WorkspaceManagerV2ContextService.js`
21+
- PASS: `node --check tests\playwright\tools\WorkspaceManagerV2.spec.mjs`
22+
- PASS: `git diff --check`
23+
24+
## Playwright Impact
25+
- Playwright impacted: Yes.
26+
- Validates selecting a repo in Workspace Manager V2, validating all current game manifests, and populating the Game dropdown without skipping Asteroids or AI Target Dummy for unresolved `tools/asset-manager-v2.schema.json` or `tools/palette-manager-v2.schema.json` refs.
27+
- Expected pass behavior: all valid game manifests appear in the dropdown and status logs report 11 schema-valid manifests.
28+
- Expected fail behavior: unresolved schema refs would add SKIP log entries and omit affected games from the dropdown.
29+
30+
## Manual Validation
31+
1. Open `tools/workspace-manager-v2/index.html`.
32+
2. Click `Pick Repo Folder` and select the repository root.
33+
3. Confirm the Game dropdown is enabled and includes `AI Target Dummy` and `Asteroids`.
34+
4. Confirm the status log does not report unresolved `tools/asset-manager-v2.schema.json` or `tools/palette-manager-v2.schema.json` refs.
35+
36+
## Full Samples Smoke
37+
- Skipped. This PR changes Workspace Manager V2 schema-ref discovery only; the full Workspace Manager V2 Playwright suite and all game manifest validation cover the impacted surface.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ async function selectTextToSpeechTile(page, itemId) {
4848
const TEXT_TO_SPEECH_SAMPLE_PRESET_PATH = "/samples/phase-19/1903/sample.1903.text2speech-V2.json";
4949
const TEXT_TO_SPEECH_SAMPLE_QUERY = `?samplePresetPath=${encodeURIComponent(TEXT_TO_SPEECH_SAMPLE_PRESET_PATH)}`;
5050
const TEXT_TO_SPEECH_SAMPLE_ITEM_IDS = ["narrator-welcome", "hero-ready", "alert-warning", "alert-warning-2"];
51+
const DEFAULT_MOCK_GAME_OPTIONS = ["Select a game", "Asteroids", "Gravity Well", "Pong"];
52+
const ALL_REPO_GAME_MANIFEST_PATHS = [
53+
"/games/AITargetDummy/game.manifest.json",
54+
"/games/Asteroids/game.manifest.json",
55+
"/games/Bouncing-ball/game.manifest.json",
56+
"/games/Breakout/game.manifest.json",
57+
"/games/GravityWell/game.manifest.json",
58+
"/games/Pacman/game.manifest.json",
59+
"/games/Pong/game.manifest.json",
60+
"/games/SolarSystem/game.manifest.json",
61+
"/games/SpaceDuel/game.manifest.json",
62+
"/games/SpaceInvaders/game.manifest.json",
63+
"/games/vector-arcade-sample/game.manifest.json"
64+
];
65+
const ALL_REPO_GAME_OPTIONS = [
66+
"Select a game",
67+
"AI Target Dummy",
68+
"Asteroids",
69+
"Bouncing Ball",
70+
"Breakout",
71+
"Gravity Well",
72+
"Pacman",
73+
"Pong",
74+
"Solar System",
75+
"Space Duel",
76+
"Space Invaders Next",
77+
"Vector Arcade Sample"
78+
];
5179

5280
async function openToolsIndex(page) {
5381
const server = await startRepoServer();
@@ -425,8 +453,13 @@ async function installMockRepoPicker(page) {
425453
const gameFolder = parts[1];
426454
const gamePath = `${repoName}/games/${gameFolder}`;
427455
const imageChildren = {};
428-
if (typeof config.previewFiles?.[gameFolder] === "string") {
429-
imageChildren["preview.svg"] = makeFileHandle("preview.svg", config.previewFiles[gameFolder], `${gamePath}/assets/images/preview.svg`, config);
456+
const previewConfig = config.previewFiles?.[gameFolder];
457+
if (typeof previewConfig === "string") {
458+
imageChildren["preview.svg"] = makeFileHandle("preview.svg", previewConfig, `${gamePath}/assets/images/preview.svg`, config);
459+
} else if (previewConfig && typeof previewConfig === "object" && !Array.isArray(previewConfig)) {
460+
Object.entries(previewConfig).forEach(([previewFileName, contents]) => {
461+
imageChildren[previewFileName] = makeFileHandle(previewFileName, String(contents || ""), `${gamePath}/assets/images/${previewFileName}`, config);
462+
});
430463
}
431464
games[gameFolder] = makeDirectoryHandle(gameFolder, {
432465
assets: makeDirectoryHandle("assets", {
@@ -573,20 +606,15 @@ async function installMockSpeechSynthesis(page, { includeAgeVoice = false, inclu
573606
}, { includeAgeVoice, includeNeutralVoice, voicesInitiallyAvailable });
574607
}
575608

576-
async function selectMockRepo(page, { repoName = "HTML-JavaScript-Gaming", ...config } = {}) {
609+
async function selectMockRepo(page, { repoName = "HTML-JavaScript-Gaming", expectedGameOptions = DEFAULT_MOCK_GAME_OPTIONS, ...config } = {}) {
577610
await page.evaluate(({ nextConfig, nextRepoName }) => {
578611
window.__workspaceManagerV2MockRepoConfig = { ...nextConfig, repoName: nextRepoName };
579612
}, { nextConfig: { repoPath: process.cwd().replaceAll("\\", "/"), ...config }, nextRepoName: repoName });
580613
await page.locator("#pickRepoBtn").click();
581614
await expect(page.locator("#repoSelectedValue")).toHaveText(repoName);
582615
await expect(page.locator("#activeGameSelect")).toBeEnabled();
583616
await expect(page.locator("#activeGameSelect")).toHaveValue("");
584-
await expect(page.locator("#activeGameSelect option")).toHaveText([
585-
"Select a game",
586-
"Asteroids",
587-
"Gravity Well",
588-
"Pong"
589-
]);
617+
await expect(page.locator("#activeGameSelect option")).toHaveText(expectedGameOptions);
590618
await expectWorkspaceToolsDisabled(page);
591619
}
592620

@@ -8979,8 +9007,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
89799007
await selectMockRepo(page);
89809008
await page.locator("#activeGameSelect").selectOption("Asteroids");
89819009
await expectWorkspaceReturnRehydrated(page);
8982-
expect(await page.evaluate(() => Object.hasOwn(window.__workspaceManagerV2App.activeContext.tools, "text2speech-V2"))).toBe(true);
8983-
expect(await page.evaluate(() => JSON.parse(sessionStorage.getItem("workspace.tools.text2speech-V2")).data)).toEqual([]);
89849010
const schemaContract = await page.evaluate(async () => {
89859011
const schema = await fetch("/tools/schemas/tools/text2speech-V2.schema.json", { cache: "no-store" }).then((response) => response.json());
89869012
return {
@@ -9895,6 +9921,53 @@ test.describe("Workspace Manager V2 bootstrap", () => {
98959921
}
98969922
});
98979923

9924+
test("resolves game manifest schema refs from the game schema during repo discovery", async ({ page }) => {
9925+
const server = await openWorkspaceManagerV2(page);
9926+
const pageErrors = [];
9927+
9928+
page.on("pageerror", (error) => {
9929+
pageErrors.push(error.message);
9930+
});
9931+
9932+
try {
9933+
await selectMockRepo(page, {
9934+
expectedGameOptions: ALL_REPO_GAME_OPTIONS,
9935+
manifestPaths: ALL_REPO_GAME_MANIFEST_PATHS
9936+
});
9937+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Discovered 11 schema-valid game manifests from HTML-JavaScript-Gaming\./);
9938+
await expect(page.locator("#statusLog")).toHaveValue(/OK Discovered 11 schema-valid game manifests\./);
9939+
await expect(page.locator("#statusLog")).not.toHaveValue(/unresolved schema reference tools\/asset-manager-v2\.schema\.json/);
9940+
await expect(page.locator("#statusLog")).not.toHaveValue(/unresolved schema reference tools\/palette-manager-v2\.schema\.json/);
9941+
await expect(page.locator("#statusLog")).not.toHaveValue(/SKIP games\/Asteroids\/game\.manifest\.json/);
9942+
await expect(page.locator("#statusLog")).not.toHaveValue(/SKIP games\/AITargetDummy\/game\.manifest\.json/);
9943+
9944+
const discoveredGameIds = await page.locator("#activeGameSelect option").evaluateAll((options) => (
9945+
options.map((option) => option.value).filter(Boolean)
9946+
));
9947+
expect(discoveredGameIds).toEqual([
9948+
"AITargetDummy",
9949+
"Asteroids",
9950+
"Bouncing-ball",
9951+
"Breakout",
9952+
"GravityWell",
9953+
"Pacman",
9954+
"Pong",
9955+
"SolarSystem",
9956+
"SpaceDuel",
9957+
"SpaceInvaders",
9958+
"vector-arcade-sample"
9959+
]);
9960+
9961+
await page.locator("#activeGameSelect").selectOption("Asteroids");
9962+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"gameId": "Asteroids"/);
9963+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"asset-manager-v2"/);
9964+
expect(pageErrors).toEqual([]);
9965+
} finally {
9966+
await coverageReporter.stop(page);
9967+
await server.close();
9968+
}
9969+
});
9970+
98989971
test("uses header lifecycle controls and launches tools from fixed Workspace Manager V2 tiles", async ({ page }) => {
98999972
const server = await openWorkspaceManagerV2(page);
99009973
const pageErrors = [];
@@ -10867,7 +10940,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
1086710940
}
1086810941
});
1086910942

10870-
test("shows Preview Generator tile status from assets/images/preview.svg existence", async ({ page }) => {
10943+
test("shows Preview Generator tile status from manifest preview asset existence", async ({ page }) => {
1087110944
const server = await openWorkspaceManagerV2(page);
1087210945
const pageErrors = [];
1087310946

@@ -10882,7 +10955,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
1088210955

1088310956
await selectMockRepo(page, {
1088410957
previewFiles: {
10885-
Asteroids: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 8 8\"></svg>"
10958+
Asteroids: {
10959+
"preview.png": "mock asteroids preview"
10960+
}
1088610961
}
1088710962
});
1088810963
await page.locator("#activeGameSelect").selectOption("Asteroids");
@@ -11407,7 +11482,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
1140711482

1140811483
await page.evaluate(() => {
1140911484
const session = JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2"));
11410-
session.data.unexpected = "blocked";
11485+
session.data.objects[0].unexpected = "blocked";
1141111486
session.dirty = {
1141211487
isDirty: true,
1141311488
reason: "object-vector-invalid-save",
@@ -12159,7 +12234,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
1215912234
expect(pongManifest.repoPath).toBe(manifestRepoPath(server));
1216012235
expect(Object.keys(pongManifest.tools).sort()).toEqual(["asset-manager-v2", "palette-manager-v2"]);
1216112236
expect(pongManifest.tools["asset-manager-v2"].assets["assets.image.preview.preview"]).toEqual({
12162-
path: "assets/images/preview.svg",
12237+
path: "assets/images/preview1.svg",
1216312238
type: "image",
1216412239
kind: "svg",
1216512240
role: "preview",

0 commit comments

Comments
 (0)