From 86c47239918a90a4ed385df7f916d4b2e8a170bb Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 24 Apr 2026 00:11:43 -0700 Subject: [PATCH] test(setup-gbrain): list-orphans active-ref filtering + delete-project 404 6 new tests bringing the supabase-provision suite to 28: list-orphans: - Filters to gbrain-prefixed projects, excludes the active-ref derived from ~/.gbrain/config.json's pooler URL - Treats all gbrain-prefixed projects as orphans when no config exists (first run on a new machine) - Respects custom --name-prefix for users who named their brain something else delete-project: - Happy path sends DELETE /v1/projects/ and returns {deleted_ref} - 404 surfaces cleanly (exit 2, "404" in stderr) - Missing positional rejected with exit 2 Uses per-test tmpdir HOME with a stubbed ~/.gbrain/config.json so active-ref extraction runs against deterministic fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/gbrain-supabase-provision.test.ts | 131 +++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/test/gbrain-supabase-provision.test.ts b/test/gbrain-supabase-provision.test.ts index beb2cd29..917ebde5 100644 --- a/test/gbrain-supabase-provision.test.ts +++ b/test/gbrain-supabase-provision.test.ts @@ -14,6 +14,8 @@ */ import { describe, test, expect, afterEach } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); @@ -410,6 +412,135 @@ describe('pooler-url', () => { }); }); +describe('list-orphans (D20)', () => { + const MOCK_PROJECTS = [ + { ref: 'aaaaaaaaaaaaaaaaaaaa', name: 'gbrain', created_at: '2026-04-20', region: 'us-east-1' }, + { ref: 'bbbbbbbbbbbbbbbbbbbb', name: 'gbrain-backup', created_at: '2026-04-21', region: 'us-east-1' }, + { ref: 'cccccccccccccccccccc', name: 'my-production', created_at: '2026-04-15', region: 'us-west-2' }, + { ref: 'dddddddddddddddddddd', name: 'gbrain', created_at: '2026-04-22', region: 'eu-west-1' }, + ]; + + test('lists gbrain-prefixed projects that are NOT the active brain', async () => { + mock = startMock({ + 'GET /v1/projects': () => jsonResp(MOCK_PROJECTS), + }); + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-orphan-')); + // use top-level fs + fs.mkdirSync(path.join(home, '.gbrain')); + fs.writeFileSync( + path.join(home, '.gbrain', 'config.json'), + JSON.stringify({ + engine: 'postgres', + // Active brain points at aaaaaaaaaaaaaaaaaaaa + database_url: 'postgresql://postgres.aaaaaaaaaaaaaaaaaaaa:pw@host:6543/postgres', + }) + ); + try { + const r = await runBin(['list-orphans', '--json'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + SUPABASE_API_BASE: mock.url, + HOME: home, + }); + expect(r.status).toBe(0); + const j = JSON.parse(r.stdout); + expect(j.active_ref).toBe('aaaaaaaaaaaaaaaaaaaa'); + expect(j.orphans.length).toBe(2); + const refs = j.orphans.map((o: any) => o.ref).sort(); + expect(refs).toEqual(['bbbbbbbbbbbbbbbbbbbb', 'dddddddddddddddddddd']); + // my-production is NOT in orphans — filtered out by gbrain prefix + expect(refs).not.toContain('cccccccccccccccccccc'); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + + test('treats all gbrain-prefixed projects as orphans when no active config exists', async () => { + mock = startMock({ + 'GET /v1/projects': () => jsonResp(MOCK_PROJECTS), + }); + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-no-cfg-')); + try { + const r = await runBin(['list-orphans', '--json'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + SUPABASE_API_BASE: mock.url, + HOME: home, + }); + expect(r.status).toBe(0); + const j = JSON.parse(r.stdout); + expect(j.active_ref).toBeNull(); + // All 3 gbrain-prefixed projects are orphans when no active config + expect(j.orphans.length).toBe(3); + } finally { + // use top-level fs + fs.rmSync(home, { recursive: true, force: true }); + } + }); + + test('respects custom --name-prefix', async () => { + mock = startMock({ + 'GET /v1/projects': () => + jsonResp([ + { ref: 'aaaaaaaaaaaaaaaaaaaa', name: 'my-prefix-one', created_at: '2026-04-20' }, + { ref: 'bbbbbbbbbbbbbbbbbbbb', name: 'gbrain', created_at: '2026-04-20' }, + ]), + }); + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-prefix-')); + try { + const r = await runBin(['list-orphans', '--name-prefix', 'my-prefix', '--json'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + SUPABASE_API_BASE: mock.url, + HOME: home, + }); + const j = JSON.parse(r.stdout); + expect(j.orphans.length).toBe(1); + expect(j.orphans[0].name).toBe('my-prefix-one'); + } finally { + // use top-level fs + fs.rmSync(home, { recursive: true, force: true }); + } + }); +}); + +describe('delete-project (D20)', () => { + test('issues DELETE /v1/projects/ and returns the deleted ref', async () => { + let deletedPath = ''; + mock = startMock({ + 'DELETE /v1/projects/abcdefghijklmnopqrst': (req) => { + deletedPath = new URL(req.url).pathname; + return jsonResp({ id: 1, ref: 'abcdefghijklmnopqrst', name: 'gbrain' }); + }, + }); + const r = await runBin(['delete-project', 'abcdefghijklmnopqrst', '--json'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + SUPABASE_API_BASE: mock.url, + }); + expect(r.status).toBe(0); + expect(deletedPath).toBe('/v1/projects/abcdefghijklmnopqrst'); + const j = JSON.parse(r.stdout); + expect(j.deleted_ref).toBe('abcdefghijklmnopqrst'); + }); + + test('surfaces 404 when the project does not exist', async () => { + mock = startMock({ + 'DELETE /v1/projects/nonexistent': () => jsonResp({ message: 'Project not found' }, 404), + }); + const r = await runBin(['delete-project', 'nonexistent'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + SUPABASE_API_BASE: mock.url, + }); + expect(r.status).toBe(2); + expect(r.stderr).toContain('404'); + }); + + test('requires a ref', async () => { + const r = await runBin(['delete-project'], { + SUPABASE_ACCESS_TOKEN: 'sbp_test', + }); + expect(r.status).toBe(2); + expect(r.stderr).toContain('missing'); + }); +}); + describe('general', () => { test('unknown subcommand exits 2', async () => { const r = await runBin(['nope']);