Skip to content

Commit 9bc5d89

Browse files
committed
Add end-to-end tests for sqlite-memory
1 parent 3a4514b commit 9bc5d89

6 files changed

Lines changed: 354 additions & 3 deletions

File tree

.github/workflows/main.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ jobs:
3030
run: npx playwright install --with-deps && npx playwright install msedge && npx playwright install chrome
3131

3232
- name: build, test and release sqlite-wasm
33-
run: cd sqlite-wasm && npm run deploy
33+
run: cd sqlite-wasm && npm run deploy
34+
35+
- name: e2e sqlite-memory
36+
env:
37+
APIKEY: ${{ secrets.APIKEY }}
38+
run: cd sqlite-wasm && node test/e2e.cjs

.github/workflows/manual.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ jobs:
7878
checkout_branch modules/sqlite-memory "$SQLITE_MEMORY_BRANCH"
7979
8080
- name: build sqlite-wasm
81-
run: cd sqlite-wasm && npm run build && npm pack
81+
run: cd sqlite-wasm && npm run build && npm i && npm pack
82+
83+
- name: e2e sqlite-memory
84+
env:
85+
APIKEY: ${{ secrets.APIKEY }}
86+
run: cd sqlite-wasm && node test/e2e.cjs
8287

8388
- name: resolve build artifact
8489
id: dist_artifact

sqlite-wasm/test/e2e.cjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { chromium } = require('playwright');
2+
const { spawn } = require('child_process');
3+
4+
const apikey = process.env.APIKEY;
5+
if (!apikey) {
6+
console.error('E2E FAILED: APIKEY environment variable not set');
7+
process.exit(1);
8+
}
9+
10+
const PORT = 3572;
11+
12+
(async () => {
13+
// Start http-server with COOP/COEP headers for SharedArrayBuffer support
14+
const server = spawn(
15+
'npx',
16+
[
17+
'http-server',
18+
'-p',
19+
String(PORT),
20+
'-H',
21+
'Cross-Origin-Opener-Policy: same-origin',
22+
'-H',
23+
'Cross-Origin-Embedder-Policy: require-corp',
24+
'-c-1',
25+
],
26+
{ stdio: 'ignore', detached: true },
27+
);
28+
29+
await new Promise((r) => setTimeout(r, 3000));
30+
31+
try {
32+
const browser = await chromium.launch();
33+
const context = await browser.newContext();
34+
const page = await context.newPage();
35+
36+
const url = `http://127.0.0.1:${PORT}/test/e2e.html?apikey=${encodeURIComponent(apikey)}`;
37+
await page.goto(url);
38+
39+
// Wait for results (network calls can be slow)
40+
await page.waitForFunction(
41+
() => document.body.innerText.includes('E2E Results'),
42+
{ timeout: 120000 },
43+
);
44+
45+
const output = await page.evaluate(() => document.body.innerText);
46+
console.log(output);
47+
48+
await browser.close();
49+
50+
if (output.includes('E2E FAILED') || !output.includes('0 failed')) {
51+
process.exit(1);
52+
}
53+
console.log('✅ sqlite-memory WASM e2e tests passed');
54+
} finally {
55+
process.kill(-server.pid);
56+
}
57+
})();

sqlite-wasm/test/e2e.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!doctype html>
2+
<html lang="en-us">
3+
<body>
4+
<script>
5+
(function () {
6+
const params = new URLSearchParams(window.location.search);
7+
const apikey = params.get('apikey') || '';
8+
9+
const logHtml = function (cssClass, ...args) {
10+
const ln = document.createElement('div');
11+
if (cssClass) ln.classList.add(cssClass);
12+
ln.append(document.createTextNode(args.join(' ')));
13+
document.body.append(ln);
14+
};
15+
const w = new Worker(
16+
'e2e.js?sqlite3.dir=../sqlite-wasm/jswasm&apikey=' +
17+
encodeURIComponent(apikey),
18+
);
19+
w.onmessage = function ({ data }) {
20+
switch (data.type) {
21+
case 'log':
22+
logHtml(data.payload.cssClass, ...data.payload.args);
23+
break;
24+
default:
25+
logHtml('error', 'Unhandled message:', data.type);
26+
}
27+
};
28+
})();
29+
</script>
30+
</body>
31+
</html>

