import assert from 'node:assert/strict'; import test from 'node:test'; import { Hono } from 'hono'; import { platformSettings, teamMemberships, users, type Database } from '@pig/db'; import { createApp } from '../src/app'; import type { Principal } from '../src/lib/auth'; import { loadConfig } from '../src/lib/config'; import type { ApiEnv } from '../src/lib/mutation'; import { createPiggyChatRoutes, type PiggyChatProxyOptions, } from '../src/routes/piggy-chat'; const principal: Principal = { userId: '10000000-0000-4000-8000-000000000001', email: 'member@example.com', name: 'Member', isPlatformAdmin: false, teams: [{ team: 'demand', role: 'member' }], via: 'jwt', scopes: ['read', 'write'], }; function appFor( fetchImpl: typeof fetch, identity: Principal = principal, overrides: Partial = {}, ) { const app = new Hono(); app.use('*', async (context, next) => { context.set('principal', identity); await next(); }); app.route( '/', createPiggyChatRoutes({ enabled: true, internalUrl: 'http://127.0.0.1:8931', internalToken: 'internal-token-with-at-least-32-characters', fetchImpl, ...overrides, }), ); return app; } const ndjson = () => new Response(`${JSON.stringify({ type: 'done', inputTokens: 1, outputTokens: 1 })}\n`, { status: 200, headers: { 'content-type': 'application/x-ndjson' }, }); test('the authenticated proxy forwards bounded identity and relays NDJSON unchanged', async () => { let forwarded: Record | undefined; const fetchImpl: typeof fetch = async (input, init) => { assert.equal(String(input), 'http://127.0.0.1:8931/internal/chat'); assert.equal( new Headers(init?.headers).get('authorization'), 'Bearer internal-token-with-at-least-32-characters', ); forwarded = JSON.parse(String(init?.body)) as Record; return new Response( `${JSON.stringify({ type: 'content_delta', delta: 'Scoped answer' })}\n` + `${JSON.stringify({ type: 'done', inputTokens: 2, outputTokens: 3 })}\n`, { status: 200, headers: { 'content-type': 'application/x-ndjson' } }, ); }; const app = appFor(fetchImpl); const response = await app.request('/api/piggy/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: 'Summarise this contract.', context: { type: 'contract', id: '20000000-0000-4000-8000-000000000002', label: 'Order form', }, }), }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') ?? '', /application\/x-ndjson/); assert.deepEqual(forwarded, { principalUserId: principal.userId, message: 'Summarise this contract.', context: { type: 'contract', id: '20000000-0000-4000-8000-000000000002', label: 'Order form', }, }); assert.equal( await response.text(), `${JSON.stringify({ type: 'content_delta', delta: 'Scoped answer' })}\n` + `${JSON.stringify({ type: 'done', inputTokens: 2, outputTokens: 3 })}\n`, ); }); test('a credential without read scope never reaches the internal service', async () => { let fetched = false; const app = appFor( async () => { fetched = true; return new Response(); }, { ...principal, scopes: ['write'] }, ); const response = await app.request('/api/piggy/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: 'Read the book.' }), }); assert.equal(response.status, 403); assert.equal(fetched, false); }); test('a docked page context reaches the chat service unaltered', async () => { let forwarded: Record | undefined; const app = appFor(async (_input, init) => { forwarded = JSON.parse(String(init?.body)) as Record; return ndjson(); }); const response = await app.request('/api/piggy/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: 'What is idle?', context: { type: 'page', route: '/capacity', label: 'Capacity' }, }), }); assert.equal(response.status, 200); assert.deepEqual(forwarded?.context, { type: 'page', route: '/capacity', label: 'Capacity', }); }); // A page context carries no record, so admitting one would put a nonsense // shape in front of the model rather than failing at the boundary. test('a page context may not smuggle a record id, and an unknown route is refused', async () => { let fetched = false; const app = appFor(async () => { fetched = true; return ndjson(); }); for (const context of [ { type: 'page', route: '/not-a-page' }, { type: 'page', route: '/margin', id: '20000000-0000-4000-8000-000000000002' }, { type: 'page' }, ]) { const response = await app.request('/api/piggy/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: 'Where are we?', context }), }); assert.equal(response.status, 400); assert.equal(((await response.json()) as { code: string }).code, 'invalid_request'); } assert.equal(fetched, false); }); test('the stored admin toggle disables chat without the environment changing', async () => { let fetched = false; let piggyEnabled = true; const app = appFor( async () => { fetched = true; return ndjson(); }, principal, { resolvePiggyEnabled: async () => piggyEnabled }, ); assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: true, canUse: true, }); piggyEnabled = false; assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: false, canUse: false, }); const response = await app.request('/api/piggy/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: 'Where are we?' }), }); assert.equal(response.status, 503); assert.equal(fetched, false); }); // Losing the settings row must degrade to the environment gate. A dock on // every page turns one failed query into a site-wide outage otherwise. test('an unreadable settings row falls back to the environment gate', async () => { const app = appFor(async () => ndjson(), principal, { resolvePiggyEnabled: async () => { throw new Error('platform settings unavailable'); }, }); assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: true, canUse: true, }); }); test('the environment gate still overrides a stored toggle that says yes', async () => { const app = appFor(async () => ndjson(), principal, { enabled: false, resolvePiggyEnabled: async () => true, }); assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: false, canUse: false, }); }); // --------------------------------------------------------------------------- // Composition // --------------------------------------------------------------------------- /** * Enough of a Database to authenticate a development request and read the * settings row, and nothing more. * * Predicates are ignored on purpose: this asserts a WIRING, and a fake that * tried to execute SQL semantics would be a worse test of the wiring and a * pointless test of Drizzle. Anything the app queries beyond these three * tables comes back empty, which is what an untouched deployment looks like. */ function stubDatabase(store: { piggyEnabled: boolean }): Database { const rowsFor = (table: unknown): Record[] => { if (table === users) { return [ { id: principal.userId, email: principal.email, name: principal.name, isPlatformAdmin: false, deactivatedAt: null, }, ]; } if (table === teamMemberships) return [{ team: 'demand', role: 'member' }]; if (table === platformSettings) return [{ piggyEnabled: store.piggyEnabled }]; return []; }; const query = (rows: Record[]): Record => { const chain: Record = { from: (table: unknown) => query(rowsFor(table)), where: () => chain, limit: () => chain, orderBy: () => chain, innerJoin: () => chain, leftJoin: () => chain, values: () => chain, onConflictDoNothing: () => chain, returning: () => chain, then: (resolve: (value: Record[]) => unknown) => resolve(rows), }; return chain; }; return { select: () => query([]), insert: (table: unknown) => query(rowsFor(table)), } as unknown as Database; } /** * The regression this file could not previously catch. * * The three toggle tests above build the routes themselves and inject a * resolver, so every one of them stayed green through a release in which * `createApp` never passed one — turning Piggy off in the admin UI did nothing * at all in production. Only a request through the composed app proves the * stored setting is consulted, so this one goes through `createApp`. */ test('createApp wires the stored toggle into the chat routes', async () => { const store = { piggyEnabled: false }; const config = loadConfig({ NODE_ENV: 'development', DATABASE_URL: 'postgres://pig:pig@localhost:5432/pig', PIGGY_ENABLED: 'true', PIGGY_INTERNAL_URL: 'http://127.0.0.1:8931', PIGGY_INTERNAL_TOKEN: 'internal-token-with-at-least-32-characters', }); // Null provider is the development path: no token, principal comes from the // first user in the table. What is under test is the toggle, not the auth. const app = createApp(config, stubDatabase(store), null); assert.equal(config.PIGGY_ENABLED, true); assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: false, canUse: false, }); store.piggyEnabled = true; assert.deepEqual(await (await app.request('/api/piggy/status')).json(), { enabled: true, canUse: true, }); });