File size: 4,552 Bytes
ea7122c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { Worker } from 'node:worker_threads';
import test from 'node:test';

test('actual worker module initializes and serves shell/editor/export RPCs without loading a model', { timeout: 10_000 }, async t => {
  // Supply only browser worker messaging globals. The app's real module graph,
  // real shell, actual runtime imports, and RPC entrypoint execute unchanged.
  const url = new URL('../src/agent.worker.js', import.meta.url).href;
  const worker = new Worker(`
    const { parentPort } = require('node:worker_threads');
    globalThis.self = globalThis;
    globalThis.postMessage = message => parentPort.postMessage(message);
    parentPort.on('message', data => globalThis.onmessage({ data }));
    import(${JSON.stringify(url)}).catch(error => { throw error; });
  `, { eval: true });
  t.after(() => worker.terminate());
  const events = [];
  worker.on('message', message => events.push(message));
  let initialized;
  while (!initialized) {
    const [event] = await once(worker, 'message');
    if (event.type === 'fatal') throw new Error(event.error);
    if (event.type === 'initialized') initialized = event;
  }
  assert.match(initialized.files['/workspace/sales.csv'], /Notebook,12/);
  assert.deepEqual(initialized.messages, []);
  assert(events.some(event => event.type === 'persistence_error'));
  let id = 0;
  async function rpc(action, args) {
    const requestId = ++id;
    worker.postMessage({ id: requestId, action, args });
    for (;;) {
      const [event] = await once(worker, 'message');
      if (event.type === 'reply' && event.id === requestId) {
        if (event.error) throw Object.assign(new Error(event.error), { code: event.errorCode });
        return event.value;
      }
    }
  }
  assert.equal(await rpc('cache_status'), false);
  const shell = await rpc('shell', { command: 'printf "verified\\n" > /workspace/worker-check.txt; cat /workspace/worker-check.txt' });
  assert.equal(shell.exitCode, 0);
  assert.equal(shell.stdout, 'verified\n');
  await rpc('write', { path: 'worker-check.txt', content: 'edited' });
  assert.equal(await rpc('read', { path: 'worker-check.txt' }), 'edited');
  const exported = await rpc('snapshot');
  assert.equal(exported.runtime, 'minicpm5-gguf');
  assert.equal(exported.files['/workspace/worker-check.txt'], 'edited');
  assert(exported.entries.some(([path]) => path === '/workspace/worker-check.txt'));
  const fresh = await rpc('new_chat');
  assert.deepEqual(fresh.messages, []);
  assert.equal(fresh.files['/workspace/worker-check.txt'], 'edited');
  await assert.rejects(rpc('prompt', { text: 'hello' }), /Load the model first/);
  await assert.rejects(rpc('load', { cachedOnly: true }), error => error.code === 'MODEL_CACHE_MISS');
  assert.equal(await rpc('stop'), true);
  assert(events.some(event => event.type === 'workspace'));
  assert(events.some(event => event.type === 'idle'));
});

test('a failed worker initialization identifies the failure stage and rejects subsequent RPCs', { timeout: 10_000 }, async t => {
  const url = new URL('../src/agent.worker.js', import.meta.url).href;
  const worker = new Worker(`
    const { parentPort } = require('node:worker_threads');
    globalThis.self = globalThis;
    globalThis.postMessage = message => parentPort.postMessage(message);
    parentPort.on('message', data => globalThis.onmessage({ data }));
    // Simulate an unreadable workspace rather than a model or storage outage.
    // Loading malformed saved entries fails after IndexedDB has opened.
    globalThis.indexedDB = { open() {
      const db = { transaction() { return { objectStore() { return { get() {
        const request = { result: { entries: [['/workspace/file', { type: 'file', base64: 'invalid!' }]] } };
        queueMicrotask(() => request.onsuccess());
        return request;
      } }; } }; } };
      const request = { result: db };
      queueMicrotask(() => request.onsuccess());
      return request;
    } };
    import(${JSON.stringify(url)}).catch(error => { throw error; });
  `, { eval: true });
  t.after(() => worker.terminate());
  const [fatal] = await once(worker, 'message');
  assert.equal(fatal.type, 'fatal');
  assert.equal(fatal.stage, 'initialization');
  assert.match(fatal.error, /Reload the page/);
  worker.postMessage({ id: 1, action: 'read', args: { path: 'README.md' } });
  const [reply] = await once(worker, 'message');
  assert.equal(reply.type, 'reply');
  assert.equal(reply.id, 1);
  assert(reply.error);
});