Skip to content

Commit 5e7c751

Browse files
committed
Merge PR_26174_ALFA_001-idea-board-create-project-api-contract
# Conflicts: # assets/toolbox/idea-board/js/index.js # docs_build/dev/reports/codex_changed_files.txt # docs_build/dev/reports/codex_review.diff
2 parents 50d2a1f + 07dab22 commit 5e7c751

7 files changed

Lines changed: 185 additions & 3 deletions

assets/toolbox/idea-board/js/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js";
2+
import { getSessionCurrent } from "../../../../src/api/session-api-client.js";
23

34
const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]);
45
const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]);
56
const userId = "user-1";
67
const gameHubRoute = "toolbox/game-hub/index.html";
8+
const signInRoute = "account/sign-in.html";
79
let gameHubRepository = null;
810

911
const ideaTable = [
@@ -561,6 +563,28 @@ function gameHubUrl(record) {
561563
return `${gameHubRoute}${suffix}`;
562564
}
563565

566+
function signInUrl() {
567+
return new URL(signInRoute, document.baseURI || window.location.href).href;
568+
}
569+
570+
function currentSessionState() {
571+
try {
572+
const session = getSessionCurrent();
573+
return {
574+
apiAvailable: true,
575+
authenticated: Boolean(session?.authenticated && session.userKey),
576+
session,
577+
};
578+
} catch (error) {
579+
console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || ""));
580+
return {
581+
apiAvailable: false,
582+
authenticated: false,
583+
session: null,
584+
};
585+
}
586+
}
587+
564588
function createProject(root, ideaId) {
565589
const record = ideaRecord(ideaId);
566590
if (!record) {
@@ -571,6 +595,16 @@ function createProject(root, ideaId) {
571595
updateStatus(root, "Set this idea to Ready before creating a project.");
572596
return;
573597
}
598+
const sessionState = currentSessionState();
599+
if (!sessionState.apiAvailable) {
600+
updateStatus(root, "Sign-in status could not be verified. Try again shortly.");
601+
return;
602+
}
603+
if (!sessionState.authenticated) {
604+
updateStatus(root, "Sign in to create a Game Hub project.");
605+
window.location.href = signInUrl();
606+
return;
607+
}
574608
const repository = gameHubProjectRepository();
575609
const project = repository.createGame({
576610
name: record.idea,
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`

tests/playwright/tools/IdeaBoardTableNotes.spec.mjs

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test } from "@playwright/test";
22
import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js";
33
import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs";
4+
import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs";
45
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
56

67
function restoreEnvValue(key, value) {
@@ -112,7 +113,10 @@ async function expectNoNavigationFallbackUi(page) {
112113
}
113114

114115
test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
115-
const server = await startRepoServer();
116+
const server = await startRepoServer({
117+
gameJourneyCompletionMetricsLegacyDbPath: null,
118+
gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(),
119+
});
116120
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
117121
const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
118122
const previousSupabaseEnv = {
@@ -131,6 +135,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
131135
const pageErrors = [];
132136
const consoleErrors = [];
133137
const mutatingApiRequests = [];
138+
const createGamePayloads = [];
134139

135140
page.on("response", (response) => {
136141
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 }) => {
144149
if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text());
145150
});
146151
page.on("request", (request) => {
147-
if (request.url().includes("/api/") && request.method() !== "GET") {
148-
mutatingApiRequests.push(`${request.method()} ${request.url()}`);
152+
const requestUrl = request.url();
153+
if (requestUrl.includes("/api/") && request.method() !== "GET") {
154+
mutatingApiRequests.push(`${request.method()} ${requestUrl}`);
155+
}
156+
if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
157+
createGamePayloads.push(request.postDataJSON());
149158
}
150159
});
151160

@@ -228,6 +237,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
228237
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20");
229238
await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes");
230239
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]);
240+
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
231241

232242
await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard");
233243
await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg");
@@ -336,6 +346,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
336346
await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
337347
await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0);
338348
await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0);
349+
expect(createGamePayloads).toHaveLength(1);
350+
const [createGameInput] = createGamePayloads[0].args;
351+
expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]);
352+
expect(createGameInput).toMatchObject({
353+
name: "Lantern Reef",
354+
purpose: "Game",
355+
sourceIdea: {
356+
idea: "Lantern Reef",
357+
pitch: "Guide light through a reef that rearranges at dusk.",
358+
notes: ["Use dusk tide changes as the first Game Hub planning note."],
359+
},
360+
status: "Planning",
361+
});
339362
await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click();
340363
await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0);
341364
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 }) => {
374397
}
375398
});
376399

400+
test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => {
401+
const server = await startRepoServer();
402+
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
403+
const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
404+
const previousMetricsDbPath = process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH;
405+
const previousSupabaseEnv = {
406+
GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL,
407+
GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY,
408+
GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY,
409+
GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL,
410+
};
411+
process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
412+
process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
413+
process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board";
414+
process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH = `tmp/test-results/idea-board-${process.pid}-${Date.now()}.sqlite`;
415+
process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key";
416+
process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key";
417+
process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`;
418+
const createGameRequests = [];
419+
420+
page.on("request", (request) => {
421+
const requestUrl = request.url();
422+
if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
423+
createGameRequests.push(requestUrl);
424+
}
425+
});
426+
427+
try {
428+
await page.route("**/api/platform-settings/banner", async (route) => {
429+
await route.fulfill({
430+
contentType: "application/json",
431+
body: JSON.stringify({
432+
data: { banner: { active: false, message: "", tone: "info" } },
433+
ok: true,
434+
}),
435+
});
436+
});
437+
await page.route("**/api/toolbox/registry/snapshot", async (route) => {
438+
await route.fulfill({
439+
contentType: "application/json",
440+
body: JSON.stringify({
441+
data: {
442+
activeTools: [],
443+
readinessByStatus: {},
444+
tools: [],
445+
toolboxContract: {},
446+
},
447+
ok: true,
448+
}),
449+
});
450+
});
451+
452+
await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" });
453+
await page.locator("[data-idea-board-add-idea]").click();
454+
await page.locator("[data-idea-board-idea-input]").fill("Guest Reef");
455+
await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys.");
456+
await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
457+
await page.locator("[data-idea-board-idea-action='save']").click();
458+
await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
459+
460+
await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click();
461+
await page.waitForURL(/\/account\/sign-in\.html$/);
462+
expect(createGameRequests).toEqual([]);
463+
} finally {
464+
restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
465+
restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
466+
restoreEnvValue("GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH", previousMetricsDbPath);
467+
Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value));
468+
await server.close();
469+
}
470+
});
471+
377472
test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => {
378473
const server = await startRepoServer();
379474
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;

0 commit comments

Comments
 (0)