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/<ref> and returns {deleted_ref}
  - 404 surfaces cleanly (exit 2, "404" in stderr)
  - Missing <ref> 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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-24 00:11:43 -07:00
parent ba7b2663e8
commit 86c4723991
+131
View File
@@ -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/<ref> 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']);