sqlite-wasm/test/e2e.js

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
'use strict';
2+
(function () {
3+
const logHtml = function (cssClass, ...args) {
4+
postMessage({
5+
type: 'log',
6+
payload: { cssClass, args },
7+
});
8+
};
9+
const log = (...args) => logHtml('', ...args);
10+
const error = (...args) => logHtml('error', ...args);
11+
12+
const EMBED_TEST_TEXT =
13+
'The quick brown fox jumps over the lazy dog. This is a test of the remote embedding API.';
14+
const EXPECTED_DIMENSION = 768;
15+
const EXPECTED_EMBEDDING = [0.05142519, 0.01374194, -0.02152035, 0.0277442];
16+
const EMBEDDING_TOLERANCE = 0.001;
17+
18+
const runE2E = function (sqlite3, apikey) {
19+
const db = new sqlite3.oo1.DB('/e2e.sqlite3', 'ct');
20+
let passed = 0;
21+
let failed = 0;
22+
23+
function run(name, fn) {
24+
try {
25+
const detail = fn();
26+
passed++;
27+
log(` ${name}... PASSED${detail ? ' ' + detail : ''}`);
28+
} catch (e) {
29+
failed++;
30+
log(` ${name}... FAILED: ${e.message}`);
31+
}
32+
}
33+
34+
function exec(sql) {
35+
return db.exec({ sql, rowMode: 'array', returnValue: 'resultRows' });
36+
}
37+
38+
function execScalar(sql) {
39+
const rows = exec(sql);
40+
return rows.length > 0 ? rows[0][0] : null;
41+
}
42+
43+
log('E2E tests (sqlite-memory WASM networking):');
44+
45+
// Phase 1: Setup
46+
47+
run('memory_version', () => {
48+
const v = execScalar("SELECT memory_version();");
49+
if (!v || v.length === 0) throw new Error('empty version');
50+
return `(v${v})`;
51+
});
52+
53+
run('vector_version', () => {
54+
const v = execScalar("SELECT vector_version();");
55+
if (!v || v.length === 0) throw new Error('empty version');
56+
return `(v${v})`;
57+
});
58+
59+
run('memory_set_apikey', () => {
60+
execScalar(`SELECT memory_set_apikey('${apikey}');`);
61+
});
62+
63+
run('memory_set_model', () => {
64+
execScalar("SELECT memory_set_model('llama', 'embeddinggemma-300m');");
65+
});
66+
67+
// Phase 2: Configuration
68+
69+
run('memory_set_get_option', () => {
70+
execScalar("SELECT memory_set_option('max_tokens', 512);");
71+
const v = execScalar("SELECT memory_get_option('max_tokens');");
72+
if (Number(v) !== 512) throw new Error(`expected 512, got ${v}`);
73+
74+
const provider = execScalar("SELECT memory_get_option('provider');");
75+
if (provider !== 'llama') throw new Error(`expected llama, got ${provider}`);
76+
77+
const model = execScalar("SELECT memory_get_option('model');");
78+
if (model !== 'embeddinggemma-300m')
79+
throw new Error(`expected embeddinggemma-300m, got ${model}`);
80+
81+
execScalar("SELECT memory_set_option('max_tokens', 400);");
82+
});
83+
84+
// Phase 3: Content Management (network calls)
85+
86+
run('memory_add_text', () => {
87+
execScalar(`SELECT memory_add_text('${EMBED_TEST_TEXT}');`);
88+
const count = execScalar('SELECT COUNT(*) FROM dbmem_content;');
89+
if (count !== 1) throw new Error(`expected 1 content, got ${count}`);
90+
const chunks = execScalar('SELECT COUNT(*) FROM dbmem_vault;');
91+
if (chunks !== 1) throw new Error(`expected 1 chunk, got ${chunks}`);
92+
});
93+
94+
run('verify_embedding', () => {
95+
const rows = db.exec({
96+
sql: 'SELECT embedding FROM dbmem_vault LIMIT 1;',
97+
rowMode: 'array',
98+
returnValue: 'resultRows',
99+
});
100+
if (rows.length === 0) throw new Error('no embedding found');
101+
const blob = rows[0][0];
102+
const floats = new Float32Array(
103+
blob.buffer,
104+
blob.byteOffset,
105+
blob.byteLength / 4,
106+
);
107+
if (floats.length !== EXPECTED_DIMENSION)
108+
throw new Error(
109+
`expected dim=${EXPECTED_DIMENSION}, got ${floats.length}`,
110+
);
111+
for (let i = 0; i < EXPECTED_EMBEDDING.length; i++) {
112+
const diff = Math.abs(floats[i] - EXPECTED_EMBEDDING[i]);
113+
if (diff > EMBEDDING_TOLERANCE)
114+
throw new Error(
115+
`embedding[${i}]=${floats[i]}, expected ${EXPECTED_EMBEDDING[i]}`,
116+
);
117+
}
118+
return `(dim=${floats.length}, values verified)`;
119+
});
120+
121+
run('memory_add_text_context', () => {
122+
execScalar(
123+
"SELECT memory_add_text('SQLite is a C-language library that implements a small, fast, self-contained SQL database engine.', 'test-context');",
124+
);
125+
const ctx = execScalar(
126+
"SELECT context FROM dbmem_content WHERE context IS NOT NULL LIMIT 1;",
127+
);
128+
if (ctx !== 'test-context')
129+
throw new Error(`expected test-context, got ${ctx}`);
130+
const count = execScalar('SELECT COUNT(*) FROM dbmem_content;');
131+
if (count !== 2) throw new Error(`expected 2 content, got ${count}`);
132+
});
133+
134+
run('memory_add_text_idempotent', () => {
135+
const before = execScalar('SELECT COUNT(*) FROM dbmem_vault;');
136+
execScalar(`SELECT memory_add_text('${EMBED_TEST_TEXT}');`);
137+
const after = execScalar('SELECT COUNT(*) FROM dbmem_vault;');
138+
if (after !== before)
139+
throw new Error(`expected ${before} chunks, got ${after}`);
140+
});
141+
142+
// Phase 4: Search (network calls)
143+
144+
run('memory_search', () => {
145+
const rows = db.exec({
146+
sql: "SELECT hash, path, context, snippet, ranking FROM memory_search('fox', 5);",
147+
rowMode: 'array',
148+
returnValue: 'resultRows',
149+
});
150+
if (rows.length === 0) throw new Error('no search results');
151+
const [hash, path, , snippet, ranking] = rows[0];
152+
if (!hash) throw new Error('hash is empty');
153+
if (!path || path.length === 0) throw new Error('path is empty');
154+
if (!snippet || snippet.length === 0) throw new Error('snippet is empty');
155+
if (ranking <= 0 || ranking > 1)
156+
throw new Error(`ranking out of bounds: ${ranking}`);
157+
if (!snippet.includes('fox'))
158+
throw new Error('snippet does not contain fox');
159+
return `(ranking=${ranking.toFixed(4)})`;
160+
});
161+
162+
run('memory_search_ranking', () => {
163+
execScalar("SELECT memory_set_option('min_score', 0.0);");
164+
const rows = db.exec({
165+
sql: "SELECT ranking FROM memory_search('SQL database engine', 10);",
166+
rowMode: 'array',
167+
returnValue: 'resultRows',
168+
});
169+
if (rows.length === 0) throw new Error('no results');
170+
for (const [ranking] of rows) {
171+
if (ranking <= 0 || ranking > 1)
172+
throw new Error(`ranking out of bounds: ${ranking}`);
173+
}
174+
execScalar("SELECT memory_set_option('min_score', 0.7);");
175+
return `(${rows.length} results)`;
176+
});
177+
178+
// Phase 5: Deletion
179+
180+
run('memory_delete', () => {
181+
const hash = execScalar(
182+
'SELECT hash FROM dbmem_content WHERE context IS NULL LIMIT 1;',
183+
);
184+
const before = execScalar('SELECT COUNT(*) FROM dbmem_content;');
185+
execScalar(`SELECT memory_delete(${hash});`);
186+
const after = execScalar('SELECT COUNT(*) FROM dbmem_content;');
187+
if (after !== before - 1)
188+
throw new Error(`expected ${before - 1}, got ${after}`);
189+
});
190+
191+
run('memory_delete_context', () => {
192+
execScalar("SELECT memory_delete_context('test-context');");
193+
const count = execScalar(
194+
"SELECT COUNT(*) FROM dbmem_content WHERE context = 'test-context';",
195+
);
196+
if (count !== 0) throw new Error(`expected 0, got ${count}`);
197+
});
198+
199+
run('memory_cache_clear_model', () => {
200+
execScalar(
201+
"SELECT memory_cache_clear('llama', 'embeddinggemma-300m');",
202+
);
203+
});
204+
205+
run('memory_cache_clear', () => {
206+
execScalar('SELECT memory_cache_clear();');
207+
});
208+
209+
run('memory_clear', () => {
210+
execScalar('SELECT memory_clear();');
211+
const content = execScalar('SELECT COUNT(*) FROM dbmem_content;');
212+
if (content !== 0)
213+
throw new Error(`expected 0 content, got ${content}`);
214+
const vault = execScalar('SELECT COUNT(*) FROM dbmem_vault;');
215+
if (vault !== 0) throw new Error(`expected 0 vault, got ${vault}`);
216+
});
217+
218+
db.close();
219+
log(`\n=== E2E Results: ${passed} passed, ${failed} failed ===`);
220+
return failed === 0;
221+
};
222+
223+
if (globalThis.window !== globalThis) {
224+
let sqlite3Js = 'sqlite3.js';
225+
const urlParams = new URL(globalThis.location.href).searchParams;
226+
if (urlParams.has('sqlite3.dir')) {
227+
sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
228+
}
229+
importScripts(sqlite3Js);
230+
}
231+
const urlParams = new URL(globalThis.location.href).searchParams;
232+
const apikey = urlParams.get('apikey') || '';
233+
234+
globalThis
235+
.sqlite3InitModule({
236+
print: log,
237+
printErr: console.error,
238+
})
239+
.then(function (sqlite3) {
240+
try {
241+
if (!apikey) {
242+
error('E2E FAILED: apikey not provided');
243+
return;
244+
}
245+
const success = runE2E(sqlite3, apikey);
246+
if (!success) {
247+
error('E2E FAILED');
248+
}
249+
} catch (e) {
250+
error('Exception:', e.message);
251+
}
252+
});
253+
})();

wasm.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ int dbmem_remote_compute_embedding (dbmem_remote_engine_t *engine, const char *t
375375
emscripten_fetch_attr_t attr;
376376
emscripten_fetch_attr_init(&attr);
377377
strcpy(attr.requestMethod, "POST");
378-
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS;
378+
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS | EMSCRIPTEN_FETCH_REPLACE;
379379

380380
const char *headers[] = {
381381
"Authorization", auth_header,

0 commit comments

Comments
 (0)