// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * PLAN ยง12 core items 3, 15 and the persistence matrix: HTTP readiness, SIGTERM/SIGINT graceful * shutdown with exit code 0, in-flight requests drained, port released, database persistence * across restarts, repeated schema initialization, and server/CLI sharing one database. */ import { test, expect, assertDbInvariants, expectContainersResponse, isPortFree, pollUntil, readOperations, readSchema, readTokens, } from '../../harness'; for (const sig of ['SIGTERM', 'SIGINT'] as const) { test(`${sig}: graceful shutdown, exit code 0, port released, connections refused afterwards`, async ({ cli, startServer, http }) => { const tok = await cli.tokenNew(); const server = await startServer({ logLevel: 'DEBUG' }); expectContainersResponse(await http.get(server.url(), { token: tok })); const pid = server.pid; const exit = await server.stop(sig); expect(exit.exitCode).toBe(0); expect(exit.signal).toBeNull(); expect(exit.stdout).toBe(''); expect(exit.durationMs).toBeLessThan(5_000); const logs = server.logs(); expect(logs.unparsed, 'no error line on graceful shutdown').toEqual([]); const order = ['shutdown requested, draining http server', 'http server shut down', 'server stopped', 'database closed'].map((m) => logs.find({ msg: m })!.line); expect([...order].sort((a, b) => a - b)).toEqual(order); expect(() => process.kill(pid, 0)).toThrow(); expect(await isPortFree(server.port)).toBe(true); const r = await http.raw({ host: server.connectHost, port: server.port, data: 'GET / HTTP/1.1\r\nHost: x\r\n\r\n' }); expect(r.outcome).toBe('refused'); assertDbInvariants(server.dbPath, { operationCount: 1 }); }); } test('an in-flight request completes during graceful shutdown; new connections are refused', async ({ cli, startServer, http }) => { const tok = await cli.tokenNew(); const server = await startServer(); const f = server.fakeDocker!.many(2).delay(1500); const inflight = http.get(server.url(), { token: tok }); await pollUntil('request to reach Docker', () => (f.listCalls() === 1 ? true : undefined)); const stopping = server.stop('SIGTERM'); const r = await inflight; expect(expectContainersResponse(r)).toHaveLength(2); expect((await stopping).exitCode).toBe(0); expect(r.durationMs).toBeGreaterThan(1400); expect(server.logs().requests().map((x) => x.attrs.status)).toEqual(['200']); expect(readOperations(server.dbPath)).toHaveLength(1); }); test('state persists across restarts; schema initialization is idempotent', async ({ cli, startServer, http, ws }) => { test.slow(); const t1 = await cli.tokenNew('before first start'); const server = await startServer(); const schema = readSchema(ws.dbPath); expectContainersResponse(await http.get(server.url(), { token: t1 })); let ops = 1; const created = [t1]; for (let gen = 2; gen <= 4; gen++) { await server.restart(); expect(server.generation).toBe(gen); for (const t of created) { expectContainersResponse(await http.get(server.url(), { token: t })); ops++; } created.push(await cli.tokenNew(`generation ${gen}`)); expect(readSchema(ws.dbPath), 'schema unchanged by repeated initialization').toEqual(schema); } await cli.tokenDelete(t1); await server.restart(); const r = await http.get(server.url(), { token: t1 }); expect(r.status, 'deletion persisted across restart').toBe(401); expect((await server.stop()).exitCode).toBe(0); const rows = readTokens(ws.dbPath); expect(rows.map((x) => [x.token, x.active])).toEqual(created.map((t) => [t, t === t1 ? 0 : 1])); assertDbInvariants(ws.dbPath, { tokenCount: created.length, operationCount: ops }); for (const g of server.allOutput()) expect(g.exit?.exitCode, `generation ${g.generation} exit code`).toBe(0); }); test('two servers on different ports share one database with concurrent CLI use', async ({ cli, startServer, http, ws }) => { const a = await startServer({ label: 'a' }); const b = await startServer({ label: 'b' }); const toks = await Promise.all(Array.from({ length: 6 }, (_, i) => cli.tokenNew(`shared ${i}`))); await Promise.all(toks.flatMap((t) => [http.get(a.url(), { token: t }), http.get(b.url(), { token: t })]).map(async (p) => expectContainersResponse(await p))); await cli.tokenDelete(toks[0]); expect((await http.get(a.url(), { token: toks[0] })).status).toBe(401); expect((await http.get(b.url(), { token: toks[0] })).status).toBe(401); expect((await a.stop()).exitCode).toBe(0); expect((await b.stop()).exitCode).toBe(0); assertDbInvariants(ws.dbPath, { tokenCount: 6, operationCount: 12, auditBeforeDeletion: true }); });