Skip to content

Commit 65f4ee8

Browse files
author
DavidQ
committed
Unblock Preview Generator V2 launch when repo root endpoint is unavailable - PR_26127_015-preview-generator-repo-root-resolution-fix
1 parent 3ab6796 commit 65f4ee8

6 files changed

Lines changed: 78 additions & 121 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# PR_26127_015-preview-generator-repo-root-resolution-fix
2+
3+
## Summary
4+
- Removed the Workspace Manager V2 pre-launch absolute repoRoot resolution fetch.
5+
- Removed the `/__workspace-manager-v2/repo-root` test-server path so launch no longer depends on that endpoint.
6+
- Workspace Manager V2 now passes the existing validated session/manifest context through to Preview Generator V2.
7+
- Preview Generator V2 now hydrates workspace launch even when manifest `repoRoot` is only a display label.
8+
9+
## Repo Root Resolution Notes
10+
- Display-only repo roots such as `HTML-JavaScript-Gaming` no longer block Preview Generator V2 from opening.
11+
- Preview Generator V2 status distinguishes:
12+
- workspace launch hydrated
13+
- repoRoot display label available
14+
- absolute repoRoot missing
15+
- direct preview write unavailable until a real writable repo root is selected
16+
- Generate Preview remains disabled when workspace launch has no absolute/writable repo root.
17+
- No hidden fallback behavior was added.
18+
19+
## Launch Behavior Notes
20+
- Workspace Manager V2 launch behavior is preserved for other tools.
21+
- Preview Generator V2 still preserves manifest-relative paths such as `games/Asteroids/assets/images/preview.svg`.
22+
- Direct write support remains available only when an absolute repoRoot is present and validated.
23+
24+
## Validation
25+
- `npm run test:workspace-v2`
26+
- Result: PASS, 10 tests passed.
27+
- Validated Preview Generator V2 launches from Workspace Manager V2 with display-only repoRoot, shows actionable status, keeps Generate Preview disabled, and does not call `/__workspace-manager-v2/repo-root`.
28+
29+
## Out Of Scope
30+
- Deprecated `tools/workspace-v2` was not modified.
31+
- Sample JSON was not modified.
32+
- Full samples smoke test was skipped because this PR is Preview Generator launch unblock scoped.

