From e47ce58db3dc0275bb87671e00ec369cdfd620cd Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Thu, 20 Aug 2026 18:27:42 -0400 Subject: [PATCH] [ai] fix stub endpoint --- apps/ai/src/ai.app.ts | 36 +++++++++++++----------- apps/ai/src/openapi.ts | 15 ++++++---- apps/ai/src/test/integration/api.test.ts | 20 +++++++------ 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/apps/ai/src/ai.app.ts b/apps/ai/src/ai.app.ts index a9232f2..b594efa 100644 --- a/apps/ai/src/ai.app.ts +++ b/apps/ai/src/ai.app.ts @@ -235,28 +235,32 @@ const app = new Hono() } ) - // Whether the caller may use Maker AI at all. Always false: no model runs behind this - // worker, so the honest answer is that the feature isn't available — and false is what - // leaves the creation UI in its normal state rather than offering a tool that can't - // work. (The balances below are still served: the client reads its usage meter - // separately, and a server that bills nothing has spent nothing.) + // Whether the caller may use Maker AI at all. Granted, like the Roomie budget reads and + // unlike the Game AI checks: this is a gate, not a model call, and refusing it hides the + // feature outright. (The balances below are still zeroed: the client reads its usage + // meter separately, and a server that bills nothing has spent nothing.) // - // The body is a BARE JSON `false` — not an envelope, unlike the Game AI refusal and the - // Roomie access check on either side of it. `econ`'s - // `/api/makerai/checkfreetrialeligibility` answers the same bare shape. + // The envelope is its own shape again — PascalCase `Success`/`Error` beside a snake_case + // `error_id`, which is neither the Game AI refusal's all-lowercase body nor the Roomie + // check's `{ success, error_id, error, value }`. Reproduced as the reference sends it; + // the mixed casing is not a typo to tidy up. .get( '/makerai/user/access', describeRoute({ tags: ['Maker AI'], summary: 'May the caller use Maker AI?', description: [ - 'Asked before the client offers Maker AI. Always `false` — no model runs behind this', - 'worker. The body is a bare JSON boolean, not the `{ success, error, value }` envelope', - 'the neighbouring checks answer with.', + 'Asked before the client offers Maker AI. Always granted — the gate is about', + 'entitlement, not capacity, and nothing here meters what Maker AI would cost.', '', - '`roomInstanceSpecificCheck` (the client sends .NET’s `False`) is accepted and ignored:', - 'it asks whether the check is about the instance the player is standing in rather than', - 'the account, and the answer is the same either way. The token is still validated first.', + 'The envelope carries PascalCase `Success`/`Error` next to a snake_case `error_id`,', + 'which matches neither neighbour on this worker. That mix is what the reference sends;', + 'it is not an inconsistency to clean up.', + '', + '`roomInstanceSpecificCheck` (the client sends .NET’s `True`/`False`) is accepted and', + 'ignored: it asks whether the check is about the instance the player is standing in', + 'rather than the account, and the answer is the same either way. The token is still', + 'validated first.', ].join(' '), security: AUTHED, parameters: [ @@ -266,7 +270,7 @@ const app = new Hono() ), ], responses: { - 200: json(MakerAiAccessResponse, 'Always `false`'), + 200: json(MakerAiAccessResponse, 'Always granted'), 401: UNAUTHORIZED_RESPONSE, }, }), @@ -274,7 +278,7 @@ const app = new Hono() const id = await authedId(c) if (id === null) return unauthorized(c) - return c.json(false) + return c.json({ Success: true, Error: null, error_id: null }) } ) diff --git a/apps/ai/src/openapi.ts b/apps/ai/src/openapi.ts index 52197fd..2812c55 100644 --- a/apps/ai/src/openapi.ts +++ b/apps/ai/src/openapi.ts @@ -113,12 +113,17 @@ export const GameAiSpendSummaryDenied = GameAiAccessDenied.extend({ }) /** - * `GET /makerai/user/access` — a BARE JSON boolean, not an envelope and not a `{ value }` - * wrapper. The whole body is the answer. + * `GET /makerai/user/access` — always granted, in an envelope that belongs to this endpoint + * alone: PascalCase `Success`/`Error` beside a snake_case `error_id`, and no `value` slot. + * It is neither the Game AI refusal's all-lowercase body nor the Roomie check's + * `{ success, error_id, error, value }`. Reproduced as the reference sends it — the casing + * mix is not a typo to normalise. */ -export const MakerAiAccessResponse = z - .boolean() - .describe('Whether the caller may use Maker AI; always false — no model runs here') +export const MakerAiAccessResponse = z.object({ + Success: z.boolean().describe('Whether the caller may use Maker AI. Always true'), + Error: z.null().describe('The failure message. Null — the check always passes'), + error_id: z.null().describe('The failure code. Null — the check always passes'), +}) /** * Maker AI's dollar balances. A FLAT body — no `{ success, error, value }` envelope — and diff --git a/apps/ai/src/test/integration/api.test.ts b/apps/ai/src/test/integration/api.test.ts index ebddfc3..884c74b 100644 --- a/apps/ai/src/test/integration/api.test.ts +++ b/apps/ai/src/test/integration/api.test.ts @@ -175,29 +175,31 @@ describe('GET /roomieai/user/facts', () => { }) describe('GET /makerai/user/access', () => { - // Always false — nothing here runs a model. The body is the boolean itself, not an - // envelope, matching econ's `/api/makerai/checkfreetrialeligibility`. - it('refuses access with a bare false', async () => { - const res = await SELF.fetch(`${ORIGIN}/makerai/user/access?roomInstanceSpecificCheck=False`, { + // Granted, and pinned whole: the casing is mixed on purpose (PascalCase `Success`/`Error` + // beside a snake_case `error_id`) and matches neither neighbour on this worker, so a + // "consistency" edit has to fail here rather than on the client. + it('grants access', async () => { + const res = await SELF.fetch(`${ORIGIN}/makerai/user/access?roomInstanceSpecificCheck=True`, { headers: await bearer(), }) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('application/json') - expect(await res.text()).toBe('false') + expect(await res.json()).toEqual({ Success: true, Error: null, error_id: null }) }) it('answers the same without the query param', async () => { // `roomInstanceSpecificCheck` is ignored, so its presence, absence and value change // nothing. + const granted = { Success: true, Error: null, error_id: null } const res = await SELF.fetch(`${ORIGIN}/makerai/user/access`, { headers: await bearer() }) - expect(await res.text()).toBe('false') - const trueCheck = await SELF.fetch( - `${ORIGIN}/makerai/user/access?roomInstanceSpecificCheck=True`, + expect(await res.json()).toEqual(granted) + const falseCheck = await SELF.fetch( + `${ORIGIN}/makerai/user/access?roomInstanceSpecificCheck=False`, { headers: await bearer(), } ) - expect(await trueCheck.text()).toBe('false') + expect(await falseCheck.json()).toEqual(granted) }) it('401s without a bearer token', async () => {