import { describe, test, expect, beforeEach, mock } from 'bun:test' import { randomUUID } from 'node:crypto' import { mockConfigModule } from './helpers/mock-config' mock.module('../config', () => mockConfigModule()) import { Database } from 'bun:sqlite' import { initDatabase, resetDbSingleton, migrateDatabase, getDb, } from '../db/sqlite' import { storeReset, storeCreateSession, storeGetSession, storeUpdateSession, storeDeleteSession, storeBindSession, storeCreateEnvironment, storeGetEnvironment, storeListSessionsByOwnerUuid, } from '../store' import { resolveOwnedWebSessionId, listWebSessionsByOwnerUuid, listWebSessionSummariesByOwnerUuid, listSessionSummariesByOwnerUuid, } from '../services/session' import { storeListSessionsVisibleToUser } from '../services/session-visibility' import { Hono } from 'hono' import { sessionAuth } from '../auth/middleware' import { issueToken } from '../auth/token' function freshDb(): Database { try { resetDbSingleton() } catch {} const uniquePath = `/tmp/rcs-design-gap-${randomUUID().replace(/-/g, '').slice(0, 8)}.db` return initDatabase(uniquePath) } function createSqliteUser( db: Database, opts: { username: string; role: string }, ): string { const id = `usr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ($id, $u, 'hash', $r, $now)`, ).run({ $id: id, $u: opts.username, $r: opts.role, $now: now }) return id } function createSqliteTeam( db: Database, opts: { slug: string; createdBy: string }, ): string { const id = `team_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() db.query( `INSERT INTO teams (id, name, slug, created_by, created_at, updated_at) VALUES ($id, $name, $slug, $by, $now, $now)`, ).run({ $id: id, $name: opts.slug, $slug: opts.slug, $by: opts.createdBy, $now: now, }) return id } function addTeamMember( db: Database, teamId: string, userId: string, role: string, ): void { const now = new Date().toISOString() db.query( `INSERT OR IGNORE INTO team_members (team_id, user_id, role, added_at, added_by) VALUES ($t, $u, $r, $now, $by)`, ).run({ $t: teamId, $u: userId, $r: role, $by: userId, $now: now }) } // =========================================================================== // Gap 6: storeCreateSession persists to SQLite sessions table // =========================================================================== describe('Gap 6: storeCreateSession persists to SQLite', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('storeCreateSession writes a row to the SQLite sessions table', () => { const session = storeCreateSession({ title: 'hello', source: 'web' }) const row = db .query('SELECT * FROM sessions WHERE id = $id') .get({ $id: session.id }) as Record | null expect(row).not.toBeNull() expect(row!.id).toBe(session.id) expect(row!.title).toBe('hello') expect(row!.source).toBe('web') expect(row!.status).toBe('idle') expect(row!.visibility).toBe('private') }) test('storeCreateSession with cse_ prefix persists correctly', () => { const session = storeCreateSession({ idPrefix: 'cse_', source: 'cli' }) expect(session.id).toMatch(/^cse_/) const row = db .query('SELECT * FROM sessions WHERE id = $id') .get({ $id: session.id }) as Record | null expect(row).not.toBeNull() expect(row!.source).toBe('cli') }) test('storeUpdateSession updates the SQLite row', () => { const session = storeCreateSession({}) storeUpdateSession(session.id, { title: 'updated', status: 'active' }) const row = db .query('SELECT title, status FROM sessions WHERE id = $id') .get({ $id: session.id }) as { title: string; status: string } | null expect(row).not.toBeNull() expect(row!.title).toBe('updated') expect(row!.status).toBe('active') }) test('storeDeleteSession removes the SQLite row', () => { const session = storeCreateSession({}) expect(storeDeleteSession(session.id)).toBe(true) const row = db .query('SELECT * FROM sessions WHERE id = $id') .get({ $id: session.id }) expect(row).toBeNull() }) test('storeReset clears the sessions table', () => { storeCreateSession({}) storeCreateSession({}) storeReset() const count = db.query('SELECT COUNT(*) as cnt FROM sessions').get() as { cnt: number } expect(count.cnt).toBe(0) }) }) // =========================================================================== // Gap 7: storeCreateEnvironment persists to SQLite environments table // =========================================================================== describe('Gap 7: storeCreateEnvironment persists to SQLite', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('storeCreateEnvironment writes a row to the SQLite environments table', () => { const env = storeCreateEnvironment({ secret: 'my-secret', machineName: 'mac1', workerType: 'claude_code', }) const row = db .query('SELECT * FROM environments WHERE id = $id') .get({ $id: env.id }) as Record | null expect(row).not.toBeNull() expect(row!.id).toBe(env.id) expect(row!.machine_name).toBe('mac1') expect(row!.worker_type).toBe('claude_code') expect(row!.status).toBe('active') }) test('storeReset clears the environments table', () => { storeCreateEnvironment({ secret: 's1' }) storeReset() const count = db .query('SELECT COUNT(*) as cnt FROM environments') .get() as { cnt: number } expect(count.cnt).toBe(0) }) }) // =========================================================================== // Gap 3: storeBindSession supports team ownership // =========================================================================== describe('Gap 3: storeBindSession supports ownerType', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('storeBindSession with ownerType=team writes team ownership', () => { const session = storeCreateSession({}) const teamId = 'team_abc123' storeBindSession(session.id, teamId, 'team') const row = db .query( 'SELECT owner_type, owner_id FROM session_owners WHERE session_id = $sid', ) .get({ $sid: session.id }) as { owner_type: string owner_id: string } | null expect(row).not.toBeNull() expect(row!.owner_type).toBe('team') expect(row!.owner_id).toBe(teamId) }) test('storeBindSession defaults to ownerType=user', () => { const session = storeCreateSession({}) storeBindSession(session.id, 'user-1') const row = db .query('SELECT owner_type FROM session_owners WHERE session_id = $sid') .get({ $sid: session.id }) as { owner_type: string } | null expect(row!.owner_type).toBe('user') }) }) // =========================================================================== // Gap 4: sessionAuth admin bypass uses real admin userId // =========================================================================== describe('Gap 4: sessionAuth admin bypass uses real userId', () => { let db: Database let app: Hono beforeEach(() => { db = freshDb() storeReset() app = new Hono() app.get('/protected', async (c, next) => { const result = await sessionAuth(c, next) if (result) return result return c.json({ userId: c.get('userId'), isAdmin: c.get('isAdmin') }) }) }) test('admin API key bypass returns __system__ userId (not a real admin)', async () => { createSqliteUser(db, { username: 'admin1', role: 'admin' }) const res = await app.request('/protected', { headers: { Authorization: 'Bearer test-api-key' }, }) const body = (await res.json()) as Record expect(body.isAdmin).toBe(true) expect(body.userId).toBe('__system__') }) test('admin API key bypass returns __system__ when no admin user exists', async () => { const res = await app.request('/protected', { headers: { Authorization: 'Bearer test-api-key' }, }) const body = (await res.json()) as Record expect(body.isAdmin).toBe(true) expect(body.userId).toBe('__system__') }) }) // =========================================================================== // Gap 1: storeListSessionsByOwnerUuid uses new visibility rules // =========================================================================== describe('Gap 1: storeListSessionsByOwnerUuid uses visibility rules', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('does NOT auto-bind orphan sessions', () => { const session = storeCreateSession({}) // Don't bind it — it's an orphan const result = storeListSessionsByOwnerUuid('some-random-user') const ids = result.map(s => s.id) expect(ids).not.toContain(session.id) }) test('ACP sessions are NOT globally visible to arbitrary users', () => { const acpSession = storeCreateSession({ source: 'acp' }) // A regular user who has no ownership should NOT see it const result = storeListSessionsByOwnerUuid('random-user') const ids = result.map(s => s.id) expect(ids).not.toContain(acpSession.id) }) test('ACP sessions are visible to their owner', () => { const acpSession = storeCreateSession({ source: 'acp' }) storeBindSession(acpSession.id, 'acp-user') const result = storeListSessionsByOwnerUuid('acp-user') const ids = result.map(s => s.id) expect(ids).toContain(acpSession.id) }) test('still returns explicitly owned sessions', () => { const s1 = storeCreateSession({}) const s2 = storeCreateSession({}) storeBindSession(s1.id, 'uuid-1') storeBindSession(s2.id, 'uuid-1') const owned = storeListSessionsByOwnerUuid('uuid-1') expect(owned).toHaveLength(2) }) test('public sessions are visible to all users', () => { const publicSession = storeCreateSession({}) // Set visibility to public in SQLite db.query("UPDATE sessions SET visibility = 'public' WHERE id = $id").run({ $id: publicSession.id, }) const result = storeListSessionsByOwnerUuid('stranger') const ids = result.map(s => s.id) expect(ids).toContain(publicSession.id) }) }) // =========================================================================== // Gap 2: resolveOwnedWebSessionId removes auto-bind // =========================================================================== describe('Gap 2: resolveOwnedWebSessionId removes auto-bind', () => { beforeEach(() => { storeReset() try { resetDbSingleton() } catch {} initDatabase( `/tmp/rcs-gap2-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) }) test('does NOT auto-bind orphan session to requesting user', () => { const session = storeCreateSession({}) // Session exists but has no owner const result = resolveOwnedWebSessionId(session.id, 'random-user') expect(result).toBeNull() }) test('returns session ID when user is the owner', () => { const session = storeCreateSession({}) storeBindSession(session.id, 'owner-user') const result = resolveOwnedWebSessionId(session.id, 'owner-user') expect(result).toBe(session.id) }) test('returns null when session is owned by another user', () => { const session = storeCreateSession({}) storeBindSession(session.id, 'other-user') const result = resolveOwnedWebSessionId(session.id, 'me') expect(result).toBeNull() }) }) // =========================================================================== // Gap 5: ACP sessions associated with registered user // =========================================================================== describe('Gap 5: ACP sessions associated with registered user', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('ACP session bound to a user is visible only to that user', () => { const acpSession = storeCreateSession({ source: 'acp' }) storeBindSession(acpSession.id, 'acp-owner') // Owner can see it const ownerResult = storeListSessionsByOwnerUuid('acp-owner') expect(ownerResult.map(s => s.id)).toContain(acpSession.id) // Other user cannot see it const otherResult = storeListSessionsByOwnerUuid('other-user') expect(otherResult.map(s => s.id)).not.toContain(acpSession.id) }) }) // =========================================================================== // Gap 8: Web UI session list uses new visibility rules // =========================================================================== describe('Gap 8: Web UI session list uses visibility rules', () => { let db: Database beforeEach(() => { db = freshDb() storeReset() }) test('listWebSessionsByOwnerUuid returns only visible sessions', () => { // Create an orphan session (no owner) — should NOT appear for regular user storeCreateSession({ title: 'orphan' }) // Create a session owned by this user const mySession = storeCreateSession({ title: 'mine' }) storeBindSession(mySession.id, 'web-user') const result = listWebSessionsByOwnerUuid('web-user') const ids = result.map(s => s.id) expect(ids).not.toContain('orphan') }) test('listWebSessionsByOwnerUuid does not include ACP sessions for non-owners', () => { const acpSession = storeCreateSession({ source: 'acp', title: 'acp-agent' }) // Don't bind it to the web user const result = listWebSessionsByOwnerUuid('web-user') const ids = result.map(s => s.id) // ACP session ID gets transformed to web format, so check the raw ID isn't there expect(result.length).toBe(0) }) test('listWebSessionSummariesByOwnerUuid does not auto-bind orphans', () => { storeCreateSession({ title: 'orphan-summary' }) const result = listWebSessionSummariesByOwnerUuid('web-user') expect(result).toHaveLength(0) }) test('listSessionSummariesByOwnerUuid does not auto-bind orphans', () => { storeCreateSession({ title: 'orphan-summary-2' }) const result = listSessionSummariesByOwnerUuid('web-user') expect(result).toHaveLength(0) }) test('listWebSessionsByOwnerUuid includes public sessions', () => { const publicSession = storeCreateSession({ title: 'public-session' }) db.query("UPDATE sessions SET visibility = 'public' WHERE id = $id").run({ $id: publicSession.id, }) const result = listWebSessionsByOwnerUuid('any-user') const ids = result.map(s => s.id) expect(ids).toContain(publicSession.id) }) })