tests/helpers/playwrightRepoServer.mjs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ export async function startRepoServer() {
2929
try {
3030
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
3131
const decodedPath = decodeURIComponent(requestUrl.pathname);
32-
if (request.method === "GET" && decodedPath === "/__workspace-manager-v2/repo-root") {
33-
response.statusCode = 200;
34-
response.setHeader("Content-Type", "application/json; charset=utf-8");
35-
response.end(JSON.stringify({ repoRoot }));
36-
return;
37-
}
3832
if (request.method === "PUT" && decodedPath === "/__workspace-manager-v2/write-preview") {
3933
const absoluteWritePath = String(request.headers["x-workspace-preview-absolute-path"] || "");
4034
const relativeWritePath = String(request.headers["x-workspace-preview-relative-path"] || "");

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { expect, test } from "@playwright/test";
22
import { readFile } from "node:fs/promises";
3-
import path from "node:path";
43
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
54
import { workspaceV2CoverageReporter as coverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs";
65

@@ -522,8 +521,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
522521
await expect(page.locator('[data-launch-mode-nav="workspace"]')).toBeVisible();
523522
await expect(page.locator('[data-launch-mode-nav="workspace"] button')).toHaveText(["Generate Image", "Return to Workspace"]);
524523
await expect(page.locator("#executeBtn")).toBeVisible();
525-
await expect(page.locator("#executeBtn")).toBeEnabled();
526-
await expect(page.locator("#repoSelectedValue")).toHaveText(server.repoRoot);
524+
await expect(page.locator("#executeBtn")).toBeDisabled();
525+
await expect(page.locator("#repoSelectedValue")).toHaveText("HTML-JavaScript-Gaming");
527526
await expect(page.locator("#workspaceContextValue")).toHaveCount(0);
528527
await expect(page.locator("#repoDestinationContent")).not.toContainText("Workspace launch");
529528
await expect(page.locator("#targetTypeGames")).toBeChecked();
@@ -535,11 +534,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
535534
await expect(page.locator("#previewTargetValue")).toHaveText("games/Asteroids/assets/images/preview.svg");
536535
await expect(page.locator("#lastGeneratedImagePreview")).toBeVisible();
537536
await expect(page.locator("#lastGeneratedImageMeta")).toHaveText("Preview target: games/Asteroids/assets/images/preview.png");
538-
const absoluteAsteroidsPreviewPath = path.join(server.repoRoot, "games", "Asteroids", "assets", "images", "preview.svg");
539537
await expect(page.locator("#log")).toContainText("OK Workspace launch context hydrated for Asteroids.");
540-
await expect(page.locator("#log")).toContainText(`Repo selected from Workspace Manager V2 manifest repoRoot: ${server.repoRoot}.`);
541-
await expect(page.locator("#log")).toContainText(`Resolved repoRoot: ${server.repoRoot}`);
542-
await expect(page.locator("#log")).toContainText(`Resolved absolute preview output path: ${absoluteAsteroidsPreviewPath}`);
538+
await expect(page.locator("#log")).toContainText("Workspace repoRoot display label available: HTML-JavaScript-Gaming.");
539+
await expect(page.locator("#log")).toContainText("WARN Absolute repoRoot missing for workspace launch; manifest repoRoot is display-only: HTML-JavaScript-Gaming.");
540+
await expect(page.locator("#log")).toContainText("Direct preview write unavailable until a real writable repo root is selected.");
541+
await expect(page.locator("#log")).not.toContainText("Unable to resolve absolute repoRoot");
542+
await expect(page.locator("#log")).not.toContainText("/__workspace-manager-v2/repo-root");
543543
await expect(page.locator("#log")).toContainText("Asset folder: assets\\images");
544544
await expect(page.locator("#log")).toContainText("Manifest preview asset: assets.image.preview.preview (image/png)");
545545
await expect(page.locator("#log")).toContainText("Manifest preview source: games/Asteroids/assets/images/preview.png");
@@ -552,21 +552,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
552552
const previewStatusHeaderOrder = await page.locator(".preview-generator-v2__status-accordion-header").evaluate((header) => Array.from(header.querySelectorAll(":scope > span, :scope > div > span, :scope > div > button"), (element) => element.textContent.trim()));
553553
expect(previewStatusHeaderOrder).toEqual(["Status", "+", "Clear"]);
554554
await page.locator("#baseUrl").fill(server.baseUrl);
555-
await expect(page.locator("#executeBtn")).toBeEnabled();
556-
let previewDownloadOpened = false;
557-
page.on("download", () => {
558-
previewDownloadOpened = true;
559-
});
560-
await page.locator("#executeBtn").click();
561-
await expect(page.locator("#log")).toContainText("Workspace launch direct preview write target: games/Asteroids/assets/images/preview.svg.", { timeout: 20000 });
562-
await expect(page.locator("#log")).toContainText(`Workspace launch absolute preview output path: ${absoluteAsteroidsPreviewPath}.`, { timeout: 20000 });
563-
await expect(page.locator("#log")).toContainText("Direct preview write target: games/Asteroids/assets/images/preview.svg", { timeout: 20000 });
564-
await expect(page.locator("#log")).toContainText(`Direct preview absolute path: ${absoluteAsteroidsPreviewPath}`, { timeout: 20000 });
565-
await expect(page.locator("#log")).toContainText(`OK Direct preview write completed: ${absoluteAsteroidsPreviewPath}`, { timeout: 20000 });
566-
await expect(page.locator("#log")).toContainText("OK Asteroids", { timeout: 20000 });
567-
expect(previewDownloadOpened).toBe(false);
568-
expect(server.previewWrites.get("games/Asteroids/assets/images/preview.svg")).toContain("<svg");
569-
expect(server.previewAbsoluteWrites.get(absoluteAsteroidsPreviewPath)).toContain("<svg");
555+
await expect(page.locator("#executeBtn")).toBeDisabled();
556+
expect(server.previewWrites.size).toBe(0);
557+
expect(server.previewAbsoluteWrites.size).toBe(0);
570558
await page.locator("#returnToWorkspaceButton").click();
571559
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
572560
expect(pageErrors).toEqual([]);
@@ -613,7 +601,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
613601
}
614602
});
615603

616-
test("blocks Preview Generator V2 workspace direct write when repoRoot is not absolute", async ({ page }) => {
604+
test("opens Preview Generator V2 workspace launch with display-only repoRoot and actionable status", async ({ page }) => {
617605
const server = await startRepoServer();
618606
const pageErrors = [];
619607
const hostContextId = "workspace-manager-v2-display-root-context";
@@ -633,8 +621,13 @@ test.describe("Workspace Manager V2 bootstrap", () => {
633621
try {
634622
await expect(page.locator("#repoSelectedValue")).toHaveText("HTML-JavaScript-Gaming");
635623
await expect(page.locator("#executeBtn")).toBeDisabled();
636-
await expect(page.locator("#log")).toContainText("FAIL Workspace launch context hydration: repoRoot must be an absolute filesystem path, received HTML-JavaScript-Gaming.");
637-
await expect(page.locator("#log")).not.toContainText("OK Workspace launch context hydrated");
624+
await expect(page.locator("#previewTargetValue")).toHaveText("games/Asteroids/assets/images/preview.svg");
625+
await expect(page.locator("#log")).toContainText("OK Workspace launch context hydrated for Asteroids.");
626+
await expect(page.locator("#log")).toContainText("Workspace repoRoot display label available: HTML-JavaScript-Gaming.");
627+
await expect(page.locator("#log")).toContainText("WARN Absolute repoRoot missing for workspace launch; manifest repoRoot is display-only: HTML-JavaScript-Gaming.");
628+
await expect(page.locator("#log")).toContainText("Direct preview write unavailable until a real writable repo root is selected.");
629+
await expect(page.locator("#log")).not.toContainText("/__workspace-manager-v2/repo-root");
630+
await expect(page.locator("#log")).not.toContainText("FAIL Workspace launch context hydration");
638631
expect(server.previewWrites.size).toBe(0);
639632
expect(pageErrors).toEqual([]);
640633
} finally {

tools/preview-generator-v2/PreviewGeneratorV2App.js

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ let isGenerating = false;
2020
let workspacePreviewAssetFolder = "";
2121
let workspacePreviewFileValid = false;
2222
let workspacePreviewGameId = "";
23+
let workspaceLaunchHydrated = false;
2324
let workspaceRepoRootHydrated = false;
2425
let workspaceRepoRootName = "";
2526
let workspaceResolvedRepoRoot = "";
@@ -394,7 +395,7 @@ function updateWriteFolderSampleLabel() {
394395
}
395396

396397
function getWorkspacePreviewTargetDisplayPath() {
397-
if (isWorkspaceManagerLaunch() && (repoDirHandle || workspaceRepoRootHydrated) && workspaceGeneratedPreviewPath) {
398+
if (isWorkspaceManagerLaunch() && workspaceLaunchHydrated && workspaceGeneratedPreviewPath) {
398399
return workspaceGeneratedPreviewPath;
399400
}
400401
return workspaceManifestPreviewPath;
@@ -667,7 +668,7 @@ function isPreviewWriteError(error) {
667668

668669
function validateWorkspacePreviewWritePath(entry) {
669670
if (!isWorkspaceManagerLaunch() || !workspaceRepoRootHydrated || !workspacePreviewFileValid) {
670-
throw previewWriteError("Workspace preview write path is unavailable because launch hydration is incomplete.");
671+
throw previewWriteError("Workspace direct preview write is unavailable until an absolute repo root is selected.");
671672
}
672673
const targetPath = normalizeWorkspacePath(getWorkspacePreviewTargetDisplayPath());
673674
if (!targetPath) {
@@ -739,7 +740,7 @@ async function processOne(entry, baseUrl, waitMs) {
739740
: null;
740741
const decision = targetDirHandle
741742
? await shouldRewrite(targetDirHandle)
742-
: { rewrite: true, reason: "workspace-launch-hydrated-repo-root" };
743+
: { rewrite: true, reason: "workspace-launch-absolute-repo-root" };
743744

744745
ui.outputSummary.setWriteFolderActual(getWriteFolderDisplayPath(entry));
745746
const label = entry.targetType === "samples" ? entry.id : entry.name;
@@ -1028,6 +1029,7 @@ class PreviewGeneratorV2App {
10281029
}
10291030
logger.log("Workspace launch context hydration started.");
10301031
workspacePreviewFileValid = false;
1032+
workspaceLaunchHydrated = false;
10311033
workspaceRepoRootHydrated = false;
10321034
workspaceResolvedRepoRoot = "";
10331035
workspaceManifestPreviewPath = "";
@@ -1048,45 +1050,39 @@ class PreviewGeneratorV2App {
10481050
return;
10491051
}
10501052

1051-
const resolvedRepoRoot = String(manifest.repoRoot || "").trim();
1052-
if (!isAbsoluteFilesystemPath(resolvedRepoRoot)) {
1053-
repoDisplayName = resolvedRepoRoot;
1054-
workspaceRepoRootName = resolvedRepoRoot;
1055-
ui.setRepoDestinationDisplayName(resolvedRepoRoot || "not selected");
1056-
logger.log(`FAIL Workspace launch context hydration: repoRoot must be an absolute filesystem path, received ${resolvedRepoRoot || "(empty)"}.`);
1057-
this.syncGeneratePreviewButton();
1058-
return;
1059-
}
1060-
1061-
let resolvedPreviewOutputPath = "";
1062-
try {
1063-
resolvedPreviewOutputPath = resolveWorkspaceAbsolutePreviewOutputPath(resolvedRepoRoot, previewTarget.generatedPreviewPath);
1064-
} catch (error) {
1065-
logger.log(`FAIL Workspace launch context hydration: ${error.message}`);
1066-
this.syncGeneratePreviewButton();
1067-
return;
1068-
}
1069-
1070-
repoDisplayName = resolvedRepoRoot;
1053+
const manifestRepoRoot = String(manifest.repoRoot || "").trim();
1054+
repoDisplayName = manifestRepoRoot;
10711055
repoDirHandle = null;
1072-
workspaceRepoRootHydrated = true;
1073-
workspaceRepoRootName = resolvedRepoRoot;
1074-
workspaceResolvedRepoRoot = resolvedRepoRoot;
1056+
workspaceLaunchHydrated = true;
1057+
workspaceRepoRootName = manifestRepoRoot;
10751058
workspacePreviewAssetFolder = previewTarget.previewAssetFolder;
10761059
workspacePreviewGameId = manifest.gameId;
10771060
workspaceManifestPreviewPath = previewTarget.manifestPreviewPath;
10781061
workspaceGeneratedPreviewPath = previewTarget.generatedPreviewPath;
1079-
workspaceAbsolutePreviewOutputPath = resolvedPreviewOutputPath;
1062+
if (isAbsoluteFilesystemPath(manifestRepoRoot)) {
1063+
try {
1064+
workspaceAbsolutePreviewOutputPath = resolveWorkspaceAbsolutePreviewOutputPath(manifestRepoRoot, previewTarget.generatedPreviewPath);
1065+
workspaceRepoRootHydrated = true;
1066+
workspaceResolvedRepoRoot = manifestRepoRoot;
1067+
} catch (error) {
1068+
logger.log(`WARN Workspace direct preview write unavailable: ${error.message}`);
1069+
}
1070+
}
10801071
ui.setRepoDestinationDisplayName(repoDisplayName);
10811072
ui.targetSource.setSelectedTargetType("games");
10821073
ui.targetSource.showWorkspaceGamesOnly();
10831074
ui.assetFolder.setValue(workspacePreviewAssetFolder);
10841075
ui.pathsOrIds.setValue(manifest.gameId);
10851076
await updatePathPreviewLabels();
10861077
logger.log(`OK Workspace launch context hydrated for ${manifest.gameId}.`);
1087-
logger.log(`Repo selected from Workspace Manager V2 manifest repoRoot: ${repoDisplayName}.`);
1088-
logger.log(`Resolved repoRoot: ${workspaceResolvedRepoRoot}`);
1089-
logger.log(`Resolved absolute preview output path: ${workspaceAbsolutePreviewOutputPath}`);
1078+
logger.log(`Workspace repoRoot display label available: ${repoDisplayName}.`);
1079+
if (workspaceRepoRootHydrated) {
1080+
logger.log(`Resolved repoRoot: ${workspaceResolvedRepoRoot}`);
1081+
logger.log(`Resolved absolute preview output path: ${workspaceAbsolutePreviewOutputPath}`);
1082+
} else {
1083+
logger.log(`WARN Absolute repoRoot missing for workspace launch; manifest repoRoot is display-only: ${repoDisplayName || "(empty)"}.`);
1084+
logger.log("Direct preview write unavailable until a real writable repo root is selected.");
1085+
}
10901086
logger.log("Target source: games");
10911087
logger.log(`Asset folder: ${getAssetFolderDisplayPath()}`);
10921088
logger.log(`Manifest preview asset: ${previewTarget.previewAssetId} (${previewTarget.previewAssetType}/${previewTarget.previewAssetKind})`);

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,9 @@ export class WorkspaceManagerV2App {
150150
this.statusLog.fail(`Launch blocked: ${validation.message}`);
151151
return;
152152
}
153-
const launchContextResult = await this.contextService.buildLaunchContextForTool(this.activeContext, toolId);
154-
if (!launchContextResult.ok) {
155-
this.statusLog.fail(`Launch blocked: ${launchContextResult.message}`);
156-
return;
157-
}
158-
if (launchContextResult.repoRoot) {
159-
this.statusLog.ok(`Resolved Workspace Manager V2 launch repoRoot: ${launchContextResult.repoRoot}`);
160-
}
161153
const hostContextId = this.activeHostContextId
162-
? this.contextService.writePersistedContext(this.activeHostContextId, launchContextResult.context)
163-
: this.contextService.persistContext(launchContextResult.context);
154+
? this.contextService.writePersistedContext(this.activeHostContextId, this.activeContext)
155+
: this.contextService.persistContext(this.activeContext);
164156
this.activeHostContextId = hostContextId;
165157
this.statusLog.ok(`Stored Workspace Manager V2 schema-valid manifest ${hostContextId} for ${toolId}.`);
166158
this.contextService.launchTool(toolId, hostContextId, { workspaceMode: this.activeWorkspaceMode });

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

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
const HOST_CONTEXT_STORAGE_KEY = "workspace-manager-v2-active-host-context-id";
22
const WORKSPACE_MANIFEST_SCHEMA_PATH = "/tools/schemas/workspace.manifest.schema.json";
3-
const WORKSPACE_REPO_ROOT_ENDPOINT = "/__workspace-manager-v2/repo-root";
43
const ASSET_MANAGER_V2_TOOL_KEY = "asset-manager-v2";
54
const PALETTE_MANAGER_V2_TOOL_KEY = "palette-manager-v2";
6-
const PREVIEW_GENERATOR_V2_TOOL_KEY = "preview-generator-v2";
75
const TEMPORARY_UAT_MANIFEST_PATH = "/games/_template/workspace-manager-v2-UAT.manifest.json";
86
const WORKSPACE_LAUNCHABLE_TOOLS = Object.freeze([
97
Object.freeze({
@@ -66,13 +64,6 @@ function makeHostContextId() {
6664
return `workspace-manager-v2-${Date.now().toString(36)}`;
6765
}
6866

69-
function isAbsoluteFilesystemPath(value) {
70-
const pathValue = String(value || "").trim();
71-
return /^[A-Za-z]:[\\/]/.test(pathValue)
72-
|| pathValue.startsWith("/")
73-
|| pathValue.startsWith("\\\\");
74-
}
75-
7667
function temporaryUatGameFromManifest(workspaceManifest) {
7768
if (workspaceManifest?.gameId !== "_template"
7869
|| workspaceManifest?.gameRoot !== "games/_template/"
@@ -361,47 +352,6 @@ export class WorkspaceManagerV2ContextService {
361352
};
362353
}
363354

364-
async resolveAbsoluteRepoRoot() {
365-
if (typeof this.fetchRef !== "function") {
366-
return { ok: false, message: "Fetch API is unavailable; Workspace Manager V2 cannot resolve the absolute repoRoot." };
367-
}
368-
try {
369-
const response = await this.fetchRef(WORKSPACE_REPO_ROOT_ENDPOINT, { cache: "no-store" });
370-
if (!response.ok) {
371-
return { ok: false, message: `Unable to resolve absolute repoRoot from ${WORKSPACE_REPO_ROOT_ENDPOINT}: ${response.status}` };
372-
}
373-
const payload = await response.json();
374-
const repoRoot = String(payload?.repoRoot || "").trim();
375-
if (!repoRoot) {
376-
return { ok: false, message: `${WORKSPACE_REPO_ROOT_ENDPOINT} did not return repoRoot.` };
377-
}
378-
if (!isAbsoluteFilesystemPath(repoRoot)) {
379-
return { ok: false, message: `Resolved repoRoot must be an absolute filesystem path; received ${repoRoot}.` };
380-
}
381-
return { ok: true, repoRoot };
382-
} catch (error) {
383-
return { ok: false, message: `Unable to resolve absolute repoRoot from ${WORKSPACE_REPO_ROOT_ENDPOINT}: ${error.message}` };
384-
}
385-
}
386-
387-
async buildLaunchContextForTool(workspaceManifest, toolId) {
388-
const launchContext = clone(workspaceManifest);
389-
let resolvedRepoRoot = "";
390-
if (toolId === PREVIEW_GENERATOR_V2_TOOL_KEY) {
391-
const repoRootResult = await this.resolveAbsoluteRepoRoot();
392-
if (!repoRootResult.ok) {
393-
return repoRootResult;
394-
}
395-
launchContext.repoRoot = repoRootResult.repoRoot;
396-
resolvedRepoRoot = repoRootResult.repoRoot;
397-
}
398-
const validation = await this.validateGeneratedManifest(launchContext);
399-
if (!validation.ok) {
400-
return validation;
401-
}
402-
return { ok: true, context: launchContext, repoRoot: resolvedRepoRoot };
403-
}
404-
405355
async loadWorkspaceManifestSchema() {
406356
if (typeof this.fetchRef !== "function") {
407357
return { ok: false, message: "Fetch API is unavailable; Workspace Manager V2 cannot validate workspace manifests." };

0 commit comments

Comments
 (0)