Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/historical-snapshot-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webgpu-search": patch
---

Add a frozen snapshot from the published 1.0.1 package to verify backward-compatible restoration, document contents, and ordered search scores in CI.
1 change: 1 addition & 0 deletions docs/IMPROVEMENT-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ There are about 300 tests, but several core modules have none of their own. This
- **Why:** AGENTS.md §2.4 requires the two paths to score the same, and nothing enforces it today.

### 2.2 Snapshot codec tests
- **Historical compatibility covered:** `scripts/test-snapshot.ts` loads the frozen `test/fixtures/snapshot-1.0.1.bin` written by the published npm package, with provenance and fixed search expectations in its JSON manifest. Existing round-trip and typed corruption/profile error checks remain in the same suite, which CI runs through `test:snapshot`.
- Add `test/snapshot-codec.test.ts`:
- Saving an index and loading it back gives identical search results.
- Bad input is rejected with the right typed error: wrong magic bytes, wrong version, truncated data, a corrupted checksum, a mismatched text profile.
Expand Down
34 changes: 34 additions & 0 deletions packages/webgpu-search/test/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Historical snapshots

`snapshot-1.0.1.bin` was written by the **published npm package
`webgpu-search@1.0.1`**, not by the working tree. Its JSON manifest records
the npm tarball URL and integrity, input documents, index options, snapshot
format and SHA-256, and ordered search results from that released package.

The fixture protects compatibility with an older writer independently of
current serialization code. The snapshot suite loads the bytes directly and
checks result IDs, scores, matched fields, and restored documents. CI already
runs it through `bun run test:snapshot`.

## Preserve the fixture

Do not regenerate or update this pair to make a failing test pass. Investigate
the compatibility regression instead. Add a separately versioned fixture when
covering another release; retain the existing ones.

To reproduce the original generation in a temporary directory:

1. Install `webgpu-search@1.0.1` with
`npm install --ignore-scripts --no-audit --no-fund --save-exact webgpu-search@1.0.1`.
2. Verify its `package-lock.json` integrity matches `producer.integrity` in
the manifest. Import `DocumentIndex` from that installation, outside this
monorepo so workspace aliases cannot select current code.
3. Pass the manifest's `documents` and `options` to `DocumentIndex.create()`.
Write `new Uint8Array(index.serialize())` to a temporary file.
4. Run each manifest query using its `mode` and `cpuScorer: 'exact'`. Compare
`totalMatches` and ordered `{ id, score, matchedField }` against `expected`.
Destroy the index when finished.

