import { describe, test, expect, beforeEach, mock } from 'bun:test' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { randomUUID } from 'node:crypto' import { tmpdir } from 'node:os' import { mockConfigModule } from './helpers/mock-config' // Mock config before any module imports mock.module('../config', () => mockConfigModule()) import { Hono } from 'hono' import { Database } from 'bun:sqlite' // Dynamic imports — modules may or may not exist let migrateDatabase: ((db: Database) => void) | undefined let initDatabase: ((path?: string) => Database) | undefined let resetDbSingleton: (() => void) | undefined let getDb: (() => Database) | undefined try { const dbMod = await import('../db/sqlite') migrateDatabase = dbMod.migrateDatabase getDb = dbMod.getDb initDatabase = dbMod.initDatabase resetDbSingleton = dbMod.resetDbSingleton } catch { // Module not implemented yet } let authRoutes: Hono | undefined try { const mod = await import('../routes/web/auth-routes') authRoutes = mod.default } catch { // Module not implemented yet } let sessionRoutes: Hono | undefined try { const mod = await import('../routes/web/sessions') sessionRoutes = mod.default } catch { // Module not implemented yet } let controlRoutes: Hono | undefined try { const mod = await import('../routes/web/control') controlRoutes = mod.default } catch { // Module not implemented yet } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const PKG_ROOT = resolve(import.meta.dir, '../..') const RCS_SRC = resolve(PKG_ROOT, 'src') const BRIDGE_SRC = resolve(PKG_ROOT, '..', '..', '..', 'src', 'bridge') const WEB_SRC = resolve(PKG_ROOT, 'web', 'src') function readFileSafe(p: string): string | null { try { return readFileSync(p, 'utf-8') } catch { return null } } function freshDb(): Database { if (resetDbSingleton) { try { resetDbSingleton() } catch { // ignore } } if (initDatabase) { const uniquePath = resolve( tmpdir(), `rcs-test-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) return initDatabase(uniquePath) } const db = new Database(':memory:') db.exec('PRAGMA foreign_keys = ON') if (migrateDatabase) migrateDatabase(db) return db } interface TestUser { id: string username: string role: string accessToken: string } function createUserDirectly( db: Database, opts: { username: string; role: string }, ): TestUser { const id = `usr_${randomUUID().replace(/-/g, '')}` const now = new Date().toISOString() const passwordHash = '$argon2id$v=19$m=65536,t=3,p=1$dGVzdHNhbHR0ZXN0$' + 'dGVzdGhhc2h0ZXN0aGFzaHRlc3RoYXNodGVzdGhhc2g' try { db.query( `INSERT INTO users (id, username, password_hash, role, created_at) VALUES ($id, $username, $hash, $role, $now)`, ).run({ $id: id, $username: opts.username, $hash: passwordHash, $role: opts.role, $now: now, }) } catch { // table may not exist } return { id, username: opts.username, role: opts.role, accessToken: '' } } function issueTokenForUser(db: Database, userId: string): string { try { const { issueSessionToken } = require('../auth/session') as { issueSessionToken: ( userId: string, db: Database, ) => { accessToken: string; refreshToken: string } } const { accessToken } = issueSessionToken(userId, db) return accessToken } catch { const { createHash } = require('node:crypto') as typeof import('node:crypto') const token = `rct_${randomUUID().replace(/-/g, '')}` const tokenHash = createHash('sha256').update(token).digest('hex') const now = new Date().toISOString() const expiresAt = new Date(Date.now() + 3600_000).toISOString() try { db.query( `INSERT INTO session_tokens (token_hash, user_id, kind, expires_at, created_at) VALUES ($hash, $userId, 'access', $exp, $now)`, ).run({ $hash: tokenHash, $userId: userId, $exp: expiresAt, $now: now }) } catch { // table may not exist } return token } } async function createAndLoginUser( db: Database, opts: { username: string; role: string }, ): Promise { const user = createUserDirectly(db, opts) user.accessToken = issueTokenForUser(db, user.id) return user } function authHeader(token: string): Record { return { Authorization: `Bearer ${token}` } } function createApp(): Hono { const app = new Hono() if (authRoutes) app.route('/web/auth', authRoutes) if (sessionRoutes) app.route('/web', sessionRoutes) if (controlRoutes) app.route('/web', controlRoutes) return app } // =========================================================================== // TESTS — RCS_WORKER_TOKENS env removed // =========================================================================== describe('RCS_WORKER_TOKENS env removed', () => { let db: Database beforeEach(() => { db = freshDb() }) test('EnvironmentRecord type does not contain workerTokenHash field', async () => { // Read store.ts and check that EnvironmentRecord no longer has workerTokenHash const storeContent = readFileSafe(resolve(RCS_SRC, 'store.ts')) if (!storeContent) return // Check the EnvironmentRecord interface/type definition // After Phase 3, workerTokenHash should not appear in the type const envRecordMatch = storeContent.match( /(?:interface|type)\s+EnvironmentRecord[^}]*}/s, ) if (envRecordMatch) { expect(envRecordMatch[0]).not.toContain('workerTokenHash') } }) test('storeCreateEnvironment does not accept workerTokenHash parameter', async () => { const storeContent = readFileSafe(resolve(RCS_SRC, 'store.ts')) if (!storeContent) return // After Phase 3, the StoreEnvironmentRequest type should not include workerTokenHash const reqTypeMatch = storeContent.match( /(?:interface|type)\s+Store\w*Request[^}]*}/s, ) if (reqTypeMatch) { expect(reqTypeMatch[0]).not.toContain('workerTokenHash') } // Also check the function signature doesn't reference workerTokenHash const createFnMatch = storeContent.match( /function\s+storeCreateEnvironment[^{]*\{[^}]*\}/s, ) if (createFnMatch) { expect(createFnMatch[0]).not.toContain('workerTokenHash') } }) }) // =========================================================================== // TESTS — requireWorkerToken middleware removed // =========================================================================== describe('requireWorkerToken middleware removed', () => { test('requireWorkerToken does not exist in auth/middleware.ts', () => { const middlewareContent = readFileSafe( resolve(RCS_SRC, 'auth', 'middleware.ts'), ) if (!middlewareContent) return // After Phase 3, requireWorkerToken should not be exported const hasExport = /export\s+(?:async\s+)?function\s+requireWorkerToken/.test( middlewareContent, ) expect(hasExport).toBe(false) }) test('verifyWorkerTokenForEnv does not exist in auth/middleware.ts', () => { const middlewareContent = readFileSafe( resolve(RCS_SRC, 'auth', 'middleware.ts'), ) if (!middlewareContent) return const hasExport = /export\s+(?:async\s+)?function\s+verifyWorkerTokenForEnv/.test( middlewareContent, ) expect(hasExport).toBe(false) }) test('POST /web/sessions/:id/events does not require X-Worker-Token', async () => { const app = createApp() const db = freshDb() const user = await createAndLoginUser(db, { username: 'worker-test', role: 'member', }) const sessionId = `ses_${randomUUID().replace(/-/g, '')}` try { const now = new Date().toISOString() db.query( `INSERT INTO sessions (id, created_at, updated_at) VALUES ($id, $now, $now)`, ).run({ $id: sessionId, $now: now }) db.query( `INSERT INTO session_owners (session_id, owner_type, owner_id) VALUES ($sid, 'user', $uid)`, ).run({ $sid: sessionId, $uid: user.id }) } catch { // tables may not exist } // Access without X-Worker-Token — should not get a worker_token_required error const res = await app.request(`/web/sessions/${sessionId}/events`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(user.accessToken), }, body: JSON.stringify({ type: 'message', content: 'test' }), }) // Should NOT return worker_token_required error if (res.status === 401) { const body = (await res.json()) as Record const errorType = (body.error as Record)?.type || body.type expect(errorType).not.toBe('worker_token_required') } }) test('POST /web/sessions/:id/control does not require X-Worker-Token', async () => { const app = createApp() const db = freshDb() const user = await createAndLoginUser(db, { username: 'control-test', role: 'member', }) const sessionId = `ses_${randomUUID().replace(/-/g, '')}` try { const now = new Date().toISOString() db.query( `INSERT INTO sessions (id, created_at, updated_at) VALUES ($id, $now, $now)`, ).run({ $id: sessionId, $now: now }) db.query( `INSERT INTO session_owners (session_id, owner_type, owner_id) VALUES ($sid, 'user', $uid)`, ).run({ $sid: sessionId, $uid: user.id }) } catch { // tables may not exist } const res = await app.request(`/web/sessions/${sessionId}/control`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(user.accessToken), }, body: JSON.stringify({ action: 'interrupt' }), }) if (res.status === 401) { const body = (await res.json()) as Record const errorType = (body.error as Record)?.type || body.type expect(errorType).not.toBe('worker_token_required') } }) test('POST /web/sessions/:id/interrupt does not require X-Worker-Token', async () => { const app = createApp() const db = freshDb() const user = await createAndLoginUser(db, { username: 'interrupt-test', role: 'member', }) const sessionId = `ses_${randomUUID().replace(/-/g, '')}` try { const now = new Date().toISOString() db.query( `INSERT INTO sessions (id, created_at, updated_at) VALUES ($id, $now, $now)`, ).run({ $id: sessionId, $now: now }) db.query( `INSERT INTO session_owners (session_id, owner_type, owner_id) VALUES ($sid, 'user', $uid)`, ).run({ $sid: sessionId, $uid: user.id }) } catch { // tables may not exist } const res = await app.request(`/web/sessions/${sessionId}/interrupt`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(user.accessToken), }, body: JSON.stringify({}), }) if (res.status === 401) { const body = (await res.json()) as Record const errorType = (body.error as Record)?.type || body.type expect(errorType).not.toBe('worker_token_required') } }) test('existing routes do not call worker token verification', () => { // Check sessions.ts does not import or use requireWorkerToken/verifyWorkerTokenForEnv const sessionsContent = readFileSafe( resolve(RCS_SRC, 'routes', 'web', 'sessions.ts'), ) if (sessionsContent) { expect(sessionsContent).not.toContain('requireWorkerToken') expect(sessionsContent).not.toContain('verifyWorkerTokenForEnv') } // Check control.ts does not import or use them either const controlContent = readFileSafe( resolve(RCS_SRC, 'routes', 'web', 'control.ts'), ) if (controlContent) { expect(controlContent).not.toContain('requireWorkerToken') expect(controlContent).not.toContain('verifyWorkerTokenForEnv') } }) }) // =========================================================================== // TESTS — worker token related code removed // =========================================================================== describe('worker token related code removed', () => { test('src/bridge/bridgeConfig.ts does not read RCS_WORKER_TOKENS', () => { const bridgeConfigContent = readFileSafe( resolve(BRIDGE_SRC, 'bridgeConfig.ts'), ) if (!bridgeConfigContent) return expect(bridgeConfigContent).not.toContain('RCS_WORKER_TOKENS') }) test('src/bridge/bridgeConfig.ts does not fall back to SALUZI_BRIDGE_OAUTH_TOKEN as worker token', () => { const bridgeConfigContent = readFileSafe( resolve(BRIDGE_SRC, 'bridgeConfig.ts'), ) if (!bridgeConfigContent) return // getWorkerToken() should be removed entirely, or at least not reference // SALUZI_BRIDGE_OAUTH_TOKEN as a worker token fallback const hasWorkerTokenFn = /function\s+getWorkerToken\b/.test( bridgeConfigContent, ) if (hasWorkerTokenFn) { // If the function still exists, it should not reference SALUZI_BRIDGE_OAUTH_TOKEN // as a worker token fallback const fnBody = bridgeConfigContent.match( /function\s+getWorkerToken\b[^}]*\}/s, ) if (fnBody) { expect(fnBody[0]).not.toContain('SALUZI_BRIDGE_OAUTH_TOKEN') } } }) test('Web UI client.ts does not send X-Worker-Token header', () => { const clientContent = readFileSafe(resolve(WEB_SRC, 'api', 'client.ts')) if (!clientContent) return expect(clientContent).not.toContain('X-Worker-Token') }) test('Web UI no longer has WorkerTokenPrompt or it is not referenced', () => { // Check if WorkerTokenPrompt.tsx exists and is referenced const workerTokenPromptPath = resolve( WEB_SRC, 'components', 'WorkerTokenPrompt.tsx', ) const promptContent = readFileSafe(workerTokenPromptPath) if (promptContent) { // File exists — check it's not imported anywhere const pagesDir = resolve(WEB_SRC, 'pages') const dashboardContent = readFileSafe(resolve(pagesDir, 'Dashboard.tsx')) const sessionDetailContent = readFileSafe( resolve(pagesDir, 'SessionDetail.tsx'), ) if (dashboardContent) { expect(dashboardContent).not.toContain('WorkerTokenPrompt') } if (sessionDetailContent) { expect(sessionDetailContent).not.toContain('WorkerTokenPrompt') } } // If file doesn't exist, test passes }) test('useWorkerTokens hook is not imported in active components', () => { // Check if useWorkerTokens is still referenced in active components const dashboardContent = readFileSafe( resolve(WEB_SRC, 'pages', 'Dashboard.tsx'), ) const sessionDetailContent = readFileSafe( resolve(WEB_SRC, 'pages', 'SessionDetail.tsx'), ) if (dashboardContent) { expect(dashboardContent).not.toMatch(/import.*useWorkerTokens/) } if (sessionDetailContent) { expect(sessionDetailContent).not.toMatch(/import.*useWorkerTokens/) } }) test('getWorkerToken and getWorkerTokenHash are removed from bridgeConfig', () => { const bridgeConfigContent = readFileSafe( resolve(BRIDGE_SRC, 'bridgeConfig.ts'), ) if (!bridgeConfigContent) return expect(bridgeConfigContent).not.toMatch( /export\s+function\s+getWorkerToken\b/, ) expect(bridgeConfigContent).not.toMatch( /export\s+function\s+getWorkerTokenHash\b/, ) }) })