Skip to content

Commit cc31a77

Browse files
author
DavidQ
committed
Finish manifest-driven runtime cleanup after background asset resolution - PR_26139_016-final-manifest-runtime-cleanup
1 parent 5b7ac64 commit cc31a77

6 files changed

Lines changed: 237 additions & 79 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# PR_26139_016-final-manifest-runtime-cleanup Report
2+
3+
## Summary
4+
5+
- Removed remaining optional chrome asset path assumptions from shared runtime conventions.
6+
- Background, bezel, and preview image paths now resolve from Asset Manager V2 manifest image assets by `role` only.
7+
- Optional background/bezel/preview assets remain optional: if the role is absent, the runtime/tool path is empty and no hardcoded file is requested.
8+
- Workspace Manager V2 preview tile lookup no longer invents `assets/images/preview.svg`; it uses the manifest preview asset role when present.
9+
- Preserved Asteroids manifest object geometry, ship flame flicker, asteroid scale tuning, and the shared collision/transform path.
10+
11+
## Files Changed
12+
13+
- `src/engine/runtime/gameImageConvention.js`
14+
- `src/engine/runtime/fullscreenBezel.js`
15+
- `tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
16+
- `tests/core/BackgroundImageAndFullscreenBezel.test.mjs`
17+
- `tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs`
18+
- `docs/dev/reports/PR_26139_016-final-manifest-runtime-cleanup_report.md`
19+
20+
## Cleanup Decisions
21+
22+
- `resolveGameImageConventionPaths()` now returns empty optional chrome paths for background, bezel, and preview. It no longer constructs game-local optional image filenames.
23+
- `resolveManifestChromeAssetPaths()` selects image assets only from `tools.asset-manager-v2.assets` where `type: "image"` and `role` is `background`, `bezel`, or `preview`.
24+
- `fullscreenBezel` accepts a provided manifest payload and waits for manifest chrome resolution before attaching a bezel element.
25+
- Workspace Manager V2 preview status now resolves the preview path from the active game manifest Asset Manager `role: "preview"` entry.
26+
- The remaining `vectorMaps` reference in Object Vector Studio V2 is an intentional deprecated-input rejection guard, not active runtime/tool dependency.
27+
- Asset Manager authoring defaults, Asteroids gameplay state defaults, ship flame flicker, and asteroid scale tuning were preserved as intentional behavior.
28+
29+
## Asset Resolution Results
30+
31+
- Current Asteroids background role resolves to `/games/Asteroids/assets/images/deluxe.png`.
32+
- Current Asteroids bezel role resolves to `/games/Asteroids/assets/images/bezel.png`.
33+
- Current Asteroids preview role resolves to `games/Asteroids/assets/images/preview.png` for Workspace Manager V2 repo-relative preview status.
34+
- Removing the background role leaves the background layer unavailable without requesting `background.png` or `deluxe.png`.
35+
- Removing the bezel role leaves the bezel layer detached with `path: ""` and no request for `bezel.png`.
36+
- No optional background/bezel/preview fallback path is produced by shared runtime conventions.
37+
38+
## Validation
39+
40+
- PASS: `node --check src/engine/runtime/gameImageConvention.js`
41+
- PASS: `node --check src/engine/runtime/fullscreenBezel.js`
42+
- PASS: `node --check tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
43+
- PASS: `node --check tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs`
44+
- PASS: `node --check tests/core/BackgroundImageAndFullscreenBezel.test.mjs`
45+
- PASS: `npm run build:manifest`
46+
- PASS: `npx playwright test tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs --project=playwright --workers=1 --reporter=list`
47+
- 3 passed.
48+
- Verified no request for `/games/Asteroids/assets/images/background.png`.
49+
- Verified background image loads only from the Asset Manager background role.
50+
- Verified bezel image loads only from the Asset Manager bezel role.
51+
- Verified absent background role causes no optional background image request or 404.
52+
- Verified absent bezel role causes no optional bezel image request or 404.
53+
- PASS: `npx playwright test tests/playwright/tools/CollisionInspectorV2.spec.mjs --project=playwright --workers=1 --reporter=list`
54+
- 4 passed.
55+
- Verified Collision Inspector V2 remains on the shared manifest/collision path.
56+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "loads Object Vector Studio V2 runtime assets into Asteroids gameplay rendering"`
57+
- 1 passed.
58+
- Verified Asteroids manifest object loading and Object Vector Studio V2 runtime geometry path.
59+
- PASS: targeted Asteroids launch validation
60+
- Boot reached `boot-complete`.
61+
- Object Vector runtime loaded from manifest objects.
62+
- Background path was `/games/Asteroids/assets/images/deluxe.png`.
63+
- Bezel path was `/games/Asteroids/assets/images/bezel.png`.
64+
- No page errors or failed requests were observed.
65+
- PASS: targeted manifest chrome/Workspace preview validation
66+
- Convention paths returned empty optional chrome paths.
67+
- Manifest chrome resolved background, bezel, and preview from Asset Manager roles.
68+
- Removing preview role returned an empty Workspace Manager preview path.
69+
- PASS: `git diff --check`
70+
- Git emitted line-ending normalization warnings for two touched test files; command exit code was 0.
71+
72+
## Non-Gate Note
73+
74+
- Attempted `node -e "import('./tests/core/BackgroundImageAndFullscreenBezel.test.mjs').then((m)=>m.run())"`.
75+
- It failed at the existing fullscreen-host expectation before the new optional-asset assertions: `testResolvePreferredFullscreenTargetKeepsCanvasOnlyParent` still expects a canvas-only parent to remain the fullscreen target, while the current runtime wraps it in a dedicated fullscreen host.
76+
- This was treated as outside the PR_016 validation gate because the requested validations are the browser/runtime manifest asset paths and shared tool launch paths.
77+
78+
## Full Regression
79+
80+
- Full regression and full samples smoke were not run per the PR request.

src/engine/runtime/fullscreenBezel.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,9 @@ export default class fullscreenBezel {
733733
this.manifestPath = resolved.manifestPath;
734734
this.path = resolved.bezelPath;
735735
this.stretchConfigPath = resolveBezelStretchConfigPath(this.path, DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME, this.manifestPath);
736+
this.manifestPayload = options.manifestPayload && typeof options.manifestPayload === "object" && !Array.isArray(options.manifestPayload)
737+
? options.manifestPayload
738+
: null;
736739
this.host = null;
737740
this.element = null;
738741
this.ready = false;
@@ -780,7 +783,8 @@ export default class fullscreenBezel {
780783
this.manifestResolvePromise = resolveManifestChromeAssetPaths({
781784
gameId: this.gameId,
782785
manifestPath: this.manifestPath,
783-
documentRef: this.documentRef
786+
documentRef: this.documentRef,
787+
manifestPayload: this.manifestPayload
784788
})
785789
.then((resolved) => {
786790
this.gameId = resolved.gameId || this.gameId;

src/engine/runtime/gameImageConvention.js

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -152,50 +152,26 @@ function collectColorEntriesFromManifest(manifestPayload) {
152152
return entries;
153153
}
154154

155-
function chooseSemanticImagePath(entries, semanticToken) {
156-
const token = safeText(semanticToken, "").toLowerCase();
157-
if (!token) {
158-
return "";
159-
}
160-
161-
const normalizedEntries = Array.isArray(entries) ? entries : [];
162-
163-
const byId = normalizedEntries.find((entry) => {
164-
const id = safeText(entry?.id, "").toLowerCase();
165-
const role = safeText(entry?.role, "").toLowerCase();
166-
return role === token || id.includes(token);
167-
});
168-
if (byId?.path) {
169-
return byId.path;
170-
}
171-
172-
const byFileName = normalizedEntries.find((entry) => {
173-
const candidatePath = safeText(entry?.path, "").toLowerCase();
174-
return candidatePath.includes(`/${token}.`) || candidatePath.includes(`/${token}-`) || candidatePath.includes(`_${token}.`);
175-
});
176-
if (byFileName?.path) {
177-
return byFileName.path;
178-
}
179-
180-
return "";
181-
}
182-
183155
function chooseGameBackgroundColor(entries) {
184156
const normalizedEntries = Array.isArray(entries) ? entries : [];
185157
return normalizedEntries.find((entry) => entry.id === "assets.color.background.game")
186158
|| normalizedEntries.find((entry) => entry.role === "background" && entry.id.includes(".background.game"))
187159
|| null;
188160
}
189161

190-
function chooseAssetManagerBackgroundImagePath(entries) {
162+
function chooseAssetManagerImagePath(entries, semanticRole) {
191163
const normalizedEntries = Array.isArray(entries) ? entries : [];
192-
const backgroundAsset = normalizedEntries.find((entry) => (
164+
const role = safeText(semanticRole, "").toLowerCase();
165+
if (!role) {
166+
return "";
167+
}
168+
const asset = normalizedEntries.find((entry) => (
193169
entry?.sourceToolId === "asset-manager-v2"
194170
&& entry?.type === "image"
195-
&& entry?.role === "background"
171+
&& entry?.role === role
196172
&& safeText(entry?.path, "")
197173
));
198-
return backgroundAsset?.path || "";
174+
return asset?.path || "";
199175
}
200176

201177
const manifestCache = new Map();
@@ -274,7 +250,8 @@ export function resolveGameImageConventionPaths(options = {}) {
274250
backgroundColorName: "",
275251
backgroundColorPath: "",
276252
backgroundPath: "",
277-
bezelPath: gameId ? `games/${gameId}/assets/images/bezel.png` : ""
253+
bezelPath: "",
254+
previewPath: ""
278255
};
279256
}
280257

@@ -300,8 +277,9 @@ export async function resolveManifestChromeAssetPaths(options = {}) {
300277
backgroundColorHex: backgroundColor?.hex || "",
301278
backgroundColorName: backgroundColor?.name || "",
302279
backgroundColorPath: backgroundColor?.path || "",
303-
backgroundPath: chooseAssetManagerBackgroundImagePath(imageEntries),
304-
bezelPath: chooseSemanticImagePath(imageEntries, "bezel")
280+
backgroundPath: chooseAssetManagerImagePath(imageEntries, "background"),
281+
bezelPath: chooseAssetManagerImagePath(imageEntries, "bezel"),
282+
previewPath: chooseAssetManagerImagePath(imageEntries, "preview")
305283
};
306284
}
307285

tests/core/BackgroundImageAndFullscreenBezel.test.mjs

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import {
2020
} from "../../src/engine/runtime/gameImageConvention.js";
2121

2222
const ASTEROIDS_BACKGROUND_RUNTIME_PATH = "/games/Asteroids/assets/images/deluxe.png";
23+
const ASTEROIDS_BEZEL_RUNTIME_PATH = "/games/Asteroids/assets/images/bezel.png";
2324

24-
function createAssetManagerManifest({ includeBackground = true } = {}) {
25+
function createAssetManagerManifest({ includeBackground = true, includeBezel = true } = {}) {
2526
const assets = {
2627
"assets.color.background.game": {
2728
path: "palette://workspace/space-black",
@@ -33,13 +34,18 @@ function createAssetManagerManifest({ includeBackground = true } = {}) {
3334
name: "Space Black"
3435
}
3536
},
36-
"assets.image.bezel.bezel": {
37+
};
38+
if (includeBezel) {
39+
assets["assets.image.bezel.bezel"] = {
3740
path: "assets/images/bezel.png",
3841
type: "image",
3942
kind: "png",
40-
role: "bezel"
41-
}
42-
};
43+
role: "bezel",
44+
stretchOverride: {
45+
uniformEdgeStretchPx: 10
46+
}
47+
};
48+
}
4349
if (includeBackground) {
4450
assets["assets.image.background.deluxe"] = {
4551
path: "assets/images/deluxe.png",
@@ -57,6 +63,15 @@ function createAssetManagerManifest({ includeBackground = true } = {}) {
5763
};
5864
}
5965

66+
async function attachResolvedBezel(bezel, options = {}) {
67+
bezel.attach();
68+
if (bezel.manifestResolvePromise) {
69+
await bezel.manifestResolvePromise;
70+
}
71+
bezel.sync(options);
72+
return bezel.element;
73+
}
74+
6075
function createImageFactory(presentPaths, onCreate = null) {
6176
return (requestedPath) => {
6277
onCreate?.(String(requestedPath || ""));
@@ -245,10 +260,10 @@ async function testBackgroundManifestRenderOrder() {
245260
const manifestPayload = createAssetManagerManifest();
246261
const paths = resolveGameImageConventionPaths({ documentRef });
247262
assert.equal(paths.backgroundPath, "");
248-
assert.equal(paths.bezelPath, "games/Asteroids/assets/images/bezel.png");
263+
assert.equal(paths.bezelPath, "");
249264
const manifestPaths = await resolveManifestChromeAssetPaths({ documentRef, manifestPayload });
250265
assert.equal(manifestPaths.backgroundPath, ASTEROIDS_BACKGROUND_RUNTIME_PATH);
251-
assert.equal(manifestPaths.bezelPath, "/games/Asteroids/assets/images/bezel.png");
266+
assert.equal(manifestPaths.bezelPath, ASTEROIDS_BEZEL_RUNTIME_PATH);
252267
const runtimeBackgroundPath = resolveRuntimeAssetUrl(manifestPaths.backgroundPath, documentRef);
253268

254269
const layer = new backgroundImage({
@@ -296,7 +311,7 @@ async function testGameImageConventionsAreGameAgnostic() {
296311
documentRef: { location: { pathname: "/games/Asteroids/index.html" } }
297312
});
298313
assert.equal(asteroidsPaths.backgroundPath, "");
299-
assert.equal(asteroidsPaths.bezelPath, "games/Asteroids/assets/images/bezel.png");
314+
assert.equal(asteroidsPaths.bezelPath, "");
300315
assert.equal(
301316
resolveBezelStretchOverridePath({ documentRef: { location: { pathname: "/games/Asteroids/index.html" } } }),
302317
"/games/Asteroids/game.manifest.json"
@@ -311,7 +326,7 @@ async function testGameImageConventionsAreGameAgnostic() {
311326
documentRef: { location: { pathname: "/games/_template/index.html" } }
312327
});
313328
assert.equal(templatePaths.backgroundPath, "");
314-
assert.equal(templatePaths.bezelPath, "games/_template/assets/images/bezel.png");
329+
assert.equal(templatePaths.bezelPath, "");
315330
assert.equal(
316331
resolveBezelStretchOverridePath({ documentRef: { location: { pathname: "/games/_template/index.html" } } }),
317332
"/games/_template/game.manifest.json"
@@ -373,11 +388,11 @@ async function testNoOpWhenBackgroundMissing() {
373388
assert.equal(order.length, 0);
374389
}
375390

376-
function testSampleGameBackgroundAndBezelNoOpWhenMissing() {
391+
async function testSampleGameBackgroundAndBezelNoOpWhenMissing() {
377392
const documentRef = createDocumentStub("/games/SpaceInvaders/index.html");
378393
const paths = resolveGameImageConventionPaths({ documentRef });
379394
assert.equal(paths.backgroundPath, "");
380-
assert.equal(paths.bezelPath, "games/SpaceInvaders/assets/images/bezel.png");
395+
assert.equal(paths.bezelPath, "");
381396

382397
const backgroundLayer = new backgroundImage({
383398
documentRef,
@@ -398,32 +413,33 @@ function testSampleGameBackgroundAndBezelNoOpWhenMissing() {
398413
host.appendChild(canvas);
399414
documentRef.body.appendChild(host);
400415

401-
const bezel = new fullscreenBezel({ canvas, documentRef });
402-
bezel.attach();
403-
assert.equal(bezel.element.src, "/games/SpaceInvaders/assets/images/bezel.png");
404-
assert.equal(bezel.element.src.includes("/games/SpaceInvaders/games/SpaceInvaders/"), false);
405-
bezel.element.onerror?.();
416+
const bezel = new fullscreenBezel({
417+
canvas,
418+
documentRef,
419+
manifestPayload: createAssetManagerManifest({ includeBackground: false, includeBezel: false })
420+
});
421+
await attachResolvedBezel(bezel, { fullscreenActive: false, fullscreenElement: host });
422+
assert.equal(bezel.element, null);
406423
const bezelResult = bezel.sync({ fullscreenActive: true, fullscreenElement: host });
407424
assert.equal(bezelResult.visible, false);
408425
assert.equal(bezelResult.canvasLayoutMode, "fullscreen-fit");
409-
assert.equal(bezel.element.style.display, "none");
410426
assertNear(parseFloat(canvas.style.width), 1200, 0.6);
411427
assertNear(parseFloat(canvas.style.height), 900, 0.6);
412428
}
413429

414-
function testFullscreenBezelVisibilityAndHtmlAttachment() {
430+
async function testFullscreenBezelVisibilityAndHtmlAttachment() {
415431
const documentRef = createDocumentStub();
416432
const host = createElement("div", documentRef);
417433
const canvas = createElement("canvas", documentRef);
418434
host.appendChild(canvas);
419435
documentRef.body.appendChild(host);
420436

421-
const bezel = new fullscreenBezel({ canvas, documentRef });
422-
bezel.attach();
437+
const bezel = new fullscreenBezel({ canvas, documentRef, manifestPayload: createAssetManagerManifest() });
438+
await attachResolvedBezel(bezel, { fullscreenActive: false, fullscreenElement: host });
423439
assert.equal(bezel.getState().attached, true);
424440
assert.equal(bezel.element.parentElement, host);
425441
assert.equal(bezel.element.attributes["data-runtime-overlay"], "fullscreenBezel");
426-
assert.equal(bezel.element.src, "/games/Asteroids/assets/images/bezel.png");
442+
assert.equal(bezel.element.src, ASTEROIDS_BEZEL_RUNTIME_PATH);
427443
assert.equal(bezel.element.src.includes("/games/Asteroids/games/Asteroids/"), false);
428444

429445
bezel.element.onload?.();
@@ -447,7 +463,7 @@ function testFullscreenBezelVisibilityAndHtmlAttachment() {
447463
assert.equal(Number(bezel.element.style.zIndex) > Number(canvas.style.zIndex), true);
448464
}
449465

450-
function testNoOpWhenBezelMissing() {
466+
async function testNoOpWhenBezelMissing() {
451467
const documentRef = createDocumentStub("/games/MissingGame/index.html");
452468
const host = createElement("div", documentRef);
453469
const canvas = createElement("canvas", documentRef);
@@ -456,13 +472,16 @@ function testNoOpWhenBezelMissing() {
456472
host.appendChild(canvas);
457473
documentRef.body.appendChild(host);
458474

459-
const bezel = new fullscreenBezel({ canvas, documentRef });
460-
bezel.attach();
461-
bezel.element.onerror?.();
475+
const bezel = new fullscreenBezel({
476+
canvas,
477+
documentRef,
478+
manifestPayload: createAssetManagerManifest({ includeBackground: false, includeBezel: false })
479+
});
480+
await attachResolvedBezel(bezel, { fullscreenActive: false, fullscreenElement: host });
481+
assert.equal(bezel.element, null);
462482
const result = bezel.sync({ fullscreenActive: true, fullscreenElement: host });
463483
assert.equal(result.visible, false);
464484
assert.equal(result.canvasLayoutMode, "fullscreen-fit");
465-
assert.equal(bezel.element.style.display, "none");
466485
assertNear(parseFloat(canvas.style.width), 1200, 0.6);
467486
assertNear(parseFloat(canvas.style.height), 900, 0.6);
468487
}
@@ -1018,8 +1037,7 @@ async function testEngineRuntimeIntegration() {
10181037
assert.equal(engine.canvas.style.margin, "0 auto");
10191038
assert.equal(engine.canvas.style.width, "960px");
10201039
assert.equal(engine.canvas.style.height, "720px");
1021-
assert.equal(engine.fullscreenBezelLayer.element.src, "/games/Asteroids/assets/images/bezel.png");
1022-
assert.equal(engine.fullscreenBezelLayer.element.src.includes("/games/Asteroids/games/Asteroids/"), false);
1040+
assert.equal(engine.fullscreenBezelLayer.element, null);
10231041

10241042
engine.tick(1000);
10251043
await Promise.all([
@@ -1037,6 +1055,8 @@ async function testEngineRuntimeIntegration() {
10371055
`background:${ASTEROIDS_BACKGROUND_RUNTIME_PATH}`,
10381056
"scene"
10391057
]);
1058+
assert.equal(engine.fullscreenBezelLayer.element.src, ASTEROIDS_BEZEL_RUNTIME_PATH);
1059+
assert.equal(engine.fullscreenBezelLayer.element.src.includes("/games/Asteroids/games/Asteroids/"), false);
10401060

10411061
scene.session.mode = "playing";
10421062
order.length = 0;
@@ -1084,9 +1104,9 @@ export async function run() {
10841104
testResolvePreferredFullscreenTargetKeepsCanvasOnlyParent();
10851105
testResolvePreferredFullscreenTargetCreatesCanvasOnlyHost();
10861106
await testNoOpWhenBackgroundMissing();
1087-
testSampleGameBackgroundAndBezelNoOpWhenMissing();
1088-
testFullscreenBezelVisibilityAndHtmlAttachment();
1089-
testNoOpWhenBezelMissing();
1107+
await testSampleGameBackgroundAndBezelNoOpWhenMissing();
1108+
await testFullscreenBezelVisibilityAndHtmlAttachment();
1109+
await testNoOpWhenBezelMissing();
10901110
testMalformedBezelImageIsTreatedAsUnavailable();
10911111
testTransparentWindowDetectionAndAspectFit();
10921112
testFullscreenBezelTransparentWindowCanvasFit();

0 commit comments

Comments
 (0)