profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
publicMode.test.ts 3,828 bytes
1 import { mkdtemp, rm, stat } from 'node:fs/promises'
2 import { tmpdir } from 'node:os'
3 import path from 'node:path'
4 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5 import { buildApp } from './app'
6 import { applyDemoMode, isPublicDemo } from './runtimeMode'
7 import { SessionStore } from './store/sessionStore'
8 import { createLlmMock } from './providers/llmMock'
9 import { createSttMock } from './providers/sttMock'
10
11 /**
12 * The public instance is the same app with its local-machine powers removed.
13 * These tests pin the three that would otherwise be handed to strangers: the
14 * filesystem routes, the settings writer, and the caller-chosen output folder.
15 */
16 describe('public demo mode', () => {
17 let baseDir: string
18 let sandboxRoot: string
19
20 async function makeApp(publicDemo: boolean) {
21 const app = buildApp({
22 store: new SessionStore(path.join(baseDir, 'sessions')),
23 llm: createLlmMock(),
24 stt: createSttMock(),
25 demoLocked: publicDemo,
26 publicDemo,
27 sandboxRoot: publicDemo ? sandboxRoot : undefined,
28 env: { MOCK_PROVIDERS: '1' },
29 envPath: path.join(baseDir, '.env'),
30 })
31 await app.ready()
32 return app
33 }
34
35 beforeEach(async () => {
36 baseDir = await mkdtemp(path.join(tmpdir(), 'voicetask-public-'))
37 sandboxRoot = path.join(baseDir, 'sandbox')
38 })
39
40 afterEach(async () => {
41 await rm(baseDir, { recursive: true, force: true })
42 })
43
44 it('does not expose the filesystem routes', async () => {
45 const app = await makeApp(true)
46 const browse = await app.inject({ method: 'GET', url: '/api/fs/browse?path=/' })
47 const mkdirRes = await app.inject({ method: 'POST', url: '/api/fs/mkdir', payload: { path: '/tmp', name: 'x' } })
48 expect(browse.statusCode).toBe(404)
49 expect(mkdirRes.statusCode).toBe(404)
50 await app.close()
51 })
52
53 it('keeps the filesystem routes for a local run', async () => {
54 const app = await makeApp(false)
55 const browse = await app.inject({ method: 'GET', url: `/api/fs/browse?path=${encodeURIComponent(baseDir)}` })
56 expect(browse.statusCode).toBe(200)
57 await app.close()
58 })
59
60 it('refuses to change the provider settings', async () => {
61 const app = await makeApp(true)
62 const res = await app.inject({
63 method: 'POST',
64 url: '/api/config',
65 payload: { mode: 'real', anthropicKey: 'sk-test', openaiKey: 'sk-test' },
66 })
67 expect(res.statusCode).toBe(403)
68 await expect(stat(path.join(baseDir, '.env'))).rejects.toThrow()
69 await app.close()
70 })
71
72 it('writes sessions into the sandbox and ignores the requested folder', async () => {
73 const app = await makeApp(true)
74 const res = await app.inject({
75 method: 'POST',
76 url: '/api/sessions',
77 payload: { name: 'demo', targetDir: '/etc' },
78 })
79 expect(res.statusCode).toBe(201)
80 const session = res.json() as { targetDir: string }
81 expect(session.targetDir.startsWith(sandboxRoot)).toBe(true)
82 expect((await stat(session.targetDir)).isDirectory()).toBe(true)
83 await app.close()
84 })
85
86 it('answers no cross-origin request', async () => {
87 const app = await makeApp(true)
88 const res = await app.inject({
89 method: 'GET',
90 url: '/api/health',
91 headers: { origin: 'https://attacker.example' },
92 })
93 expect(res.headers['access-control-allow-origin']).toBeUndefined()
94 await app.close()
95 })
96
97 it('locks a public instance to the offline mocks', () => {
98 const env: NodeJS.ProcessEnv = { VOICETASK_PUBLIC: '1' }
99 expect(applyDemoMode([], env)).toBe(true)
100 expect(env.MOCK_PROVIDERS).toBe('1')
101 expect(isPublicDemo(env)).toBe(true)
102 expect(isPublicDemo({})).toBe(false)
103 })
104
105 it('refuses to start a public instance without a sandbox', () => {
106 expect(() => buildApp({ publicDemo: true })).toThrow(/sandboxRoot/)
107 })
108 })
109