From 07dab22987c014ed50b6e3150f3a018808aaebba Mon Sep 17 00:00:00 2001 From: DavidQ Date: Mon, 22 Jun 2026 14:14:20 -0400 Subject: [PATCH] PR_26174_ALFA_001-idea-board-create-project-api-contract --- ...project-api-contract-branch-validation.txt | 7 + ...t-api-contract-manual-validation-notes.txt | 7 + ...ect-api-contract-requirement-checklist.txt | 12 + ...e-project-api-contract-validation-lane.txt | 11 + ...-idea-board-create-project-api-contract.md | 16 ++ .../dev/reports/codex_changed_files.txt | 13 +- docs_build/dev/reports/codex_review.diff | 221 +++++++++++++++++- .../tools/IdeaBoardTableNotes.spec.mjs | 101 +++++++- toolbox/idea-board/index.js | 34 +++ 9 files changed, 407 insertions(+), 15 deletions(-) create mode 100644 docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md diff --git a/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt new file mode 100644 index 000000000..5e538ce07 --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt @@ -0,0 +1,7 @@ +Branch Validation: PASS + +- Started from clean synchronized `main` after PR #92 merged. +- PR #92 merge commit confirmed on main: b97893c78dcfed05d5b0de0c7d03127ec5575292. +- Working branch: `pr/26174-ALFA-001-idea-board-create-project-api-contract`. +- Branch is scoped to Idea Board Create Project API contract and required reports. +- Protected Project Instructions were not modified. diff --git a/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt new file mode 100644 index 000000000..298897acf --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt @@ -0,0 +1,7 @@ +Manual Validation Notes + +PASS - Confirmed Create Project remains hidden for existing non-Ready rows. +PASS - Confirmed Ready conversion sends a Game Hub repository createGame request through Local API. +PASS - Confirmed createGame request body excludes id, key, projectKey, or other browser-owned authoritative project key fields. +PASS - Confirmed converted idea has Project status, Open in Game Hub / Archive actions only, and no edit/delete/note edit controls. +PASS - Confirmed guest conversion redirects to account/sign-in.html and sends no createGame request. diff --git a/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt new file mode 100644 index 000000000..12e57ac35 --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt @@ -0,0 +1,12 @@ +Requirement Checklist: PASS + +PASS - Only Ready ideas show Create Project. +PASS - Non-Ready ideas do not show Create Project. +PASS - Create Project calls the Local API/service repository contract. +PASS - Server/API owns authoritative project key generation. +PASS - Browser does not generate or send authoritative project keys. +PASS - Converted idea status becomes Project. +PASS - Converted idea becomes locked/read-only. +PASS - Guest Create Project redirects to account/sign-in.html. +PASS - No browser-owned project arrays were added. +PASS - No silent fallback was added; API/session verification failures stop visibly. diff --git a/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt new file mode 100644 index 000000000..2c2906a2b --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt @@ -0,0 +1,11 @@ +Validation Lane Report: PASS + +Commands run: +- `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` + +Result: +- PASS: 3 tests passed. + +Notes: +- Targeted Playwright lane was used because the impacted surface is Idea Board with Game Hub handoff. +- Full samples smoke was not run by default per instruction. diff --git a/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md new file mode 100644 index 000000000..7ebb21f27 --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md @@ -0,0 +1,16 @@ +# PR_26174_ALFA_001-idea-board-create-project-api-contract + +## Purpose + +Wire Idea Board Create Project through the Local API/service contract. + +## Summary + +- Added an explicit Local API session check before Idea Board calls the Game Hub `createGame` repository method. +- Redirects unauthenticated guests to `account/sign-in.html` before any project create request is sent. +- Preserves the server/API create contract: browser sends name, purpose, sourceIdea, and status only; no browser-owned project key is sent. +- Extended targeted Playwright coverage for Ready-only create controls, non-Ready rows, converted read-only behavior, guest redirect, and API payload shape. + +## Validation + +PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 9c2ec60c8..2b9c4d3e9 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,8 +1,9 @@ -.gitignore +toolbox/idea-board/index.js +tests/playwright/tools/IdeaBoardTableNotes.spec.mjs docs_build/dev/reports/codex_review.diff docs_build/dev/reports/codex_changed_files.txt -docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore.md -docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-branch-validation.txt -docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-requirement-checklist.txt -docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-validation-lane.txt -docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-manual-validation-notes.txt +docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md +docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt +docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt +docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt +docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 74ceae1af..860d82d27 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,7 +1,216 @@ -PR_26174_ALFA_000-projectinstructions-archive-ignore review diff +diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +index 934faf46f..b2872b432 100644 +--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs ++++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +@@ -1,6 +1,7 @@ + import { expect, test } from "@playwright/test"; + import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; + import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs"; ++import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs"; + import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; -.gitignore -@@ - # Game Foundry Studio project files - assets/*.gfsp -+docs_build/dev/ProjectInstructions.zip + function restoreEnvValue(key, value) { +@@ -112,7 +113,10 @@ async function expectNoNavigationFallbackUi(page) { + } + + test("Idea Board uses accordion table ideas and notes", async ({ page }) => { +- const server = await startRepoServer(); ++ const server = await startRepoServer({ ++ gameJourneyCompletionMetricsLegacyDbPath: null, ++ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(), ++ }); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + const previousSupabaseEnv = { +@@ -131,6 +135,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + const pageErrors = []; + const consoleErrors = []; + const mutatingApiRequests = []; ++ const createGamePayloads = []; + + page.on("response", (response) => { + if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`); +@@ -144,8 +149,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text()); + }); + page.on("request", (request) => { +- if (request.url().includes("/api/") && request.method() !== "GET") { +- mutatingApiRequests.push(`${request.method()} ${request.url()}`); ++ const requestUrl = request.url(); ++ if (requestUrl.includes("/api/") && request.method() !== "GET") { ++ mutatingApiRequests.push(`${request.method()} ${requestUrl}`); ++ } ++ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { ++ createGamePayloads.push(request.postDataJSON()); + } + }); + +@@ -228,6 +237,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20"); + await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes"); + await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]); ++ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); + + await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard"); + await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg"); +@@ -336,6 +346,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); + await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0); + await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0); ++ expect(createGamePayloads).toHaveLength(1); ++ const [createGameInput] = createGamePayloads[0].args; ++ expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]); ++ expect(createGameInput).toMatchObject({ ++ name: "Lantern Reef", ++ purpose: "Game", ++ sourceIdea: { ++ idea: "Lantern Reef", ++ pitch: "Guide light through a reef that rearranges at dusk.", ++ notes: ["Use dusk tide changes as the first Game Hub planning note."], ++ }, ++ status: "Planning", ++ }); + await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click(); + await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0); + await page.locator("[data-idea-board-status-filter-option][value='Archived']").check(); +@@ -374,6 +397,78 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + } + }); + ++test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => { ++ const server = await startRepoServer(); ++ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; ++ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; ++ const previousMetricsDbPath = process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH; ++ const previousSupabaseEnv = { ++ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL, ++ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY, ++ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY, ++ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL, ++ }; ++ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; ++ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; ++ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board"; ++ process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH = `tmp/test-results/idea-board-${process.pid}-${Date.now()}.sqlite`; ++ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key"; ++ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key"; ++ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`; ++ const createGameRequests = []; ++ ++ page.on("request", (request) => { ++ const requestUrl = request.url(); ++ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { ++ createGameRequests.push(requestUrl); ++ } ++ }); ++ ++ try { ++ await page.route("**/api/platform-settings/banner", async (route) => { ++ await route.fulfill({ ++ contentType: "application/json", ++ body: JSON.stringify({ ++ data: { banner: { active: false, message: "", tone: "info" } }, ++ ok: true, ++ }), ++ }); ++ }); ++ await page.route("**/api/toolbox/registry/snapshot", async (route) => { ++ await route.fulfill({ ++ contentType: "application/json", ++ body: JSON.stringify({ ++ data: { ++ activeTools: [], ++ readinessByStatus: {}, ++ tools: [], ++ toolboxContract: {}, ++ }, ++ ok: true, ++ }), ++ }); ++ }); ++ ++ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); ++ await page.locator("[data-idea-board-add-idea]").click(); ++ await page.locator("[data-idea-board-idea-input]").fill("Guest Reef"); ++ await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys."); ++ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); ++ await page.locator("[data-idea-board-idea-action='save']").click(); ++ await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); ++ ++ await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click(); ++ await page.waitForURL(/\/account\/sign-in\.html$/); ++ expect(createGameRequests).toEqual([]); ++ } finally { ++ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); ++ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); ++ restoreEnvValue("GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH", previousMetricsDbPath); ++ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); ++ await server.close(); ++ } ++}); ++ + test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => { + const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; +diff --git a/toolbox/idea-board/index.js b/toolbox/idea-board/index.js +index d1e339336..0ae02bd7a 100644 +--- a/toolbox/idea-board/index.js ++++ b/toolbox/idea-board/index.js +@@ -1,9 +1,11 @@ + import { createServerRepositoryClient } from "../../src/api/server-api-client.js"; ++import { getSessionCurrent } from "../../src/api/session-api-client.js"; + + const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); + const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]); + const userId = "user-1"; + const gameHubRoute = "toolbox/game-hub/index.html"; ++const signInRoute = "account/sign-in.html"; + let gameHubRepository = null; + + const ideaTable = [ +@@ -561,6 +563,28 @@ function gameHubUrl(record) { + return `${gameHubRoute}${suffix}`; + } + ++function signInUrl() { ++ return new URL(signInRoute, document.baseURI || window.location.href).href; ++} ++ ++function currentSessionState() { ++ try { ++ const session = getSessionCurrent(); ++ return { ++ apiAvailable: true, ++ authenticated: Boolean(session?.authenticated && session.userKey), ++ session, ++ }; ++ } catch (error) { ++ console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || "")); ++ return { ++ apiAvailable: false, ++ authenticated: false, ++ session: null, ++ }; ++ } ++} ++ + function createProject(root, ideaId) { + const record = ideaRecord(ideaId); + if (!record) { +@@ -571,6 +595,16 @@ function createProject(root, ideaId) { + updateStatus(root, "Set this idea to Ready before creating a project."); + return; + } ++ const sessionState = currentSessionState(); ++ if (!sessionState.apiAvailable) { ++ updateStatus(root, "Sign-in status could not be verified. Try again shortly."); ++ return; ++ } ++ if (!sessionState.authenticated) { ++ updateStatus(root, "Sign in to create a Game Hub project."); ++ window.location.href = signInUrl(); ++ return; ++ } + const repository = gameHubProjectRepository(); + const project = repository.createGame({ + name: record.idea, diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs index 934faf46f..b2872b432 100644 --- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs"; +import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs"; import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; function restoreEnvValue(key, value) { @@ -112,7 +113,10 @@ async function expectNoNavigationFallbackUi(page) { } test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - const server = await startRepoServer(); + const server = await startRepoServer({ + gameJourneyCompletionMetricsLegacyDbPath: null, + gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(), + }); const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; const previousSupabaseEnv = { @@ -131,6 +135,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { const pageErrors = []; const consoleErrors = []; const mutatingApiRequests = []; + const createGamePayloads = []; page.on("response", (response) => { if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`); @@ -144,8 +149,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text()); }); page.on("request", (request) => { - if (request.url().includes("/api/") && request.method() !== "GET") { - mutatingApiRequests.push(`${request.method()} ${request.url()}`); + const requestUrl = request.url(); + if (requestUrl.includes("/api/") && request.method() !== "GET") { + mutatingApiRequests.push(`${request.method()} ${requestUrl}`); + } + if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { + createGamePayloads.push(request.postDataJSON()); } }); @@ -228,6 +237,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20"); await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes"); await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]); + await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard"); await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg"); @@ -336,6 +346,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0); await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0); + expect(createGamePayloads).toHaveLength(1); + const [createGameInput] = createGamePayloads[0].args; + expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]); + expect(createGameInput).toMatchObject({ + name: "Lantern Reef", + purpose: "Game", + sourceIdea: { + idea: "Lantern Reef", + pitch: "Guide light through a reef that rearranges at dusk.", + notes: ["Use dusk tide changes as the first Game Hub planning note."], + }, + status: "Planning", + }); await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click(); await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0); await page.locator("[data-idea-board-status-filter-option][value='Archived']").check(); @@ -374,6 +397,78 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { } }); +test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => { + const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + const previousMetricsDbPath = process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH; + const previousSupabaseEnv = { + GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL, + GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY, + GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY, + GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL, + }; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; + process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board"; + process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH = `tmp/test-results/idea-board-${process.pid}-${Date.now()}.sqlite`; + process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key"; + process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key"; + process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`; + const createGameRequests = []; + + page.on("request", (request) => { + const requestUrl = request.url(); + if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { + createGameRequests.push(requestUrl); + } + }); + + try { + await page.route("**/api/platform-settings/banner", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + data: { banner: { active: false, message: "", tone: "info" } }, + ok: true, + }), + }); + }); + await page.route("**/api/toolbox/registry/snapshot", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + data: { + activeTools: [], + readinessByStatus: {}, + tools: [], + toolboxContract: {}, + }, + ok: true, + }), + }); + }); + + await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); + await page.locator("[data-idea-board-add-idea]").click(); + await page.locator("[data-idea-board-idea-input]").fill("Guest Reef"); + await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys."); + await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); + await page.locator("[data-idea-board-idea-action='save']").click(); + await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); + + await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click(); + await page.waitForURL(/\/account\/sign-in\.html$/); + expect(createGameRequests).toEqual([]); + } finally { + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); + restoreEnvValue("GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH", previousMetricsDbPath); + Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); + await server.close(); + } +}); + test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => { const server = await startRepoServer(); const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; diff --git a/toolbox/idea-board/index.js b/toolbox/idea-board/index.js index d1e339336..0ae02bd7a 100644 --- a/toolbox/idea-board/index.js +++ b/toolbox/idea-board/index.js @@ -1,9 +1,11 @@ import { createServerRepositoryClient } from "../../src/api/server-api-client.js"; +import { getSessionCurrent } from "../../src/api/session-api-client.js"; const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]); const userId = "user-1"; const gameHubRoute = "toolbox/game-hub/index.html"; +const signInRoute = "account/sign-in.html"; let gameHubRepository = null; const ideaTable = [ @@ -561,6 +563,28 @@ function gameHubUrl(record) { return `${gameHubRoute}${suffix}`; } +function signInUrl() { + return new URL(signInRoute, document.baseURI || window.location.href).href; +} + +function currentSessionState() { + try { + const session = getSessionCurrent(); + return { + apiAvailable: true, + authenticated: Boolean(session?.authenticated && session.userKey), + session, + }; + } catch (error) { + console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || "")); + return { + apiAvailable: false, + authenticated: false, + session: null, + }; + } +} + function createProject(root, ideaId) { const record = ideaRecord(ideaId); if (!record) { @@ -571,6 +595,16 @@ function createProject(root, ideaId) { updateStatus(root, "Set this idea to Ready before creating a project."); return; } + const sessionState = currentSessionState(); + if (!sessionState.apiAvailable) { + updateStatus(root, "Sign-in status could not be verified. Try again shortly."); + return; + } + if (!sessionState.authenticated) { + updateStatus(root, "Sign in to create a Game Hub project."); + window.location.href = signInUrl(); + return; + } const repository = gameHubProjectRepository(); const project = repository.createGame({ name: record.idea,