Skip to content

Commit 3a97a03

Browse files
author
DavidQ
committed
Add explicit resolved output path logging to Preview Generator V2 writes - PR_26128_030-preview-generator-write-path-logging
1 parent 631b84b commit 3a97a03

4 files changed

Lines changed: 250 additions & 6 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Playwright Preview Generator V2 Write Path Logging
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|logs actionable Preview Generator V2 output path resolution failures"`
5+
- `npm run test:workspace-v2`
6+
7+
## Results
8+
- Focused Preview Generator V2 write logging coverage: passed 2/2.
9+
- Workspace Manager V2 suite: passed 17/17.
10+
11+
## Targeted Coverage
12+
- Generated a Preview Generator V2 workspace preview image.
13+
- Verified the log shows `OK WRITE Asteroids`.
14+
- Verified the log shows resolved relative output path `games/Asteroids/assets/images/preview.svg`.
15+
- Verified the log shows handle-exposed absolute output path `HTML-JavaScript-Gaming/games/Asteroids/assets/images/preview.svg`.
16+
- Verified the log identifies source resolution context as `workspace.tools.preview-generator-v2.data`, selected game `Asteroids`, and resolved `assets/images` target.
17+
- Verified summary counts remain present after generation.
18+
- Verified failed target-directory resolution logs actionable `FAIL PATH` details.
19+
- Verified failed resolution logs relative output path, absolute output path state, and source resolution context.
20+
- Verified failed resolution increments the summary `Failed` count while preserving `Written` and `Skipped` counts.
21+
22+
## Skipped
23+
- Full samples smoke test was skipped by request. The relevant Preview Generator V2 successful write, path logging, failure logging, and Workspace Manager V2 launch 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+
# Preview Generator V2 Write Path Logging
2+
3+
## Scope
4+
- Added explicit per-write path logging to Preview Generator V2.
5+
- Successful writes now log `OK WRITE` with:
6+
- target label/game name
7+
- resolved relative output path
8+
- absolute output path when the repo handle exposes one
9+
- source resolution context
10+
- Preserved existing per-item `RUN`, `OUT`, `OK`, `WARN`, `FAIL`, and `SKIP` logging.
11+
- Preserved summary counts for:
12+
- `Written`
13+
- `Warnings`
14+
- `Failed`
15+
- `Skipped`
16+
17+
## Successful Write Logging
18+
- Workspace-launched Preview Generator V2 writes now identify:
19+
- `workspace.tools.preview-generator-v2.data`
20+
- selected game
21+
- resolved `assets/images` target
22+
- resolved relative preview path
23+
- handle-exposed absolute preview path when available.
24+
- The Asteroids workspace flow logs:
25+
- `OK WRITE Asteroids`
26+
- `Resolved relative output path: games/Asteroids/assets/images/preview.svg`
27+
- `Absolute output path: HTML-JavaScript-Gaming/games/Asteroids/assets/images/preview.svg`
28+
- `Source resolution context: workspace.tools.preview-generator-v2.data; selected game: Asteroids; resolved assets/images target: assets/images; target type: games`
29+
30+
## Failure Logging
31+
- Output path resolution now fails before capture/write when the target directory cannot be resolved.
32+
- Failure logs include:
33+
- `FAIL PATH`
34+
- target label
35+
- exact target directory error
36+
- relative output path resolution state
37+
- absolute output path resolution state when available
38+
- source resolution context.
39+
- Failed path resolution contributes to the existing summary `Failed` count.
40+
41+
## Guardrails
42+
- No hidden fallback output paths were added.
43+
- No sample JSON was modified.
44+
- No roadmap content was modified.
45+
- Existing batch logging and summary sections are preserved.
46+
47+
## Skipped
48+
- Full samples smoke test was skipped by request. The changed surface is Preview Generator V2 write logging and is covered by the focused successful-write and failed-path-resolution Playwright coverage inside `npm run test:workspace-v2`.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ async function openAssetManagerV2(page, query = "") {
1818
return server;
1919
}
2020

21+
async function openPreviewGeneratorV2(page, query = "") {
22+
const server = await startRepoServer();
23+
await coverageReporter.start(page);
24+
await page.goto(`${server.baseUrl}/tools/preview-generator-v2/index.html${query}`, { waitUntil: "networkidle" });
25+
return server;
26+
}
27+
2128
async function openToolsIndex(page) {
2229
const server = await startRepoServer();
2330
await coverageReporter.start(page);
@@ -186,6 +193,58 @@ async function readWorkspaceSessionHydration(page) {
186193
});
187194
}
188195

