File size: 6,759 Bytes
97ee7cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { expect, test, type Page } from '@playwright/test';

let sharedPage: Page;

test.describe('RAG vector store (worker-side)', () => {
  test.describe.configure({ mode: 'serial' });

  test.beforeAll(async ({ browser }) => {
    sharedPage = await browser.newPage();
    await sharedPage.goto('/tests/runtime-harness.html');
    const supported = await sharedPage.evaluate(async () => {
      const { initI18n } = await import('/src/services/i18n.ts');
      await initI18n();
      const { mlWorker } = await import('/src/services/ml-worker.ts');
      const ok = await mlWorker.init();
      if (!ok) return false;
      await mlWorker.loadModel('embeddings');
      return true;
    });
    if (!supported) test.skip(true, 'ML worker not supported');
  });

  test.afterAll(async () => {
    await sharedPage?.close();
  });

  async function clearVectorDB() {
    await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');
      await mlWorker.vectorStoreReset();
    });
  }

  test('ingest → count → search round-trip', async () => {
    await clearVectorDB();
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');

      const items = [
        { text: 'Iran sanctions debate intensifies in Washington', pubDate: Date.now() - 86400000, source: 'Reuters', url: 'https://example.com/1' },
        { text: 'Ukraine frontline positions shift near Bakhmut', pubDate: Date.now() - 172800000, source: 'AP', url: 'https://example.com/2' },
        { text: 'China trade talks resume with EU delegation', pubDate: Date.now() - 259200000, source: 'BBC', url: 'https://example.com/3' },
      ];

      const stored = await mlWorker.vectorStoreIngest(items);
      const count = await mlWorker.vectorStoreCount();
      const results = await mlWorker.vectorStoreSearch(['Iran sanctions policy'], 5, 0.3);

      return { stored, count, results, topText: results[0]?.text ?? '' };
    });

    expect(result.stored).toBe(3);
    expect(result.count).toBe(3);
    expect(result.results.length).toBeGreaterThan(0);
    expect(result.topText).toContain('Iran');
    expect(result.results[0]!.score).toBeGreaterThanOrEqual(0.3);
  });

  test('minScore filtering excludes dissimilar results', async () => {
    await clearVectorDB();
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');

      await mlWorker.vectorStoreIngest([
        { text: 'Weather forecast sunny skies tomorrow morning', pubDate: Date.now(), source: 'Weather', url: '' },
      ]);

      const results = await mlWorker.vectorStoreSearch(['Iran nuclear weapons program sanctions'], 5, 0.8);
      return { count: results.length };
    });

    expect(result.count).toBe(0);
  });

  test('search returns empty when embeddings model not loaded', async () => {
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');
      await mlWorker.unloadModel('embeddings');
      const results = await mlWorker.vectorStoreSearch(['test query'], 5, 0.3);
      // Reload embeddings for subsequent tests
      await mlWorker.loadModel('embeddings');
      return { count: results.length };
    });

    expect(result.count).toBe(0);
  });

  test('deduplicates across multi-query matches keeping max score', async () => {
    await clearVectorDB();
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');

      await mlWorker.vectorStoreIngest([
        { text: 'Military operations expand in eastern regions', pubDate: Date.now(), source: 'Reuters', url: 'https://example.com/1' },
      ]);

      const results = await mlWorker.vectorStoreSearch(
        ['military operations', 'eastern military expansion'],
        5,
        0.2,
      );

      return { count: results.length };
    });

    expect(result.count).toBe(1);
  });

  test('handles empty URL in items', async () => {
    await clearVectorDB();
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');

      const stored = await mlWorker.vectorStoreIngest([
        { text: 'Headline without a URL', pubDate: Date.now(), source: 'Test', url: '' },
        { text: 'Another headline no URL', pubDate: Date.now(), source: 'Test', url: '' },
      ]);

      const count = await mlWorker.vectorStoreCount();
      return { stored, count };
    });

    expect(result.stored).toBe(2);
    expect(result.count).toBe(2);
  });

  test('worker-unavailable path degrades gracefully', async () => {
    const result = await sharedPage.evaluate(async () => {
      const mod = await import('/src/services/ml-worker.ts');
      const { mlWorker } = mod;

      const fresh = Object.create(Object.getPrototypeOf(mlWorker));
      Object.assign(fresh, { worker: null, isReady: false, pendingRequests: new Map(), loadedModels: new Set(), capabilities: null });
      const ingestResult = await fresh.vectorStoreIngest([
        { text: 'test', pubDate: Date.now(), source: 'Test', url: '' },
      ]);
      const searchResult = await fresh.vectorStoreSearch(['test'], 5, 0.3);
      const countResult = await fresh.vectorStoreCount();
      return { stored: ingestResult, searchCount: searchResult.length, count: countResult };
    });

    expect(result.stored).toBe(0);
    expect(result.searchCount).toBe(0);
    expect(result.count).toBe(0);
  });

  test('queue resilience after IDB error', async () => {
    await clearVectorDB();
    const result = await sharedPage.evaluate(async () => {
      const { mlWorker } = await import('/src/services/ml-worker.ts');

      await mlWorker.vectorStoreIngest([
        { text: 'Valid headline about economic policy', pubDate: Date.now(), source: 'Reuters', url: 'https://example.com/1' },
      ]);
      const countBefore = await mlWorker.vectorStoreCount();

      indexedDB.deleteDatabase('worldmonitor_vector_store');

      try {
        await mlWorker.vectorStoreIngest([
          { text: 'Headline during IDB disruption', pubDate: Date.now(), source: 'Test', url: '' },
        ]);
      } catch {
        // Expected — IDB handle was invalidated
      }

      await mlWorker.vectorStoreIngest([
        { text: 'Recovery headline after IDB reset', pubDate: Date.now(), source: 'AP', url: 'https://example.com/3' },
      ]);
      const countAfter = await mlWorker.vectorStoreCount();

      return { countBefore, countAfter, recovered: countAfter > 0 };
    });

    expect(result.countBefore).toBe(1);
    expect(result.recovered).toBe(true);
  });
});