The resulting file must match `snapshotSha256`; reproduction must not overwrite
the checked-in artifact. Queries cover weighted multi-field ranking, ASCII,
German and Greek case folding, decomposed accents, CJK, emoji/ZWJ, and no hits.
Binary file not shown.
187 changes: 187 additions & 0 deletions packages/webgpu-search/test/fixtures/snapshot-1.0.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"producer": {
"package": "webgpu-search",
"version": "1.0.1",
"tarball": "https://registry.npmjs.org/webgpu-search/-/webgpu-search-1.0.1.tgz",
"integrity": "sha512-psu6FVagazLpKH1miZez3PqIYhZ2rqBLDupogh7D5nZIr6teC6RpvgYEtm0v3m3MEuV2ll/JTdzz0JVwAPmApQ=="
},
"options": {
"fields": [
{
"name": "title",
"weight": 2
},
{
"name": "content",
"weight": 1
}
],
"preferGpu": false
},
"documents": [
{
"id": "ascii-1",
"title": "WebGPU search",
"content": "Fast parallel search for documents."
},
{
"id": "ascii-2",
"title": "Search guide",
"content": "WebGPU search with a CPU fallback."
},
{
"id": "unicode",
"title": "Straße café",
"content": "İstanbul and Greek Σ σ ς."
},
{
"id": "cjk",
"title": "東京 検索",
"content": "日本語 search guide."
},
{
"id": "emoji",
"title": "Developer 👩‍💻",
"content": "Launch 🚀 with emoji."
}
],
"queries": [
{
"query": "webgpu",
"mode": "substring",
"expected": {
"totalMatches": 2,
"results": [
{
"id": "ascii-1",
"score": 1986,
"matchedField": "title"
},
{
"id": "ascii-2",
"score": 972,
"matchedField": "content"
}
]
}
},
{
"query": "search",
"mode": "fuzzy",
"expected": {
"totalMatches": 3,
"results": [
{
"id": "ascii-2",
"score": 748,
"matchedField": "title"
},
{
"id": "ascii-1",
"score": 726,
"matchedField": "title"
},
{
"id": "cjk",
"score": 359,
"matchedField": "content"
}
]
}
},
{
"query": "strasse",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "unicode",
"score": 1990,
"matchedField": "title"
}
]
}
},
{
"query": "café",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "unicode",
"score": 1824,
"matchedField": "title"
}
]
}
},
{
"query": "σ",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "unicode",
"score": 775,
"matchedField": "content"
}
]
}
},
{
"query": "東京",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "cjk",
"score": 1994,
"matchedField": "title"
}
]
}
},
{
"query": "👩‍💻",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "emoji",
"score": 1780,
"matchedField": "title"
}
]
}
},
{
"query": "🚀",
"mode": "substring",
"expected": {
"totalMatches": 1,
"results": [
{
"id": "emoji",
"score": 911,
"matchedField": "content"
}
]
}
},
{
"query": "absent-query",
"mode": "substring",
"expected": {
"totalMatches": 0,
"results": []
}
}
],
"snapshotSha256": "b34c39e7698e5b1d0054b57b6f4d07b5ca1a41661787cf8267ea1f9241297ff2",
"snapshotFormatVersion": 4
}
35 changes: 35 additions & 0 deletions scripts/test-snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import assert from 'node:assert';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import snapshotFixture from '../packages/webgpu-search/test/fixtures/snapshot-1.0.1.json';
import {
DocumentIndex,
SearchWorkerClient,
Expand Down Expand Up @@ -1571,6 +1574,38 @@ async function runSnapshotTests() {
console.log(' ✅ Profile guards + live-format semantics confirmed');
}

// Unlike live round-trips, these bytes and expectations come from the
// published 1.0.1 writer and must survive changes to the current encoder.
console.log('18. Testing the frozen webgpu-search@1.0.1 snapshot...');
{
const bytes = await readFile(new URL(
'../packages/webgpu-search/test/fixtures/snapshot-1.0.1.bin', import.meta.url
));
assert.strictEqual(createHash('sha256').update(bytes).digest('hex'), snapshotFixture.snapshotSha256);
const snapshot = Uint8Array.from(bytes).buffer;
assert.strictEqual(decodeSnapshotHeader(snapshot).formatVersion, snapshotFixture.snapshotFormatVersion);
const restored = await DocumentIndex.fromSnapshot(snapshot, { options: { preferGpu: false } });
try {
assert.strictEqual(restored.getStats().docCount, snapshotFixture.documents.length);
for (const entry of snapshotFixture.queries) {
const response = await restored.search(entry.query, {
mode: entry.mode as 'fuzzy' | 'substring',
cpuScorer: 'exact',
});
assert.deepStrictEqual({
totalMatches: response.totalMatches,
results: response.results.map(({ id, score, matchedField }) => ({ id, score, matchedField })),
}, entry.expected, `1.0.1 snapshot search changed for ${JSON.stringify(entry.query)}`);
for (const hit of response.results) {
assert.deepStrictEqual(hit.doc, snapshotFixture.documents.find(doc => doc.id === hit.id));
}
}
} finally {
restored.destroy();
}
console.log(' ✅ Published 1.0.1 snapshot compatibility confirmed');
}

console.log('\n--- All Persistence Tests Passed Successfully! ✅ ---');
}

Expand Down
Loading