196+
async function installMissingGamePreviewRepoPicker(page) {
197+
await page.addInitScript(() => {
198+
function makeDirectoryHandle(name, children = {}, path = name) {
199+
return {
200+
children,
201+
kind: "directory",
202+
name,
203+
path,
204+
async getDirectoryHandle(childName, options = {}) {
205+
const child = children[childName];
206+
if (child?.kind === "directory") {
207+
return child;
208+
}
209+
if (options.create) {
210+
const created = makeDirectoryHandle(childName, {}, `${path}/${childName}`);
211+
children[childName] = created;
212+
return created;
213+
}
214+
throw new Error(`Missing directory: ${path}/${childName}`);
215+
},
216+
async getFileHandle(childName, options = {}) {
217+
const child = children[childName];
218+
if (child?.kind === "file") {
219+
return child;
220+
}
221+
if (options.create) {
222+
const created = {
223+
kind: "file",
224+
name: childName,
225+
path: `${path}/${childName}`,
226+
async createWritable() {
227+
return {
228+
async write() {},
229+
async close() {}
230+
};
231+
}
232+
};
233+
children[childName] = created;
234+
return created;
235+
}
236+
throw new Error(`Missing file: ${path}/${childName}`);
237+
}
238+
};
239+
}
240+
241+
window.showDirectoryPicker = async () => makeDirectoryHandle("HTML-JavaScript-Gaming", {
242+
games: makeDirectoryHandle("games", {}, "HTML-JavaScript-Gaming/games"),
243+
tools: makeDirectoryHandle("tools", {}, "HTML-JavaScript-Gaming/tools")
244+
});
245+
});
246+
}
247+
189248
async function expectSessionInspectorV2AccordionToggles(page, contentId) {
190249
const header = page.locator(`.accordion-v2__header[aria-controls="${contentId}"]`);
191250
const content = page.locator(`#${contentId}`);
@@ -1819,7 +1878,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18191878
await expect(page.locator("#log")).toContainText("Workspace launch repo context resolved from session storage; independent repo selection is not required.");
18201879
await expect(page.locator("#log")).not.toContainText("Direct preview write");
18211880
await expect(page.locator("#log")).not.toContainText("Resolved repoPath");
1822-
await expect(page.locator("#log")).not.toContainText("absolute preview output path");
18231881
await expect(page.locator("#log")).not.toContainText("Unable to resolve absolute repoRoot");
18241882
await expect(page.locator("#log")).not.toContainText("/__workspace-manager-v2/repo-root");
18251883
await expect(page.locator("#log")).not.toContainText("/__workspace-manager-v2/write-preview");
@@ -1838,6 +1896,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18381896
await expect(page.locator("#log")).toContainText("Starting execution...", { timeout: 20000 });
18391897
await expect(page.locator("#log")).toContainText("RUN Asteroids", { timeout: 20000 });
18401898
await expect(page.locator("#log")).toContainText("OUT games\\Asteroids\\assets\\images\\preview.svg", { timeout: 20000 });
1899+
await expect(page.locator("#log")).toContainText("OK WRITE Asteroids", { timeout: 20000 });
1900+
await expect(page.locator("#log")).toContainText("Resolved relative output path: games/Asteroids/assets/images/preview.svg", { timeout: 20000 });
1901+
await expect(page.locator("#log")).toContainText("Absolute output path: HTML-JavaScript-Gaming/games/Asteroids/assets/images/preview.svg", { timeout: 20000 });
1902+
await expect(page.locator("#log")).toContainText("Source resolution context: workspace.tools.preview-generator-v2.data; selected game: Asteroids; resolved assets/images target: assets/images; target type: games", { timeout: 20000 });
18411903
await expect(page.locator("#log")).toContainText("OK Asteroids", { timeout: 20000 });
18421904
await expect(page.locator("#log")).toContainText("Done.", { timeout: 20000 });
18431905
await expect(page.locator("#lastGeneratedImageMeta")).toHaveText("Last generated: Asteroids");
@@ -2046,6 +2108,41 @@ test.describe("Workspace Manager V2 bootstrap", () => {
20462108
}
20472109
});
20482110

