Skip to content

Commit 07dab22

Browse files
committed
PR_26174_ALFA_001-idea-board-create-project-api-contract
1 parent b97893c commit 07dab22

9 files changed

Lines changed: 407 additions & 15 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Branch Validation: PASS
2+
3+
- Started from clean synchronized `main` after PR #92 merged.
4+
- PR #92 merge commit confirmed on main: b97893c78dcfed05d5b0de0c7d03127ec5575292.
5+
- Working branch: `pr/26174-ALFA-001-idea-board-create-project-api-contract`.
6+
- Branch is scoped to Idea Board Create Project API contract and required reports.
7+
- Protected Project Instructions were not modified.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Manual Validation Notes
2+
3+
PASS - Confirmed Create Project remains hidden for existing non-Ready rows.
4+
PASS - Confirmed Ready conversion sends a Game Hub repository createGame request through Local API.
5+
PASS - Confirmed createGame request body excludes id, key, projectKey, or other browser-owned authoritative project key fields.
6+
PASS - Confirmed converted idea has Project status, Open in Game Hub / Archive actions only, and no edit/delete/note edit controls.
7+
PASS - Confirmed guest conversion redirects to account/sign-in.html and sends no createGame request.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Only Ready ideas show Create Project.
4+
PASS - Non-Ready ideas do not show Create Project.
5+
PASS - Create Project calls the Local API/service repository contract.
6+
PASS - Server/API owns authoritative project key generation.
7+
PASS - Browser does not generate or send authoritative project keys.
8+
PASS - Converted idea status becomes Project.
9+
PASS - Converted idea becomes locked/read-only.
10+
PASS - Guest Create Project redirects to account/sign-in.html.
11+
PASS - No browser-owned project arrays were added.
12+
PASS - No silent fallback was added; API/session verification failures stop visibly.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Validation Lane Report: PASS
2+
3+
Commands run:
4+
- `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
5+
6+
Result:
7+
- PASS: 3 tests passed.
8+
9+
Notes:
10+
- Targeted Playwright lane was used because the impacted surface is Idea Board with Game Hub handoff.
11+
- Full samples smoke was not run by default per instruction.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26174_ALFA_001-idea-board-create-project-api-contract
2+
3+
## Purpose
4+
5+
Wire Idea Board Create Project through the Local API/service contract.
6+
7+
## Summary
8+
9+
- Added an explicit Local API session check before Idea Board calls the Game Hub `createGame` repository method.
10+
- Redirects unauthenticated guests to `account/sign-in.html` before any project create request is sent.
11+
- Preserves the server/API create contract: browser sends name, purpose, sourceIdea, and status only; no browser-owned project key is sent.
12+
- Extended targeted Playwright coverage for Ready-only create controls, non-Ready rows, converted read-only behavior, guest redirect, and API payload shape.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
.gitignore
1+
toolbox/idea-board/index.js
2+
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
23
docs_build/dev/reports/codex_review.diff
34
docs_build/dev/reports/codex_changed_files.txt
4-
docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore.md
5-
docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-branch-validation.txt
6-
docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-requirement-checklist.txt
7-
docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-validation-lane.txt
8-
docs_build/dev/reports/PR_26174_ALFA_000-projectinstructions-archive-ignore-manual-validation-notes.txt
5+
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md
6+
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt
7+
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt
8+
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt
9+
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt
Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,216 @@
1-
PR_26174_ALFA_000-projectinstructions-archive-ignore review diff
1+
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
2+
index 934faf46f..b2872b432 100644
3+
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
4+
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
5+
@@ -1,6 +1,7 @@
6+
import { expect, test } from "@playwright/test";
7+
import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js";
8+
import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs";
9+
+import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs";
10+
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
211

3-
.gitignore
4-
@@
5-
# Game Foundry Studio project files
6-
assets/*.gfsp
7-
+docs_build/dev/ProjectInstructions.zip
12+
function restoreEnvValue(key, value) {
13+
@@ -112,7 +113,10 @@ async function expectNoNavigationFallbackUi(page) {
14+
}
15+
16+
test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
17+
- const server = await startRepoServer();
18+
+ const server = await startRepoServer({
19+
+ gameJourneyCompletionMetricsLegacyDbPath: null,
20+
+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(),
21+
+ });
22+
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
23+
const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
24+
const previousSupabaseEnv = {
25+
@@ -131,6 +135,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
26+
const pageErrors = [];
27+
const consoleErrors = [];
28+
const mutatingApiRequests = [];
29+
+ const createGamePayloads = [];
30+
31+
page.on("response", (response) => {
32+
if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`);
33+
@@ -144,8 +149,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
34+
if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text());
35+
});
36+
page.on("request", (request) => {
37+
- if (request.url().includes("/api/") && request.method() !== "GET") {
38+
- mutatingApiRequests.push(`${request.method()} ${request.url()}`);
39+
+ const requestUrl = request.url();
40+
+ if (requestUrl.includes("/api/") && request.method() !== "GET") {
41+
+ mutatingApiRequests.push(`${request.method()} ${requestUrl}`);
42+
+ }
43+
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
44+
+ createGamePayloads.push(request.postDataJSON());
45+
}
46+
});
47+
48+
@@ -228,6 +237,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
49+
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20");
50+
await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes");
51+
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]);
52+
+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
53+
54+
await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard");
55+
await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg");
56+
@@ -336,6 +346,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
57+
await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
58+
await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0);
59+
await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0);
60+
+ expect(createGamePayloads).toHaveLength(1);
61+
+ const [createGameInput] = createGamePayloads[0].args;
62+
+ expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]);
63+
+ expect(createGameInput).toMatchObject({
64+
+ name: "Lantern Reef",
65+
+ purpose: "Game",
66+
+ sourceIdea: {
67+
+ idea: "Lantern Reef",
68+
+ pitch: "Guide light through a reef that rearranges at dusk.",
69+
+ notes: ["Use dusk tide changes as the first Game Hub planning note."],
70+
+ },
71+
+ status: "Planning",
72+
+ });
73+
await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click();
74+
await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0);
75+
await page.locator("[data-idea-board-status-filter-option][value='Archived']").check();
76+
@@ -374,6 +397,78 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
77+
}
78+
});
79+
80+
+test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => {
81+
+ const server = await startRepoServer();
82+
+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
83+
+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
84+
+ const previousMetricsDbPath = process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH;
85+
+ const previousSupabaseEnv = {
86+
+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL,
87+
+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY,
88+
+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY,
89+
+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL,
90+
+ };
91+
+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
92+
+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
93+
+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board";
94+
+ process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH = `tmp/test-results/idea-board-${process.pid}-${Date.now()}.sqlite`;
95+
+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key";
96+
+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key";
97+
+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`;
98+
+ const createGameRequests = [];
99+
+
100+
+ page.on("request", (request) => {
101+
+ const requestUrl = request.url();
102+
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
103+
+ createGameRequests.push(requestUrl);
104+
+ }
105+
+ });
106+
+
107+
+ try {
108+
+ await page.route("**/api/platform-settings/banner", async (route) => {
109+
+ await route.fulfill({
110+
+ contentType: "application/json",
111+
+ body: JSON.stringify({
112+
+ data: { banner: { active: false, message: "", tone: "info" } },
113+
+ ok: true,
114+
+ }),
115+
+ });
116+
+ });
117+
+ await page.route("**/api/toolbox/registry/snapshot", async (route) => {
118+
+ await route.fulfill({
119+
+ contentType: "application/json",
120+
+ body: JSON.stringify({
121+
+ data: {
122+
+ activeTools: [],
123+
+ readinessByStatus: {},
124+
+ tools: [],
125+
+ toolboxContract: {},
126+
+ },
127+
+ ok: true,
128+
+ }),
129+
+ });
130+
+ });
131+
+
132+
+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" });
133+
+ await page.locator("[data-idea-board-add-idea]").click();
134+
+ await page.locator("[data-idea-board-idea-input]").fill("Guest Reef");
135+
+ await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys.");
136+
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
137+
+ await page.locator("[data-idea-board-idea-action='save']").click();
138+
+ await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
139+
+
140+
+ await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click();
141+
+ await page.waitForURL(/\/account\/sign-in\.html$/);
142+
+ expect(createGameRequests).toEqual([]);
143+
+ } finally {
144+
+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
145+
+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
146+
+ restoreEnvValue("GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH", previousMetricsDbPath);
147+
+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value));
148+
+ await server.close();
149+
+ }
150+
+});
151+
+
152+
test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => {
153+
const server = await startRepoServer();
154+
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
155+
diff --git a/toolbox/idea-board/index.js b/toolbox/idea-board/index.js
156+
index d1e339336..0ae02bd7a 100644
157+
--- a/toolbox/idea-board/index.js
158+
+++ b/toolbox/idea-board/index.js
159+
@@ -1,9 +1,11 @@
160+
import { createServerRepositoryClient } from "../../src/api/server-api-client.js";
161+
+import { getSessionCurrent } from "../../src/api/session-api-client.js";
162+
163+
const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]);
164+
const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]);
165+
const userId = "user-1";
166+
const gameHubRoute = "toolbox/game-hub/index.html";
167+
+const signInRoute = "account/sign-in.html";
168+
let gameHubRepository = null;
169+
170+
const ideaTable = [
171+
@@ -561,6 +563,28 @@ function gameHubUrl(record) {
172+
return `${gameHubRoute}${suffix}`;
173+
}
174+
175+
+function signInUrl() {
176+
+ return new URL(signInRoute, document.baseURI || window.location.href).href;
177+
+}
178+
+
179+
+function currentSessionState() {
180+
+ try {
181+
+ const session = getSessionCurrent();
182+
+ return {
183+
+ apiAvailable: true,
184+
+ authenticated: Boolean(session?.authenticated && session.userKey),
185+
+ session,
186+
+ };
187+
+ } catch (error) {
188+
+ console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || ""));
189+
+ return {
190+
+ apiAvailable: false,
191+
+ authenticated: false,
192+
+ session: null,
193+
+ };
194+
+ }
195+
+}
196+
+
197+
function createProject(root, ideaId) {
198+
const record = ideaRecord(ideaId);
199+
if (!record) {
200+
@@ -571,6 +595,16 @@ function createProject(root, ideaId) {
201+
updateStatus(root, "Set this idea to Ready before creating a project.");
202+
return;
203+
}
204+
+ const sessionState = currentSessionState();
205+
+ if (!sessionState.apiAvailable) {
206+
+ updateStatus(root, "Sign-in status could not be verified. Try again shortly.");
207+
+ return;
208+
+ }
209+
+ if (!sessionState.authenticated) {
210+
+ updateStatus(root, "Sign in to create a Game Hub project.");
211+
+ window.location.href = signInUrl();
212+
+ return;
213+
+ }
214+
const repository = gameHubProjectRepository();
215+
const project = repository.createGame({
216+
name: record.idea,

0 commit comments

Comments
 (0)