mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
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:
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user