Skip to content

Commit 2c52f56

Browse files
author
DavidQ
committed
Continue schema resolution and Workspace Manager game discovery stabilization - PR_26139_025-schema-regression-followup
1 parent 35f6945 commit 2c52f56

3 files changed

Lines changed: 144 additions & 120 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# PR_26139_025-schema-regression-followup
2+
3+
## Summary
4+
- Normalized Workspace Manager V2 schema reference loading through one recursive external `$ref` registry builder.
5+
- Updated game manifest and generated workspace manifest validation to resolve referenced tool schemas relative to their owning schema roots.
6+
- Removed the duplicate generated-workspace shallow validation plus separate tool-payload validation path.
7+
- Added a Workspace Manager V2 regression proving workspace schema refs validate `asset-manager-v2` and `palette-manager-v2` payloads without unresolved-ref errors.
8+
9+
## Scope Notes
10+
- Changed only Workspace Manager V2 schema validation/discovery code and targeted Workspace Manager V2 Playwright coverage.
11+
- Tool schemas and game manifests were not changed.
12+
- Status-log spam cleanup was handled by removing the repeated validation path; no unrelated UI logging behavior was changed.
13+
14+
## Validation
15+
- PASS: `npm run build:manifest`
16+
- PASS: `node scripts\validate-json-contracts.mjs --mode=games --details`
17+
- `game_manifest_schema_validation: total=11 invalid=0`
18+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs -g "resolves workspace manifest tool schema refs|resolves game manifest schema refs"`
19+
- 2 passed.
20+
- PASS: `npm run test:workspace-v2`
21+
- 58 passed.
22+
23+
## Workspace Manager Checks
24+
- Select Repo path is covered by Workspace Manager V2 Playwright.
25+
- Game dropdown validation confirmed all 11 schema-valid games populate:
26+
`AITargetDummy`, `Asteroids`, `Bouncing-ball`, `Breakout`, `GravityWell`, `Pacman`, `Pong`, `SolarSystem`, `SpaceDuel`, `SpaceInvaders`, `vector-arcade-sample`.
27+
- Regression assertions confirm no unresolved `asset-manager-v2.schema.json` or `palette-manager-v2.schema.json` refs appear during repo discovery.
28+
- Generated workspace validation now reports real schema failures for invalid Asset Manager V2 and Palette Manager V2 payload fields instead of unresolved schema refs.
29+
30+
## Coverage
31+
- Playwright impacted: Yes.
32+
- V8 coverage report includes changed runtime JS:
33+
`(93%) tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js - executed lines 1778/1778; executed functions 156/168`
34+
35+
## Full Samples
36+
- Full samples smoke test was skipped.
37+
- Reason: scope is limited to Workspace Manager V2 schema validation/discovery stabilization; full Workspace V2 Playwright and all game manifest validation cover the impacted paths.
38+
39+
## Manual Validation
40+
1. Open Workspace Manager V2.
41+
2. Click `Pick Repo Folder` and select the repo root.
42+
3. Confirm the Game dropdown populates with all schema-valid games, including `Asteroids` and `AITargetDummy`.
43+
4. Confirm the Status log has no unresolved `asset-manager-v2.schema.json` or `palette-manager-v2.schema.json` messages.
44+
5. Select `Asteroids` and confirm workspace JSON is generated and tools remain launchable.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9968,6 +9968,47 @@ test.describe("Workspace Manager V2 bootstrap", () => {
99689968
}
99699969
});
99709970

9971+
test("resolves workspace manifest tool schema refs from the workspace schema", async ({ page }) => {
9972+
const server = await openWorkspaceManagerV2(page);
9973+
const pageErrors = [];
9974+
9975+
page.on("pageerror", (error) => {
9976+
pageErrors.push(error.message);
9977+
});
9978+
9979+
try {
9980+
await selectMockRepo(page);
9981+
await page.locator("#activeGameSelect").selectOption("Asteroids");
9982+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"gameId": "Asteroids"/);
9983+
9984+
const validation = await page.evaluate(async () => {
9985+
const app = window.__workspaceManagerV2App;
9986+
const validWorkspace = await app.contextService.validateGeneratedManifest(app.activeContext);
9987+
const invalidAssetWorkspace = structuredClone(app.activeContext);
9988+
invalidAssetWorkspace.tools["asset-manager-v2"].schemaRefProbe = true;
9989+
const invalidPaletteWorkspace = structuredClone(app.activeContext);
9990+
invalidPaletteWorkspace.tools["palette-manager-v2"].schemaRefProbe = true;
9991+
return {
9992+
asset: await app.contextService.validateGeneratedManifest(invalidAssetWorkspace),
9993+
palette: await app.contextService.validateGeneratedManifest(invalidPaletteWorkspace),
9994+
valid: validWorkspace
9995+
};
9996+
});
9997+
9998+
expect(validation.valid, validation.valid.message).toMatchObject({ ok: true });
9999+
expect(validation.asset.ok).toBe(false);
10000+
expect(validation.asset.message).toMatch(/root\.tools\.asset-manager-v2\.schemaRefProbe is not allowed/);
10001+
expect(validation.asset.message).not.toMatch(/unresolved schema reference/);
10002+
expect(validation.palette.ok).toBe(false);
10003+
expect(validation.palette.message).toMatch(/root\.tools\.palette-manager-v2\.schemaRefProbe is not allowed/);
10004+
expect(validation.palette.message).not.toMatch(/unresolved schema reference/);
10005+
expect(pageErrors).toEqual([]);
10006+
} finally {
10007+
await coverageReporter.stop(page);
10008+
await server.close();
10009+
}
10010+
});
10011+
997110012
test("uses header lifecycle controls and launches tools from fixed Workspace Manager V2 tiles", async ({ page }) => {
997210013
const server = await openWorkspaceManagerV2(page);
997310014
const pageErrors = [];

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

Lines changed: 59 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
const GAME_MANIFEST_SCHEMA_PATH = "/tools/schemas/game.manifest.schema.json";
33
const WORKSPACE_MANIFEST_SCHEMA_PATH = "/tools/schemas/workspace.manifest.schema.json";
44
const WORKSPACE_SESSION_SCHEMA_REF = "tools/schemas/workspace.manifest.schema.json";
5-
const OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH = "/tools/schemas/tools/object-vector-studio-v2.schema.json";
65
const WORKSPACE_REPO_REFERENCE_SESSION_KEY = "workspace.repo.reference";
76
const WORKSPACE_TOOL_SESSION_KEY_PREFIX = "workspace.tools.";
87
const WORKSPACE_REPO_HANDLE_DB_NAME = "workspace-manager-v2-repo-handles";
@@ -320,6 +319,24 @@ function unsupportedFields(value, schema) {
320319
.filter((key) => !Object.prototype.hasOwnProperty.call(properties, key));
321320
}
322321

322+
function collectExternalSchemaReferences(schema, refs = new Set()) {
323+
if (Array.isArray(schema)) {
324+
schema.forEach((entry) => collectExternalSchemaReferences(entry, refs));
325+
return refs;
326+
}
327+
if (!isPlainObject(schema)) {
328+
return refs;
329+
}
330+
if (typeof schema.$ref === "string") {
331+
const [refPath] = schema.$ref.split("#");
332+
if (refPath) {
333+
refs.add(refPath);
334+
}
335+
}
336+
Object.values(schema).forEach((entry) => collectExternalSchemaReferences(entry, refs));
337+
return refs;
338+
}
339+
323340
function resolvePointer(schema, pointer) {
324341
if (!pointer || pointer === "#") {
325342
return schema;
@@ -1290,12 +1307,47 @@ export class WorkspaceManagerV2ContextService {
12901307
if (!isPlainObject(schema)) {
12911308
return { ok: false, message: `${WORKSPACE_MANIFEST_SCHEMA_PATH} did not return a schema object.` };
12921309
}
1293-
return { ok: true, schema };
1310+
const schemaRegistry = new Map();
1311+
registerSchemaReference(schemaRegistry, WORKSPACE_MANIFEST_SCHEMA_PATH, schema);
1312+
const referenceLoad = await this.loadSchemaReferences(schema, WORKSPACE_MANIFEST_SCHEMA_PATH, schemaRegistry);
1313+
if (!referenceLoad.ok) {
1314+
return referenceLoad;
1315+
}
1316+
return { ok: true, schema, schemaPath: WORKSPACE_MANIFEST_SCHEMA_PATH, schemaRegistry };
12941317
} catch (error) {
12951318
return { ok: false, message: `Unable to load ${WORKSPACE_MANIFEST_SCHEMA_PATH}: ${error.message}` };
12961319
}
12971320
}
12981321

1322+
async loadSchemaReferences(rootSchema, rootSchemaPath, schemaRegistry, loadedSchemas = new Map()) {
1323+
for (const schemaRef of collectExternalSchemaReferences(rootSchema)) {
1324+
const resolvedSchemaPath = resolveSchemaPathFromBase(rootSchemaPath, schemaRef);
1325+
if (!resolvedSchemaPath) {
1326+
return { ok: false, message: `Unable to resolve schema reference ${schemaRef} from ${rootSchemaPath}.` };
1327+
}
1328+
let referencedSchema = loadedSchemas.get(resolvedSchemaPath);
1329+
if (!referencedSchema) {
1330+
const fetchPath = `/${resolvedSchemaPath}`;
1331+
const schemaResponse = await this.fetchRef(fetchPath, { cache: "no-store" });
1332+
if (!schemaResponse.ok) {
1333+
return { ok: false, message: `Unable to load ${fetchPath}: ${schemaResponse.status}` };
1334+
}
1335+
referencedSchema = await schemaResponse.json();
1336+
if (!isPlainObject(referencedSchema)) {
1337+
return { ok: false, message: `${fetchPath} did not return a schema object.` };
1338+
}
1339+
loadedSchemas.set(resolvedSchemaPath, referencedSchema);
1340+
registerSchemaReference(schemaRegistry, resolvedSchemaPath, referencedSchema);
1341+
const nestedReferences = await this.loadSchemaReferences(referencedSchema, resolvedSchemaPath, schemaRegistry, loadedSchemas);
1342+
if (!nestedReferences.ok) {
1343+
return nestedReferences;
1344+
}
1345+
}
1346+
registerSchemaReference(schemaRegistry, schemaRef, referencedSchema, resolvedSchemaPath);
1347+
}
1348+
return { ok: true };
1349+
}
1350+
12991351
async loadGameManifestSchema() {
13001352
if (typeof this.fetchRef !== "function") {
13011353
return { ok: false, message: "Fetch API is unavailable; Workspace Manager V2 cannot validate game manifests." };
@@ -1309,54 +1361,12 @@ export class WorkspaceManagerV2ContextService {
13091361
if (!isPlainObject(schema)) {
13101362
return { ok: false, message: `${GAME_MANIFEST_SCHEMA_PATH} did not return a schema object.` };
13111363
}
1312-
const assetManagerSchemaPath = `/${TOOL_PAYLOAD_SCHEMA_REFS[ASSET_MANAGER_V2_TOOL_KEY]}`;
1313-
const assetManagerResponse = await this.fetchRef(assetManagerSchemaPath, { cache: "no-store" });
1314-
if (!assetManagerResponse.ok) {
1315-
return { ok: false, message: `Unable to load ${assetManagerSchemaPath}: ${assetManagerResponse.status}` };
1316-
}
1317-
const assetManagerSchema = await assetManagerResponse.json();
1318-
if (!isPlainObject(assetManagerSchema)) {
1319-
return { ok: false, message: `${assetManagerSchemaPath} did not return a schema object.` };
1320-
}
1321-
const paletteManagerSchemaPath = `/${TOOL_PAYLOAD_SCHEMA_REFS[PALETTE_MANAGER_V2_TOOL_KEY]}`;
1322-
const paletteManagerResponse = await this.fetchRef(paletteManagerSchemaPath, { cache: "no-store" });
1323-
if (!paletteManagerResponse.ok) {
1324-
return { ok: false, message: `Unable to load ${paletteManagerSchemaPath}: ${paletteManagerResponse.status}` };
1325-
}
1326-
const paletteManagerSchema = await paletteManagerResponse.json();
1327-
if (!isPlainObject(paletteManagerSchema)) {
1328-
return { ok: false, message: `${paletteManagerSchemaPath} did not return a schema object.` };
1329-
}
1330-
const objectVectorResponse = await this.fetchRef(OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH, { cache: "no-store" });
1331-
if (!objectVectorResponse.ok) {
1332-
return { ok: false, message: `Unable to load ${OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH}: ${objectVectorResponse.status}` };
1333-
}
1334-
const objectVectorSchema = await objectVectorResponse.json();
1335-
if (!isPlainObject(objectVectorSchema)) {
1336-
return { ok: false, message: `${OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH} did not return a schema object.` };
1337-
}
13381364
const schemaRegistry = new Map();
1339-
registerSchemaReference(schemaRegistry, assetManagerSchemaPath, assetManagerSchema);
1340-
registerSchemaReference(schemaRegistry, TOOL_PAYLOAD_SCHEMA_REFS[ASSET_MANAGER_V2_TOOL_KEY], assetManagerSchema, assetManagerSchemaPath);
1341-
registerSchemaReference(schemaRegistry, "tools/asset-manager-v2.schema.json", assetManagerSchema, assetManagerSchemaPath);
1342-
registerSchemaReference(schemaRegistry, paletteManagerSchemaPath, paletteManagerSchema);
1343-
registerSchemaReference(schemaRegistry, TOOL_PAYLOAD_SCHEMA_REFS[PALETTE_MANAGER_V2_TOOL_KEY], paletteManagerSchema, paletteManagerSchemaPath);
1344-
registerSchemaReference(schemaRegistry, "tools/palette-manager-v2.schema.json", paletteManagerSchema, paletteManagerSchemaPath);
1345-
registerSchemaReference(schemaRegistry, OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH, objectVectorSchema);
1346-
registerSchemaReference(schemaRegistry, "tools/schemas/tools/object-vector-studio-v2.schema.json", objectVectorSchema, OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH);
1347-
registerSchemaReference(schemaRegistry, "tools/object-vector-studio-v2.schema.json", objectVectorSchema, OBJECT_VECTOR_STUDIO_V2_SCHEMA_PATH);
1348-
const text2SpeechSchemaPath = `/${TOOL_PAYLOAD_SCHEMA_REFS[TEXT2SPEECH_V2_TOOL_KEY]}`;
1349-
const text2SpeechResponse = await this.fetchRef(text2SpeechSchemaPath, { cache: "no-store" });
1350-
if (!text2SpeechResponse.ok) {
1351-
return { ok: false, message: `Unable to load ${text2SpeechSchemaPath}: ${text2SpeechResponse.status}` };
1352-
}
1353-
const text2SpeechSchema = await text2SpeechResponse.json();
1354-
if (!isPlainObject(text2SpeechSchema)) {
1355-
return { ok: false, message: `${text2SpeechSchemaPath} did not return a schema object.` };
1365+
registerSchemaReference(schemaRegistry, GAME_MANIFEST_SCHEMA_PATH, schema);
1366+
const referenceLoad = await this.loadSchemaReferences(schema, GAME_MANIFEST_SCHEMA_PATH, schemaRegistry);
1367+
if (!referenceLoad.ok) {
1368+
return referenceLoad;
13561369
}
1357-
registerSchemaReference(schemaRegistry, text2SpeechSchemaPath, text2SpeechSchema);
1358-
registerSchemaReference(schemaRegistry, TOOL_PAYLOAD_SCHEMA_REFS[TEXT2SPEECH_V2_TOOL_KEY], text2SpeechSchema, text2SpeechSchemaPath);
1359-
registerSchemaReference(schemaRegistry, "tools/text2speech-V2.schema.json", text2SpeechSchema, text2SpeechSchemaPath);
13601370
return { ok: true, schema, schemaPath: GAME_MANIFEST_SCHEMA_PATH, schemaRegistry };
13611371
} catch (error) {
13621372
return { ok: false, message: `Unable to load ${GAME_MANIFEST_SCHEMA_PATH}: ${error.message}` };
@@ -1387,83 +1397,12 @@ export class WorkspaceManagerV2ContextService {
13871397
return schemaResult;
13881398
}
13891399
const workspaceContext = workspaceContextWithObjectVectorPayload(manifest);
1390-
const errors = this.validateManifestAgainstSchema(workspaceContext, schemaResult.schema);
1391-
if (!errors.length) {
1392-
const toolValidation = await this.validateToolPayloads(workspaceContext, schemaResult.schema);
1393-
errors.push(...toolValidation.errors);
1394-
}
1400+
const errors = validateSchemaValue(workspaceContext, schemaResult.schema, "root", schemaResult.schema, schemaResult.schemaRegistry, schemaResult.schemaPath);
13951401
return errors.length
13961402
? { ok: false, message: `Generated Workspace Manager V2 manifest failed schema validation: ${errors.join(" | ")}` }
13971403
: { ok: true };
13981404
}
13991405

1400-
validateManifestAgainstSchema(manifest, schema) {
1401-
const errors = [];
1402-
if (!isPlainObject(manifest)) {
1403-
return ["root must be an object"];
1404-
}
1405-
missingRequiredFields(manifest, schema).forEach((key) => {
1406-
errors.push(`root.${key} is required`);
1407-
});
1408-
unsupportedFields(manifest, schema).forEach((key) => {
1409-
errors.push(`root.${key} is not allowed`);
1410-
});
1411-
1412-
if (!isPlainObject(manifest.tools)) {
1413-
errors.push("root.tools must be an object");
1414-
return errors;
1415-
}
1416-
const toolsSchema = schemaProperties(schema).tools || {};
1417-
const toolProperties = schemaProperties(toolsSchema);
1418-
missingRequiredFields(manifest.tools, toolsSchema).forEach((key) => {
1419-
errors.push(`root.tools.${key} is required`);
1420-
});
1421-
Object.keys(manifest.tools).forEach((key) => {
1422-
if (!Object.prototype.hasOwnProperty.call(toolProperties, key)) {
1423-
errors.push(`root.tools.${key} is not allowed`);
1424-
}
1425-
});
1426-
return errors;
1427-
}
1428-
1429-
async loadToolPayloadSchema(ref) {
1430-
if (typeof ref !== "string" || !ref.startsWith("./tools/")) {
1431-
return { ok: false, message: `Unsupported workspace manifest schema reference ${ref || "(empty)"}.` };
1432-
}
1433-
const schemaPath = `/tools/schemas/${ref.slice(2)}`;
1434-
try {
1435-
const response = await this.fetchRef(schemaPath, { cache: "no-store" });
1436-
if (!response.ok) {
1437-
return { ok: false, message: `Unable to load ${schemaPath}: ${response.status}` };
1438-
}
1439-
const schema = await response.json();
1440-
return isPlainObject(schema)
1441-
? { ok: true, schema }
1442-
: { ok: false, message: `${schemaPath} did not return a schema object.` };
1443-
} catch (error) {
1444-
return { ok: false, message: `Unable to load ${schemaPath}: ${error.message}` };
1445-
}
1446-
}
1447-
1448-
async validateToolPayloads(manifest, workspaceSchema) {
1449-
const errors = [];
1450-
const toolsSchema = schemaProperties(workspaceSchema).tools || {};
1451-
const toolProperties = schemaProperties(toolsSchema);
1452-
for (const [toolKey, payload] of Object.entries(manifest.tools || {})) {
1453-
const toolSchemaRef = toolProperties[toolKey]?.$ref;
1454-
if (!toolSchemaRef) {
1455-
continue;
1456-
}
1457-
const toolSchemaResult = await this.loadToolPayloadSchema(toolSchemaRef);
1458-
if (!toolSchemaResult.ok) {
1459-
errors.push(toolSchemaResult.message);
1460-
continue;
1461-
}
1462-
errors.push(...validateSchemaValue(payload, toolSchemaResult.schema, `root.tools.${toolKey}`, toolSchemaResult.schema));
1463-
}
1464-
return { ok: errors.length === 0, errors };
1465-
}
1466-
14671406
async fetchGameManifest(game) {
14681407
const manifestResult = await this.fetchWorkspaceManifest(game.manifestPath);
14691408
return manifestResult.ok

0 commit comments

Comments
 (0)