pi-vault-mind 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/dist/src/autosync.d.ts +16 -0
- package/dist/src/autosync.js +43 -0
- package/dist/src/commands.d.ts +18 -0
- package/dist/src/commands.js +464 -10
- package/dist/src/embed-queue.d.ts +80 -0
- package/dist/src/embed-queue.js +163 -0
- package/dist/src/index.js +9 -0
- package/dist/src/lance.d.ts +7 -0
- package/dist/src/lance.js +432 -0
- package/dist/src/modal-client.d.ts +176 -0
- package/dist/src/modal-client.js +174 -0
- package/dist/src/modal-config.d.ts +42 -0
- package/dist/src/modal-config.js +60 -0
- package/dist/src/settings-ui.d.ts +7 -0
- package/dist/src/settings-ui.js +109 -1
- package/dist/src/sync.d.ts +71 -0
- package/dist/src/sync.js +211 -0
- package/dist/src/types.d.ts +102 -1
- package/dist/test/embed-queue.test.js +105 -0
- package/dist/test/index.test.js +35 -0
- package/dist/test/lance-modal.test.js +95 -0
- package/dist/test/modal-client.test.js +294 -0
- package/dist/test/modal-config.test.js +86 -0
- package/dist/test/sync.test.js +132 -0
- package/package.json +3 -2
- package/dist/test/index.test.d.ts +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import { ModalEmbeddingClient } from "../src/modal-client.js";
|
|
4
|
+
// Minimal typed helper to build a fetch mock returning canned responses.
|
|
5
|
+
function mockFetch(responses = []) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const fetchMock = async (url, init) => {
|
|
8
|
+
const u = typeof url === "string" ? url : url.toString();
|
|
9
|
+
calls.push({ url: u, init });
|
|
10
|
+
const match = responses.find((r) => (r.match ? r.match(u, init) : true)) || responses[0];
|
|
11
|
+
if (!match)
|
|
12
|
+
throw new Error(`unexpected fetch ${u}`);
|
|
13
|
+
const status = match.status ?? 200;
|
|
14
|
+
return {
|
|
15
|
+
ok: status >= 200 && status < 300,
|
|
16
|
+
status,
|
|
17
|
+
json: async () => match.body,
|
|
18
|
+
text: async () => JSON.stringify(match.body ?? {}),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
return { fetchMock, calls };
|
|
22
|
+
}
|
|
23
|
+
const baseClient = () => new ModalEmbeddingClient({ baseUrl: "https://pvm.modal.run", apiToken: "tok" });
|
|
24
|
+
describe("ModalEmbeddingClient (mocked fetch)", () => {
|
|
25
|
+
let originalFetch;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
originalFetch = globalThis.fetch;
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
globalThis.fetch = originalFetch;
|
|
31
|
+
});
|
|
32
|
+
it("health() calls GET /health without auth body", async () => {
|
|
33
|
+
const { fetchMock, calls } = mockFetch([
|
|
34
|
+
{ match: (u) => u.endsWith("/health"), body: { ok: true, default_model: "embeddinggemma" } },
|
|
35
|
+
]);
|
|
36
|
+
globalThis.fetch = fetchMock;
|
|
37
|
+
const res = await baseClient().health();
|
|
38
|
+
assert.equal(res.ok, true);
|
|
39
|
+
assert.equal(res.default_model, "embeddinggemma");
|
|
40
|
+
assert.equal(calls[0].url, "https://pvm.modal.run/health");
|
|
41
|
+
assert.equal((calls[0].init?.headers).Authorization, "Bearer tok");
|
|
42
|
+
});
|
|
43
|
+
it("embed() posts texts with model/dim/task and returns vectors", async () => {
|
|
44
|
+
const { fetchMock, calls } = mockFetch([
|
|
45
|
+
{
|
|
46
|
+
match: (u) => u.endsWith("/embed"),
|
|
47
|
+
body: {
|
|
48
|
+
model: "embeddinggemma",
|
|
49
|
+
dim: 768,
|
|
50
|
+
vectors: [
|
|
51
|
+
[0.1, 0.2],
|
|
52
|
+
[0.3, 0.4],
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
globalThis.fetch = fetchMock;
|
|
58
|
+
const res = await baseClient().embed(["a", "b"], { model: "embeddinggemma", task: "document" });
|
|
59
|
+
assert.deepEqual(res.vectors, [
|
|
60
|
+
[0.1, 0.2],
|
|
61
|
+
[0.3, 0.4],
|
|
62
|
+
]);
|
|
63
|
+
const sent = JSON.parse(calls[0].init?.body ?? "{}");
|
|
64
|
+
assert.deepEqual(sent.texts, ["a", "b"]);
|
|
65
|
+
assert.equal(sent.task, "document");
|
|
66
|
+
assert.equal(sent.model, "embeddinggemma");
|
|
67
|
+
});
|
|
68
|
+
it("syncCollections() unwraps the collections array", async () => {
|
|
69
|
+
const { fetchMock } = mockFetch([
|
|
70
|
+
{
|
|
71
|
+
match: (u) => u.endsWith("/sync/collections"),
|
|
72
|
+
body: {
|
|
73
|
+
collections: [
|
|
74
|
+
{
|
|
75
|
+
collection: "main",
|
|
76
|
+
model: "embeddinggemma",
|
|
77
|
+
dim: 768,
|
|
78
|
+
rows: 5,
|
|
79
|
+
table: "col_main__embeddinggemma__768",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
globalThis.fetch = fetchMock;
|
|
86
|
+
const cols = await baseClient().syncCollections();
|
|
87
|
+
assert.equal(cols.length, 1);
|
|
88
|
+
assert.equal(cols[0].collection, "main");
|
|
89
|
+
assert.equal(cols[0].rows, 5);
|
|
90
|
+
});
|
|
91
|
+
it("exportAll() drains pages and returns the final watermark", async () => {
|
|
92
|
+
const pages = [
|
|
93
|
+
{
|
|
94
|
+
rows: [
|
|
95
|
+
{
|
|
96
|
+
id: "1",
|
|
97
|
+
text: "t1",
|
|
98
|
+
vector: [0.1],
|
|
99
|
+
metadata: {},
|
|
100
|
+
model: "m",
|
|
101
|
+
dim: 1,
|
|
102
|
+
seq: 100,
|
|
103
|
+
created_at: "",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
next_watermark: 100,
|
|
107
|
+
count: 1,
|
|
108
|
+
done: false,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
rows: [
|
|
112
|
+
{
|
|
113
|
+
id: "2",
|
|
114
|
+
text: "t2",
|
|
115
|
+
vector: [0.2],
|
|
116
|
+
metadata: {},
|
|
117
|
+
model: "m",
|
|
118
|
+
dim: 1,
|
|
119
|
+
seq: 200,
|
|
120
|
+
created_at: "",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
next_watermark: 200,
|
|
124
|
+
count: 1,
|
|
125
|
+
done: true,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
const calls = [];
|
|
129
|
+
// Return pages in sequence based on call order.
|
|
130
|
+
let callIdx = 0;
|
|
131
|
+
const seqMock = async (url, init) => {
|
|
132
|
+
const u = typeof url === "string" ? url : url.toString();
|
|
133
|
+
calls.push({ url: u, init });
|
|
134
|
+
const body = pages[Math.min(callIdx, pages.length - 1)];
|
|
135
|
+
callIdx++;
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
status: 200,
|
|
139
|
+
json: async () => body,
|
|
140
|
+
text: async () => "",
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
globalThis.fetch = seqMock;
|
|
144
|
+
const seen = [];
|
|
145
|
+
const wm = await baseClient().exportAll("main", (rows) => {
|
|
146
|
+
seen.push(...rows.map((r) => r.id));
|
|
147
|
+
});
|
|
148
|
+
assert.deepEqual(seen, ["1", "2"]);
|
|
149
|
+
assert.equal(wm, 200);
|
|
150
|
+
});
|
|
151
|
+
it("waitForJob() polls until done", async () => {
|
|
152
|
+
const statuses = [
|
|
153
|
+
{
|
|
154
|
+
status: "running",
|
|
155
|
+
collection: "main",
|
|
156
|
+
model: "m",
|
|
157
|
+
dim: 1,
|
|
158
|
+
total: 2,
|
|
159
|
+
processed: 1,
|
|
160
|
+
updated_at: "",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
status: "done",
|
|
164
|
+
collection: "main",
|
|
165
|
+
model: "m",
|
|
166
|
+
dim: 1,
|
|
167
|
+
total: 2,
|
|
168
|
+
processed: 2,
|
|
169
|
+
updated_at: "",
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
let i = 0;
|
|
173
|
+
globalThis.fetch = (async () => {
|
|
174
|
+
const body = statuses[Math.min(i, statuses.length - 1)];
|
|
175
|
+
i++;
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
status: 200,
|
|
179
|
+
json: async () => body,
|
|
180
|
+
text: async () => "",
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
const status = await baseClient().waitForJob("job1", 0);
|
|
184
|
+
assert.equal(status.status, "done");
|
|
185
|
+
});
|
|
186
|
+
it("throws on non-OK response", async () => {
|
|
187
|
+
globalThis.fetch = (async () => ({
|
|
188
|
+
ok: false,
|
|
189
|
+
status: 401,
|
|
190
|
+
json: async () => ({}),
|
|
191
|
+
text: async () => "unauthorized",
|
|
192
|
+
}));
|
|
193
|
+
await assert.rejects(baseClient().health(), /401/);
|
|
194
|
+
});
|
|
195
|
+
it("models() unwraps the registry with native_dim", async () => {
|
|
196
|
+
const { fetchMock } = mockFetch([
|
|
197
|
+
{
|
|
198
|
+
match: (u) => u.endsWith("/models"),
|
|
199
|
+
body: {
|
|
200
|
+
default: "embeddinggemma",
|
|
201
|
+
default_dim: null,
|
|
202
|
+
models: [
|
|
203
|
+
{
|
|
204
|
+
key: "embeddinggemma",
|
|
205
|
+
hf_id: "google/embeddinggemma-300m",
|
|
206
|
+
backend: "sentence-transformers",
|
|
207
|
+
native_dim: 768,
|
|
208
|
+
matryoshka_dims: [512, 256, 128],
|
|
209
|
+
gated: true,
|
|
210
|
+
trust_remote_code: false,
|
|
211
|
+
enabled: true,
|
|
212
|
+
notes: "",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
globalThis.fetch = fetchMock;
|
|
219
|
+
const res = await baseClient().models();
|
|
220
|
+
assert.equal(res.default, "embeddinggemma");
|
|
221
|
+
assert.equal(res.models[0].native_dim, 768);
|
|
222
|
+
assert.equal(res.models[0].backend, "sentence-transformers");
|
|
223
|
+
});
|
|
224
|
+
it("listJobs() calls GET /jobs and returns the jobs array", async () => {
|
|
225
|
+
const { fetchMock, calls } = mockFetch([
|
|
226
|
+
{
|
|
227
|
+
match: (u) => u.includes("/jobs?"),
|
|
228
|
+
body: {
|
|
229
|
+
jobs: [
|
|
230
|
+
{
|
|
231
|
+
status: "done",
|
|
232
|
+
collection: "main",
|
|
233
|
+
model: "m",
|
|
234
|
+
dim: 1,
|
|
235
|
+
total: 1,
|
|
236
|
+
processed: 1,
|
|
237
|
+
updated_at: "",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
count: 1,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
globalThis.fetch = fetchMock;
|
|
245
|
+
const res = await baseClient().listJobs(10);
|
|
246
|
+
assert.equal(res.count, 1);
|
|
247
|
+
assert.equal(res.jobs[0].status, "done");
|
|
248
|
+
assert.ok(calls[0].url.includes("limit=10"));
|
|
249
|
+
});
|
|
250
|
+
it("cancelJob() posts to /jobs/{id}/cancel", async () => {
|
|
251
|
+
const { fetchMock, calls } = mockFetch([
|
|
252
|
+
{ match: (u) => u.endsWith("/cancel"), body: { job_id: "j1", cancel_requested: true } },
|
|
253
|
+
]);
|
|
254
|
+
globalThis.fetch = fetchMock;
|
|
255
|
+
const res = await baseClient().cancelJob("j1");
|
|
256
|
+
assert.equal(res.cancel_requested, true);
|
|
257
|
+
assert.equal(calls[0].init?.method ?? "GET", "POST");
|
|
258
|
+
});
|
|
259
|
+
it("waitForJob() treats cancelled as terminal", async () => {
|
|
260
|
+
let i = 0;
|
|
261
|
+
const statuses = [
|
|
262
|
+
{
|
|
263
|
+
status: "running",
|
|
264
|
+
collection: "main",
|
|
265
|
+
model: "m",
|
|
266
|
+
dim: 1,
|
|
267
|
+
total: 2,
|
|
268
|
+
processed: 1,
|
|
269
|
+
updated_at: "",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
status: "cancelled",
|
|
273
|
+
collection: "main",
|
|
274
|
+
model: "m",
|
|
275
|
+
dim: 1,
|
|
276
|
+
total: 2,
|
|
277
|
+
processed: 1,
|
|
278
|
+
updated_at: "",
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
globalThis.fetch = (async () => {
|
|
282
|
+
const body = statuses[Math.min(i, statuses.length - 1)];
|
|
283
|
+
i++;
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
status: 200,
|
|
287
|
+
json: async () => body,
|
|
288
|
+
text: async () => "",
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const status = await baseClient().waitForJob("job1", 0);
|
|
292
|
+
assert.equal(status.status, "cancelled");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured, namespacedTableName, resolveDim, resolveModalToken, resolveModel, } from "../src/modal-config.js";
|
|
4
|
+
const wiki = (over = {}) => ({
|
|
5
|
+
dataDir: ".lancedb",
|
|
6
|
+
embedding: { provider: "modal" },
|
|
7
|
+
...over,
|
|
8
|
+
});
|
|
9
|
+
describe("modal-config", () => {
|
|
10
|
+
let origToken;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
origToken = process.env[MODAL_TOKEN_ENV];
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (origToken === undefined)
|
|
16
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
17
|
+
else
|
|
18
|
+
process.env[MODAL_TOKEN_ENV] = origToken;
|
|
19
|
+
});
|
|
20
|
+
it("resolveModalToken: env wins over config apiToken", () => {
|
|
21
|
+
process.env[MODAL_TOKEN_ENV] = "env-token";
|
|
22
|
+
const cfg = wiki({
|
|
23
|
+
embedding: { provider: "modal", modal: { baseUrl: "https://x", apiToken: "cfg-token" } },
|
|
24
|
+
});
|
|
25
|
+
assert.equal(resolveModalToken(cfg), "env-token");
|
|
26
|
+
});
|
|
27
|
+
it("resolveModalToken: config apiToken is the fallback", () => {
|
|
28
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
29
|
+
const cfg = wiki({
|
|
30
|
+
embedding: { provider: "modal", modal: { baseUrl: "https://x", apiToken: "cfg-token" } },
|
|
31
|
+
});
|
|
32
|
+
assert.equal(resolveModalToken(cfg), "cfg-token");
|
|
33
|
+
});
|
|
34
|
+
it("resolveModalToken: undefined when neither set", () => {
|
|
35
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
36
|
+
const cfg = wiki({ embedding: { provider: "modal", modal: { baseUrl: "https://x" } } });
|
|
37
|
+
assert.equal(resolveModalToken(cfg), undefined);
|
|
38
|
+
});
|
|
39
|
+
it("createModalClient: null when not configured (no baseUrl or token)", () => {
|
|
40
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
41
|
+
assert.equal(createModalClient(wiki()), null);
|
|
42
|
+
assert.equal(createModalClient(wiki({ embedding: { provider: "modal", modal: { baseUrl: "https://x" } } })), null);
|
|
43
|
+
});
|
|
44
|
+
it("createModalClient: builds a client when baseUrl + token present", () => {
|
|
45
|
+
process.env[MODAL_TOKEN_ENV] = "tok";
|
|
46
|
+
const c = createModalClient(wiki({ embedding: { provider: "modal", modal: { baseUrl: "https://x/" } } }));
|
|
47
|
+
assert.ok(c);
|
|
48
|
+
});
|
|
49
|
+
it("isModalConfigured: requires provider modal + baseUrl + token", () => {
|
|
50
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
51
|
+
assert.equal(isModalConfigured(wiki()), false);
|
|
52
|
+
process.env[MODAL_TOKEN_ENV] = "tok";
|
|
53
|
+
assert.equal(isModalConfigured(wiki({ embedding: { provider: "modal", modal: { baseUrl: "https://x" } } })), true);
|
|
54
|
+
// ollama provider is not modal-configured
|
|
55
|
+
assert.equal(isModalConfigured(wiki({ embedding: { provider: "ollama", modal: { baseUrl: "https://x" } } })), false);
|
|
56
|
+
});
|
|
57
|
+
it("resolveModel: per-collection override wins, then modal.model, default embeddinggemma", () => {
|
|
58
|
+
const cfg = wiki({
|
|
59
|
+
embedding: {
|
|
60
|
+
provider: "modal",
|
|
61
|
+
modal: { baseUrl: "https://x", model: "global-model" },
|
|
62
|
+
collectionModels: { special: { model: "special-model", dim: 128 } },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
assert.equal(resolveModel(cfg), "global-model");
|
|
66
|
+
assert.equal(resolveModel(cfg, "special"), "special-model");
|
|
67
|
+
assert.equal(resolveModel(cfg, "other"), "global-model");
|
|
68
|
+
assert.equal(resolveModel(wiki()), "embeddinggemma");
|
|
69
|
+
});
|
|
70
|
+
it("resolveDim: per-collection override wins, then modal.dim", () => {
|
|
71
|
+
const cfg = wiki({
|
|
72
|
+
embedding: {
|
|
73
|
+
provider: "modal",
|
|
74
|
+
modal: { baseUrl: "https://x", dim: 512 },
|
|
75
|
+
collectionModels: { special: { model: "m", dim: 128 } },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
assert.equal(resolveDim(cfg), 512);
|
|
79
|
+
assert.equal(resolveDim(cfg, "special"), 128);
|
|
80
|
+
assert.equal(resolveDim(wiki()), undefined);
|
|
81
|
+
});
|
|
82
|
+
it("namespacedTableName mirrors the server's ADR-3 scheme", () => {
|
|
83
|
+
assert.equal(namespacedTableName("main", "embeddinggemma", 768), "col_main__embeddinggemma__768");
|
|
84
|
+
assert.equal(namespacedTableName("ctx", "minilm-l6", 384), "col_ctx__minilm-l6__384");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { connect, resetConnection } from "../src/lance.js";
|
|
8
|
+
import { MODAL_TOKEN_ENV } from "../src/modal-config.js";
|
|
9
|
+
import { getWatermark, loadSyncState, saveSyncState, syncCollection } from "../src/sync.js";
|
|
10
|
+
const MODEL = "testmodel";
|
|
11
|
+
const DIM = 3;
|
|
12
|
+
const makeCfg = (dataDir) => ({
|
|
13
|
+
version: 2,
|
|
14
|
+
collections: {
|
|
15
|
+
main: {
|
|
16
|
+
path: "collections/main.jsonl",
|
|
17
|
+
schema: ["id", "domain", "fact", "tag"],
|
|
18
|
+
dedupField: "fact",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
injectors: [],
|
|
22
|
+
wiki: {
|
|
23
|
+
dataDir,
|
|
24
|
+
embedding: {
|
|
25
|
+
provider: "modal",
|
|
26
|
+
modal: { baseUrl: "https://pvm.modal.run", model: MODEL, dim: DIM },
|
|
27
|
+
},
|
|
28
|
+
ftsEnabled: true,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Mock fetch that serves /sync/export pages based on the `since` query param.
|
|
32
|
+
function exportPagesFetch(pages) {
|
|
33
|
+
return (async (url) => {
|
|
34
|
+
const u = typeof url === "string" ? url : url.toString();
|
|
35
|
+
const sp = new URL(u).searchParams;
|
|
36
|
+
const since = Number.parseInt(sp.get("since") || "0", 10);
|
|
37
|
+
const page = pages[since] ?? { rows: [], next: since, done: true };
|
|
38
|
+
const body = {
|
|
39
|
+
rows: page.rows,
|
|
40
|
+
next_watermark: page.next,
|
|
41
|
+
count: page.rows.length,
|
|
42
|
+
done: page.done,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 200,
|
|
47
|
+
json: async () => body,
|
|
48
|
+
text: async () => "",
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const row = (id, seq, vector) => ({
|
|
53
|
+
id,
|
|
54
|
+
text: `fact ${id}`,
|
|
55
|
+
vector,
|
|
56
|
+
metadata: { domain: "d", tag: "t" },
|
|
57
|
+
model: MODEL,
|
|
58
|
+
dim: DIM,
|
|
59
|
+
seq,
|
|
60
|
+
created_at: "",
|
|
61
|
+
});
|
|
62
|
+
describe("Modal sync engine", () => {
|
|
63
|
+
let dir;
|
|
64
|
+
let origToken;
|
|
65
|
+
let origFetch;
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
dir = path.join(tmpdir(), `pvm-sync-${randomUUID()}`);
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
origToken = process.env[MODAL_TOKEN_ENV];
|
|
70
|
+
process.env[MODAL_TOKEN_ENV] = "tok";
|
|
71
|
+
origFetch = globalThis.fetch;
|
|
72
|
+
resetConnection();
|
|
73
|
+
});
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
if (origToken === undefined)
|
|
76
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
77
|
+
else
|
|
78
|
+
process.env[MODAL_TOKEN_ENV] = origToken;
|
|
79
|
+
globalThis.fetch = origFetch;
|
|
80
|
+
resetConnection();
|
|
81
|
+
});
|
|
82
|
+
it("watermark state round-trips and defaults to 0", () => {
|
|
83
|
+
const cfg = makeCfg(dir);
|
|
84
|
+
const key = `main__${MODEL}__${DIM}`;
|
|
85
|
+
assert.equal(getWatermark(cfg, "main", MODEL, DIM), 0);
|
|
86
|
+
saveSyncState(cfg, { watermarks: { [key]: 123 } });
|
|
87
|
+
// Re-read through both the keyed accessor and the full state.
|
|
88
|
+
assert.equal(getWatermark(cfg, "main", MODEL, DIM), 123);
|
|
89
|
+
assert.equal(loadSyncState(cfg).watermarks[key], 123);
|
|
90
|
+
});
|
|
91
|
+
it("drains pages into local LanceDB and advances the watermark", async () => {
|
|
92
|
+
const cfg = makeCfg(dir);
|
|
93
|
+
globalThis.fetch = exportPagesFetch({
|
|
94
|
+
0: { rows: [row("1", 100, [1, 0, 0]), row("2", 150, [0, 1, 0])], next: 150, done: false },
|
|
95
|
+
150: { rows: [row("3", 200, [0, 0, 1])], next: 200, done: true },
|
|
96
|
+
});
|
|
97
|
+
const res = await syncCollection(cfg, "main");
|
|
98
|
+
assert.equal(res.rows, 3);
|
|
99
|
+
assert.equal(res.watermark, 200);
|
|
100
|
+
assert.equal(getWatermark(cfg, "main", MODEL, DIM), 200);
|
|
101
|
+
const conn = await connect(dir);
|
|
102
|
+
const table = await conn.openTable(`col_main__${MODEL}__${DIM}`);
|
|
103
|
+
assert.equal(await table.countRows(), 3);
|
|
104
|
+
});
|
|
105
|
+
it("re-running with no new rows is a no-op (idempotent)", async () => {
|
|
106
|
+
const cfg = makeCfg(dir);
|
|
107
|
+
globalThis.fetch = exportPagesFetch({
|
|
108
|
+
0: { rows: [row("1", 100, [1, 0, 0])], next: 100, done: false },
|
|
109
|
+
100: { rows: [row("2", 200, [0, 1, 0])], next: 200, done: true },
|
|
110
|
+
200: { rows: [], next: 200, done: true },
|
|
111
|
+
});
|
|
112
|
+
await syncCollection(cfg, "main");
|
|
113
|
+
const second = await syncCollection(cfg, "main"); // since=200 → empty, done
|
|
114
|
+
assert.equal(second.rows, 0);
|
|
115
|
+
assert.equal(second.watermark, 200);
|
|
116
|
+
const conn = await connect(dir);
|
|
117
|
+
const table = await conn.openTable(`col_main__${MODEL}__${DIM}`);
|
|
118
|
+
assert.equal(await table.countRows(), 2);
|
|
119
|
+
});
|
|
120
|
+
it("re-syncing the same ids merges (no duplicates)", async () => {
|
|
121
|
+
const cfg = makeCfg(dir);
|
|
122
|
+
globalThis.fetch = exportPagesFetch({
|
|
123
|
+
0: { rows: [row("1", 100, [1, 0, 0])], next: 100, done: false },
|
|
124
|
+
100: { rows: [row("1", 200, [1, 0, 0])], next: 200, done: true },
|
|
125
|
+
});
|
|
126
|
+
await syncCollection(cfg, "main"); // inserts id=1
|
|
127
|
+
await syncCollection(cfg, "main", { full: true }); // re-fetches id=1 → merge by id
|
|
128
|
+
const conn = await connect(dir);
|
|
129
|
+
const table = await conn.openTable(`col_main__${MODEL}__${DIM}`);
|
|
130
|
+
assert.equal(await table.countRows(), 1);
|
|
131
|
+
});
|
|
132
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"check": "biome check --write . && tsc --noEmit",
|
|
79
79
|
"build": "tsc && tsc -p tsconfig.test.json",
|
|
80
80
|
"dev": "tsc --watch",
|
|
81
|
-
"test": "node --test dist/test
|
|
81
|
+
"test": "node --test dist/test/*.test.js",
|
|
82
|
+
"e2e:modal": "node scripts/modal-e2e-smoke.mjs"
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|