返回 ViMax
server-lib.test.mjs
根目录 / web / server-lib.test.mjs
1 import {mkdtemp, mkdir, readFile, stat, writeFile} from 'node:fs/promises';
2 import os from 'node:os';
3 import path from 'node:path';
4 import {afterEach, describe, expect, it} from 'vitest';
5 import {deleteSession, listSessionArtifacts, readSessionHistory, readSessionState, resolveArtifactPath, storeWorkspaceUpload} from './server-lib.mjs';
6
7 const roots = [];
8
9 afterEach(async () => {
10 const {rm} = await import('node:fs/promises');
11 await Promise.all(roots.splice(0).map((root) => rm(root, {recursive: true, force: true})));
12 });
13
14 async function fixture() {
15 const root = await mkdtemp(path.join(os.tmpdir(), 'vimax-web-'));
16 roots.push(root);
17 await mkdir(path.join(root, '.vimax', 'logs'), {recursive: true});
18 await mkdir(path.join(root, '.working_dir', 'session-1', 'script2video', 'shots', '0'), {recursive: true});
19 await writeFile(path.join(root, '.vimax', 'sessions.json'), JSON.stringify({
20 active_session_id: 'session-1',
21 sessions: {
22 'session-1': {session_id: 'session-1', project_name: 'Ocean campaign', working_dir: '.working_dir/session-1', stage: 'rendering', updated_at: '2026-07-17T10:00:00'},
23 },
24 }));
25 return root;
26 }
27
28 describe('web bridge state', () => {
29 it('returns sanitized session records', async () => {
30 const root = await fixture();
31 const state = await readSessionState(root);
32 expect(state.activeSessionId).toBe('session-1');
33 expect(state.sessions[0]).toMatchObject({sessionId: 'session-1', projectName: 'Ocean campaign', stage: 'rendering'});
34 });
35
36 it('restores persisted turn history', async () => {
37 const root = await fixture();
38 await writeFile(path.join(root, '.vimax', 'logs', 'loop_history.jsonl'), `${JSON.stringify({
39 session_id: 'session-1', turn_id: 'turn-1', raw_user_input: 'Make a film', final_assistant_text: 'Planning is ready', status: 'completed', tool_rounds: [],
40 })}\n`);
41 const history = await readSessionHistory(root, 'session-1');
42 expect(history.map((message) => message.role)).toEqual(['user', 'assistant']);
43 });
44
45 it('hides workspace upload metadata from restored user messages', async () => {
46 const root = await fixture();
47 await writeFile(path.join(root, '.vimax', 'logs', 'loop_history.jsonl'), `${JSON.stringify({
48 session_id: 'session-1',
49 turn_id: 'turn-upload',
50 raw_user_input: 'Use this script <workspace_uploads>["uploads/script.txt"]</workspace_uploads>',
51 })}\n`);
52 const history = await readSessionHistory(root, 'session-1');
53 expect(history[0].text).toBe('Use this script');
54 });
55
56 it('does not expose successful tool result payloads in restored history', async () => {
57 const root = await fixture();
58 await writeFile(path.join(root, '.vimax', 'logs', 'loop_history.jsonl'), `${JSON.stringify({
59 session_id: 'session-1',
60 turn_id: 'turn-tool',
61 raw_user_input: 'Plan a video',
62 tool_rounds: [{tool_results: [{
63 name: 'vimax_narrative_planning',
64 ok: true,
65 content: JSON.stringify({session_id: 'session-1', working_dir: '.working_dir/session-1', generated: ['script.json']}),
66 }]}],
67 })}\n`);
68 const history = await readSessionHistory(root, 'session-1');
69 const activity = history.find((message) => message.role === 'activity');
70 expect(activity).toMatchObject({text: 'Completed', status: 'done', stage: 'completed'});
71 expect(JSON.stringify(activity)).not.toContain('working_dir');
72 });
73
74 it('lists media artifacts and blocks path traversal', async () => {
75 const root = await fixture();
76 await writeFile(path.join(root, '.working_dir', 'session-1', 'script2video', 'shots', '0', 'first_frame.png'), 'image');
77 const artifacts = await listSessionArtifacts(root, 'session-1');
78 expect(artifacts[0]).toMatchObject({kind: 'image', name: 'first_frame.png'});
79 expect(() => resolveArtifactPath(root, 'session-1', '../../secrets')).toThrow(/escapes/);
80 });
81
82 it('stores uploads inside the session without overwriting matching names', async () => {
83 const root = await fixture();
84 const first = await storeWorkspaceUpload(root, 'session-1', 'script.txt', Buffer.from('first'));
85 const second = await storeWorkspaceUpload(root, 'session-1', 'script.txt', Buffer.from('second'));
86 expect(first).toMatchObject({name: 'script.txt', path: 'uploads/script.txt', size: 5});
87 expect(second).toMatchObject({name: 'script (2).txt', path: 'uploads/script (2).txt', size: 6});
88 expect(await readFile(path.join(root, '.working_dir', 'session-1', second.path), 'utf8')).toBe('second');
89 await expect(storeWorkspaceUpload(root, 'session-1', '../escape.txt', Buffer.from('no'))).rejects.toThrow(/unsupported/);
90 });
91
92 it('deletes project state, artifacts, and matching log records', async () => {
93 const root = await fixture();
94 const logPath = path.join(root, '.vimax', 'logs', 'loop_history.jsonl');
95 await writeFile(logPath, [
96 JSON.stringify({session_id: 'session-1', raw_user_input: 'Delete me'}),
97 JSON.stringify({session_id: 'session-2', raw_user_input: 'Keep me'}),
98 '',
99 ].join('\n'));
100
101 const state = await deleteSession(root, 'session-1');
102 expect(state).toEqual({activeSessionId: '', sessions: []});
103 await expect(stat(path.join(root, '.working_dir', 'session-1'))).rejects.toThrow();
104 expect(await readFile(logPath, 'utf8')).toContain('session-2');
105 expect(await readFile(logPath, 'utf8')).not.toContain('session-1');
106 });
107 });
108
108 lines Plain Text