diff --git a/src/commands/projects.ts b/src/commands/projects.ts index bef32b8..150cb42 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -259,6 +259,17 @@ function registerCreateCommand(projects: Command): void { `Created ${c.bold(filled.name)} ${c.dim(`(${result.id})`)} on ${c.bold(env.name)}` ) ); + if (filled.platforms.length === 0) { + console.log(c.dim(" No platforms enabled.")); + console.log( + c.dim(" See accepted platform names with `photon projects create --help`.") + ); + console.log( + c.dim( + ` Enable one with \`photon spectrum platforms enable --project '${result.id}'\`.` + ) + ); + } console.log( c.dim(` To make this the active project: export PHOTON_PROJECT_ID='${result.id}'`) ); diff --git a/tests/contract/projects.contract.test.ts b/tests/contract/projects.contract.test.ts index 6163f44..cd73990 100644 --- a/tests/contract/projects.contract.test.ts +++ b/tests/contract/projects.contract.test.ts @@ -1,5 +1,17 @@ -import { describe, test, expect, beforeAll, afterAll } from "bun:test"; -import { startMockServer, stopMockServer } from "../helpers/mock-server.ts"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; +import { + getMockProjectCreateRequests, + resetMockState, + startMockServer, + stopMockServer, +} from "../helpers/mock-server.ts"; import { runCommand } from "../helpers/cli-runner.ts"; let baseUrl: string; @@ -12,6 +24,96 @@ afterAll(async () => { await stopMockServer(); }); +beforeEach(() => { + resetMockState(); +}); + +describe("photon projects create", () => { + test("explains how to enable a platform when created without platforms", async () => { + const { stdout, exitCode } = await runCommand( + ["projects", "create", "--name", "No Platform"], + { + env: { + CI: "1", + PHOTON_TOKEN: "test-token", + PHOTON_API_HOST: baseUrl, + }, + }, + ); + + expect(exitCode).toBe(0); + expect(getMockProjectCreateRequests()).toEqual([ + { + name: "No Platform", + location: "United States", + platforms: [], + template: false, + observability: false, + }, + ]); + expect(stdout).toContain("No platforms enabled."); + expect(stdout).toContain("photon projects create --help"); + expect(stdout).toContain( + "photon spectrum platforms enable --project '00000000-0000-4000-a000-000000000001'", + ); + }); + + test("does not show the no-platform hint for an explicit platform", async () => { + const { stdout, exitCode } = await runCommand( + [ + "projects", + "create", + "--name", + "iMessage Project", + "--platforms", + "imessage", + ], + { + env: { + CI: "1", + PHOTON_TOKEN: "test-token", + PHOTON_API_HOST: baseUrl, + }, + }, + ); + + expect(exitCode).toBe(0); + expect(getMockProjectCreateRequests()).toEqual([ + { + name: "iMessage Project", + location: "United States", + platforms: ["imessage"], + template: false, + observability: false, + }, + ]); + expect(stdout).not.toContain("No platforms enabled."); + expect(stdout).not.toContain("photon projects create --help"); + expect(stdout).not.toContain("spectrum platforms enable"); + }); + + test("keeps --json output unchanged when no platform is selected", async () => { + const { stdout, exitCode } = await runCommand( + ["projects", "create", "--name", "JSON Project", "--json"], + { + env: { + CI: "1", + PHOTON_TOKEN: "test-token", + PHOTON_API_HOST: baseUrl, + }, + }, + ); + + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(Object.keys(parsed)).toEqual(["id", "name", "env"]); + expect(parsed.id).toBe("00000000-0000-4000-a000-000000000001"); + expect(parsed.name).toBe("JSON Project"); + expect(typeof parsed.env).toBe("string"); + expect(getMockProjectCreateRequests()[0]?.platforms).toEqual([]); + }); +}); + describe("photon projects list", () => { test("lists project names from fixtures", async () => { const { stdout, exitCode } = await runCommand(["projects", "list"], { diff --git a/tests/helpers/mock-server.ts b/tests/helpers/mock-server.ts index 0455822..6a4e76d 100644 --- a/tests/helpers/mock-server.ts +++ b/tests/helpers/mock-server.ts @@ -39,6 +39,7 @@ interface MockState { lineAvatarResponseFault: "missing-avatar-url" | "missing-upload-key" | null; lineProfileRequests: MockLineProfileRequest[]; profileSyncRequests: MockProfileSyncRequest[]; + projectCreateRequests: Record[]; } export interface MockLineProfileRequest { @@ -70,6 +71,7 @@ const state: MockState = { lineAvatarResponseFault: null, lineProfileRequests: [], profileSyncRequests: [], + projectCreateRequests: [], }; export function setMockSubscription(sub: "free" | "active"): void { @@ -102,6 +104,10 @@ export function getMockLineProfileRequests(): MockLineProfileRequest[] { return state.lineProfileRequests.map((request) => ({ ...request })); } +export function getMockProjectCreateRequests(): Record[] { + return state.projectCreateRequests.map((request) => ({ ...request })); +} + export function resetMockState(): void { state.subscription = subscriptionFree; state.forceUnauthorized = false; @@ -110,6 +116,7 @@ export function resetMockState(): void { state.lineAvatarResponseFault = null; state.lineProfileRequests = []; state.profileSyncRequests = []; + state.projectCreateRequests = []; } function requireAuth(headers: Record) { @@ -292,9 +299,10 @@ const app = new Elysia() if (found) return found; return projectFixture; }) - .post("/api/projects", ({ headers }) => { + .post("/api/projects", ({ body, headers }) => { const denied = requireAuth(headers as Record); if (denied) return denied; + state.projectCreateRequests.push({ ...(body as Record) }); return { success: true, id: projectFixture.id }; }) .get("/api/projects/:id/subscription", ({ headers }) => {