2111+
test("logs actionable Preview Generator V2 output path resolution failures", async ({ page }) => {
2112+
await installMissingGamePreviewRepoPicker(page);
2113+
const server = await openPreviewGeneratorV2(page);
2114+
const pageErrors = [];
2115+
2116+
page.on("pageerror", (error) => {
2117+
pageErrors.push(error.message);
2118+
});
2119+
2120+
try {
2121+
await page.locator("#pickRepoBtn").click();
2122+
await expect(page.locator("#repoSelectedValue")).toHaveText("HTML-JavaScript-Gaming");
2123+
await page.locator("#targetTypeGames").check();
2124+
await page.locator("#baseUrl").fill(server.baseUrl);
2125+
await page.locator("#sampleList").fill("MissingGame");
2126+
await expect(page.locator("#executeBtn")).toBeEnabled();
2127+
2128+
await page.locator("#executeBtn").click();
2129+
const log = page.locator("#log");
2130+
await expect(log).toContainText("FAIL PATH MissingGame", { timeout: 10000 });
2131+
await expect(log).toContainText("Unable to resolve target directory: Missing directory: HTML-JavaScript-Gaming/games/MissingGame", { timeout: 10000 });
2132+
await expect(log).toContainText("relative output path: games/MissingGame/assets/images/preview.svg", { timeout: 10000 });
2133+
await expect(log).toContainText("absolute output path: HTML-JavaScript-Gaming/games/MissingGame/assets/images/preview.svg", { timeout: 10000 });
2134+
await expect(log).toContainText("source resolution context: preview-generator-v2 form controls; selected game: MissingGame; resolved assets/images target: assets/images; target type: games", { timeout: 10000 });
2135+
await expect(log).toContainText("Written: 0", { timeout: 10000 });
2136+
await expect(log).toContainText("Failed: 1", { timeout: 10000 });
2137+
await expect(log).toContainText("Skipped: 0", { timeout: 10000 });
2138+
await expect(log).toContainText("Done.", { timeout: 10000 });
2139+
expect(pageErrors).toEqual([]);
2140+
} finally {
2141+
await coverageReporter.stop(page);
2142+
await server.close();
2143+
}
2144+
});
2145+
20492146
test("loads Gravity Well and Pong manifests as current Workspace Manager V2 manifests", async ({ page }) => {
20502147
const server = await openWorkspaceManagerV2(page);
20512148
const pageErrors = [];

tools/preview-generator-v2/PreviewGeneratorV2App.js

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ function normalizeWorkspacePath(value) {
7272
.replace(/^\/+|\/+$/g, "");
7373
}
7474

75+
function normalizeOutputPath(value) {
76+
return normalizeWorkspacePath(value);
77+
}
78+
7579
function repoRootNameMatches(selectedRepoName, expectedRepoRoot) {
7680
const expected = String(expectedRepoRoot || "").trim();
7781
if (!expected) {
@@ -424,6 +428,59 @@ function getFullOutputPath(entry) {
424428
return `${getWriteFolderDisplayPath(entry)}\\${OUTPUT_NAME}`;
425429
}
426430

431+
function getFullOutputRelativePath(entry) {
432+
return normalizeOutputPath(`${getWriteFolderRelativePath(entry)}/${OUTPUT_NAME}`);
433+
}
434+
435+
function outputSourceContext(entry) {
436+
const selectedGame = entry.targetType === "games"
437+
? String(entry.name || workspacePreviewGameId || "").trim()
438+
: String(workspacePreviewGameId || "").trim();
439+
return [
440+
isWorkspaceManagerLaunch() ? `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.data` : "preview-generator-v2 form controls",
441+
`selected game: ${selectedGame || "(not a game target)"}`,
442+
`resolved assets/images target: ${getAssetFolderRelativePath() || "(empty)"}`,
443+
`target type: ${entry.targetType || "(unknown)"}`
444+
].join("; ");
445+
}
446+
447+
function resolveOutputPathState(entry) {
448+
try {
449+
const relativeOutputPath = getFullOutputRelativePath(entry);
450+
const repoRootPath = normalizeOutputPath(repoDirHandle?.path);
451+
return {
452+
ok: true,
453+
absoluteOutputPath: repoRootPath ? normalizeOutputPath(`${repoRootPath}/${relativeOutputPath}`) : "",
454+
relativeOutputPath,
455+
sourceContext: outputSourceContext(entry)
456+
};
457+
} catch (error) {
458+
return {
459+
ok: false,
460+
absoluteOutputPath: "",
461+
message: error.message,
462+
relativeOutputPath: "",
463+
sourceContext: outputSourceContext(entry)
464+
};
465+
}
466+
}
467+
468+
function absoluteOutputPathFromWrite(fileHandle, pathState) {
469+
return normalizeOutputPath(fileHandle?.path) || pathState.absoluteOutputPath || "";
470+
}
471+
472+
function logWritePath(label, pathState, writeResult) {
473+
logger.log(`OK WRITE ${label}`);
474+
logger.log(`Resolved relative output path: ${pathState.relativeOutputPath}`);
475+
const absoluteOutputPath = writeResult.absoluteOutputPath || pathState.absoluteOutputPath;
476+
logger.log(`Absolute output path: ${absoluteOutputPath || "unavailable (repo handle does not expose an absolute path)"}`);
477+
logger.log(`Source resolution context: ${pathState.sourceContext}`);
478+
}
479+
480+
function logOutputPathFailure(label, pathState, reason) {
481+
logger.log(`FAIL PATH ${label} (${reason}; relative output path: ${pathState.relativeOutputPath || "(unresolved)"}; absolute output path: ${pathState.absoluteOutputPath || "(unavailable)"}; source resolution context: ${pathState.sourceContext})`);
482+
}
483+
427484
function updateWriteFolderSampleLabel() {
428485
const assetFolder = getAssetFolderDisplayPath() || "assets/images";
429486
const targetType = ui.getSelectedTargetType();
@@ -703,7 +760,7 @@ async function shouldRewrite(targetDirHandle) {
703760
return { rewrite: false, reason: "existing-preview-without-capture-timeout" };
704761
}
705762

706-
async function writePreview(targetDirHandle, svgContent) {
763+
async function writePreview(targetDirHandle, svgContent, pathState) {
707764
const relativeOutputFolder = getAssetFolderRelativePath();
708765

709766
const targetDir = relativeOutputFolder
@@ -714,14 +771,32 @@ async function writePreview(targetDirHandle, svgContent) {
714771
const writable = await fileHandle.createWritable();
715772
await writable.write(svgContent);
716773
await writable.close();
774+
return {
775+
absoluteOutputPath: absoluteOutputPathFromWrite(fileHandle, pathState),
776+
relativeOutputPath: pathState.relativeOutputPath
777+
};
717778
}
718779

719780
async function processOne(entry, baseUrl, waitMs) {
720-
const targetDirHandle = await getTargetDirHandle(repoDirHandle, entry);
781+
const label = entry.targetType === "samples" ? entry.id : entry.name;
782+
const pathState = resolveOutputPathState(entry);
783+
if (!pathState.ok) {
784+
logOutputPathFailure(label, pathState, pathState.message);
785+
logger.log("");
786+
return { id: label, status: "failed", reason: `output-path-resolution-failed: ${pathState.message}` };
787+
}
788+
789+
let targetDirHandle;
790+
try {
791+
targetDirHandle = await getTargetDirHandle(repoDirHandle, entry);
792+
} catch (error) {
793+
logOutputPathFailure(label, pathState, `Unable to resolve target directory: ${error.message}`);
794+
logger.log("");
795+
return { id: label, status: "failed", reason: `output-path-resolution-failed: ${error.message}` };
796+
}
721797
const decision = await shouldRewrite(targetDirHandle);
722798

723799
ui.outputSummary.setWriteFolderActual(getWriteFolderDisplayPath(entry));
724-
const label = entry.targetType === "samples" ? entry.id : entry.name;
725800

726801
if (!decision.rewrite) {
727802
logger.log(`SKIP ${label} (${decision.reason})`);
@@ -750,7 +825,8 @@ async function processOne(entry, baseUrl, waitMs) {
750825
}
751826

752827
const svgContent = await capture.extractSvgFromFrame();
753-
await writePreview(targetDirHandle, svgContent);
828+
const writeResult = await writePreview(targetDirHandle, svgContent, pathState);
829+
logWritePath(label, pathState, writeResult);
754830
ui.setLastGeneratedImage(svgContent, label);
755831

756832
const stillHasTimeout = svgContent.includes(CAPTURE_TIMEOUT_MARKER);
@@ -766,7 +842,7 @@ async function processOne(entry, baseUrl, waitMs) {
766842
} catch (error) {
767843
const fallback = capture.buildFallbackSvg(`${CAPTURE_TIMEOUT_MARKER}: ${error.message}`);
768844
try {
769-
await writePreview(targetDirHandle, fallback);
845+
await writePreview(targetDirHandle, fallback, pathState);
770846
} catch (writeError) {
771847
const reason = `${error.message}; fallback write failed: ${writeError.message}`;
772848
logger.log(`FAIL ${label} (${reason})`);

0 commit comments

Comments
 (0)