@indigoai-us/hq-cloud 6.18.14-alpha.2 → 6.18.14
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/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js +379 -0
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +76 -50
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.test.js +2 -199
- package/dist/bin/sync-runner-watch-loop.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +1 -12
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -32
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +4 -4
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +22 -18
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +32 -44
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +10 -0
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +28 -1
- package/dist/object-io.test.js.map +1 -1
- package/dist/qmd-reindex.d.ts +10 -30
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +41 -148
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.js +77 -110
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +10 -14
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +40 -71
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +1 -84
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync-core.d.ts +7 -0
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +12 -0
- package/dist/sync-core.js.map +1 -1
- package/package.json +1 -1
- package/dist/sync/targeted-pull-prefixes.d.ts +0 -9
- package/dist/sync/targeted-pull-prefixes.d.ts.map +0 -1
- package/dist/sync/targeted-pull-prefixes.js +0 -43
- package/dist/sync/targeted-pull-prefixes.js.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.d.ts +0 -2
- package/dist/sync/targeted-pull-prefixes.test.d.ts.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.js +0 -33
- package/dist/sync/targeted-pull-prefixes.test.js.map +0 -1
package/dist/qmd-reindex.test.js
CHANGED
|
@@ -14,7 +14,7 @@ import { describe, it, expect } from "vitest";
|
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as os from "os";
|
|
16
16
|
import * as path from "path";
|
|
17
|
-
import { reindexAfterSync,
|
|
17
|
+
import { reindexAfterSync, looksCorrupt, resolveExecTimeoutMs, DEFAULT_QMD_EXEC_TIMEOUT_MS, parseIndexDirFromStatus, parseCollectionPathFromShow, pathsEquivalent, repairPathDriftEnabled, acquireDualReindexLocks, } from "./qmd-reindex.js";
|
|
18
18
|
/** A lock acquirer that always succeeds — keeps command-sequence tests pure. */
|
|
19
19
|
const noLock = () => ({ release() { } });
|
|
20
20
|
/** Identity realpath so path-drift comparisons stay deterministic in fakes. */
|
|
@@ -22,7 +22,7 @@ const identityRealpath = (p) => path.resolve(p);
|
|
|
22
22
|
/** Build a fake exec that returns status 0 for everything and logs calls. */
|
|
23
23
|
function fakeExec(opts) {
|
|
24
24
|
const calls = [];
|
|
25
|
-
const exec =
|
|
25
|
+
const exec = (args) => {
|
|
26
26
|
calls.push(args);
|
|
27
27
|
const override = opts?.resultFor?.(args);
|
|
28
28
|
if (override)
|
|
@@ -48,9 +48,9 @@ function fakeExec(opts) {
|
|
|
48
48
|
return { exec, calls };
|
|
49
49
|
}
|
|
50
50
|
describe("reindexAfterSync", () => {
|
|
51
|
-
it("no-ops when the path is not an HQ root",
|
|
51
|
+
it("no-ops when the path is not an HQ root", () => {
|
|
52
52
|
const { exec, calls } = fakeExec();
|
|
53
|
-
const r =
|
|
53
|
+
const r = reindexAfterSync("/tmp/not-hq", {
|
|
54
54
|
exec,
|
|
55
55
|
acquireReindexLock: noLock,
|
|
56
56
|
existsSync: () => false, // core/core.yaml absent
|
|
@@ -58,9 +58,9 @@ describe("reindexAfterSync", () => {
|
|
|
58
58
|
expect(r.qmdAvailable).toBe(false);
|
|
59
59
|
expect(calls).toHaveLength(0);
|
|
60
60
|
});
|
|
61
|
-
it("no-ops when qmd is unavailable (collection list errors)",
|
|
61
|
+
it("no-ops when qmd is unavailable (collection list errors)", () => {
|
|
62
62
|
const { exec, calls } = fakeExec({ failOn: "collection" });
|
|
63
|
-
const r =
|
|
63
|
+
const r = reindexAfterSync("/hq", {
|
|
64
64
|
exec,
|
|
65
65
|
acquireReindexLock: noLock,
|
|
66
66
|
existsSync: () => true, // core.yaml present
|
|
@@ -69,9 +69,9 @@ describe("reindexAfterSync", () => {
|
|
|
69
69
|
expect(r.updated).toBe(false);
|
|
70
70
|
expect(calls).toEqual([["status"], ["collection", "list"]]);
|
|
71
71
|
});
|
|
72
|
-
it("registers a missing company collection then runs an incremental update",
|
|
72
|
+
it("registers a missing company collection then runs an incremental update", () => {
|
|
73
73
|
const { exec, calls } = fakeExec({ listStdout: "Collections (1):\n\nHQ (qmd://HQ/)\n" });
|
|
74
|
-
const r =
|
|
74
|
+
const r = reindexAfterSync("/hq", {
|
|
75
75
|
exec,
|
|
76
76
|
acquireReindexLock: noLock,
|
|
77
77
|
existsSync: () => true,
|
|
@@ -90,9 +90,9 @@ describe("reindexAfterSync", () => {
|
|
|
90
90
|
["update"],
|
|
91
91
|
]);
|
|
92
92
|
});
|
|
93
|
-
it("skips collections that already exist at the desired path",
|
|
93
|
+
it("skips collections that already exist at the desired path", () => {
|
|
94
94
|
const { exec, calls } = fakeExec({ listStdout: "acme (qmd://acme/)\n" });
|
|
95
|
-
const r =
|
|
95
|
+
const r = reindexAfterSync("/hq", {
|
|
96
96
|
exec,
|
|
97
97
|
acquireReindexLock: noLock,
|
|
98
98
|
existsSync: () => true,
|
|
@@ -109,7 +109,7 @@ describe("reindexAfterSync", () => {
|
|
|
109
109
|
["update"],
|
|
110
110
|
]);
|
|
111
111
|
});
|
|
112
|
-
it("detects path drift without repairing when HQ_QMD_REPAIR_PATH_DRIFT is off",
|
|
112
|
+
it("detects path drift without repairing when HQ_QMD_REPAIR_PATH_DRIFT is off", () => {
|
|
113
113
|
const drifted = path.join("/mirror", "knowledge-acme");
|
|
114
114
|
const { exec, calls } = fakeExec({
|
|
115
115
|
listStdout: "acme (qmd://acme/)\n",
|
|
@@ -120,7 +120,7 @@ describe("reindexAfterSync", () => {
|
|
|
120
120
|
}
|
|
121
121
|
: undefined,
|
|
122
122
|
});
|
|
123
|
-
const r =
|
|
123
|
+
const r = reindexAfterSync("/hq", {
|
|
124
124
|
exec,
|
|
125
125
|
acquireReindexLock: noLock,
|
|
126
126
|
existsSync: () => true,
|
|
@@ -136,7 +136,7 @@ describe("reindexAfterSync", () => {
|
|
|
136
136
|
// Detect-only must still allow embed (no repair this cycle).
|
|
137
137
|
expect(r.embedded).toBe(true);
|
|
138
138
|
});
|
|
139
|
-
it("repairs path drift when HQ_QMD_REPAIR_PATH_DRIFT=1 and skips embed",
|
|
139
|
+
it("repairs path drift when HQ_QMD_REPAIR_PATH_DRIFT=1 and skips embed", () => {
|
|
140
140
|
const drifted = path.join("/mirror", "knowledge-acme");
|
|
141
141
|
const desired = path.join("/hq", "companies", "acme", "knowledge");
|
|
142
142
|
const { exec, calls } = fakeExec({
|
|
@@ -145,7 +145,7 @@ describe("reindexAfterSync", () => {
|
|
|
145
145
|
? { status: 0, stdout: `Collection: acme\n Path: ${drifted}\n` }
|
|
146
146
|
: undefined,
|
|
147
147
|
});
|
|
148
|
-
const r =
|
|
148
|
+
const r = reindexAfterSync("/hq", {
|
|
149
149
|
exec,
|
|
150
150
|
acquireReindexLock: noLock,
|
|
151
151
|
existsSync: () => true,
|
|
@@ -168,7 +168,7 @@ describe("reindexAfterSync", () => {
|
|
|
168
168
|
["update"],
|
|
169
169
|
]);
|
|
170
170
|
});
|
|
171
|
-
it("does not remove a drifted collection when desired path has no indexable markdown",
|
|
171
|
+
it("does not remove a drifted collection when desired path has no indexable markdown", () => {
|
|
172
172
|
const drifted = path.join("/mirror", "knowledge-acme");
|
|
173
173
|
const { exec, calls } = fakeExec({
|
|
174
174
|
listStdout: "acme (qmd://acme/)\n",
|
|
@@ -177,7 +177,7 @@ describe("reindexAfterSync", () => {
|
|
|
177
177
|
: undefined,
|
|
178
178
|
});
|
|
179
179
|
// hasIndexableMarkdown false → skipped before show/repair (desired empty).
|
|
180
|
-
const r =
|
|
180
|
+
const r = reindexAfterSync("/hq", {
|
|
181
181
|
exec,
|
|
182
182
|
acquireReindexLock: noLock,
|
|
183
183
|
existsSync: () => true,
|
|
@@ -189,9 +189,9 @@ describe("reindexAfterSync", () => {
|
|
|
189
189
|
expect(r.collectionsRepaired).toEqual([]);
|
|
190
190
|
expect(calls.some((c) => c[1] === "remove")).toBe(false);
|
|
191
191
|
});
|
|
192
|
-
it("skips company dirs with no indexable markdown",
|
|
192
|
+
it("skips company dirs with no indexable markdown", () => {
|
|
193
193
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
194
|
-
const r =
|
|
194
|
+
const r = reindexAfterSync("/hq", {
|
|
195
195
|
exec,
|
|
196
196
|
acquireReindexLock: noLock,
|
|
197
197
|
existsSync: () => true,
|
|
@@ -201,9 +201,9 @@ describe("reindexAfterSync", () => {
|
|
|
201
201
|
expect(r.collectionsAdded).toEqual([]);
|
|
202
202
|
expect(calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
203
203
|
});
|
|
204
|
-
it("registers populated company projects with the shell name and mask",
|
|
204
|
+
it("registers populated company projects with the shell name and mask", () => {
|
|
205
205
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
206
|
-
const r =
|
|
206
|
+
const r = reindexAfterSync("/hq", {
|
|
207
207
|
exec,
|
|
208
208
|
acquireReindexLock: noLock,
|
|
209
209
|
existsSync: () => true,
|
|
@@ -228,10 +228,10 @@ describe("reindexAfterSync", () => {
|
|
|
228
228
|
["update"],
|
|
229
229
|
]);
|
|
230
230
|
});
|
|
231
|
-
it("skips missing and empty company projects dirs",
|
|
231
|
+
it("skips missing and empty company projects dirs", () => {
|
|
232
232
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
233
233
|
const emptyProjects = path.join("/hq", "companies", "empty", "projects");
|
|
234
|
-
const r =
|
|
234
|
+
const r = reindexAfterSync("/hq", {
|
|
235
235
|
exec,
|
|
236
236
|
acquireReindexLock: noLock,
|
|
237
237
|
existsSync: (p) => p !== path.join("/hq", "companies", "missing", "projects"),
|
|
@@ -242,10 +242,10 @@ describe("reindexAfterSync", () => {
|
|
|
242
242
|
expect(r.collectionsAdded).toEqual([]);
|
|
243
243
|
expect(calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
244
244
|
});
|
|
245
|
-
it("registers populated personal knowledge with the shell name and mask",
|
|
245
|
+
it("registers populated personal knowledge with the shell name and mask", () => {
|
|
246
246
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
247
247
|
const personalKnowledge = path.join("/hq", "personal", "knowledge");
|
|
248
|
-
const r =
|
|
248
|
+
const r = reindexAfterSync("/hq", {
|
|
249
249
|
exec,
|
|
250
250
|
acquireReindexLock: noLock,
|
|
251
251
|
existsSync: () => true,
|
|
@@ -269,9 +269,9 @@ describe("reindexAfterSync", () => {
|
|
|
269
269
|
["update"],
|
|
270
270
|
]);
|
|
271
271
|
});
|
|
272
|
-
it("skips absent and empty personal knowledge",
|
|
272
|
+
it("skips absent and empty personal knowledge", () => {
|
|
273
273
|
const absent = fakeExec({ listStdout: "" });
|
|
274
|
-
const absentResult =
|
|
274
|
+
const absentResult = reindexAfterSync("/hq", {
|
|
275
275
|
exec: absent.exec,
|
|
276
276
|
acquireReindexLock: noLock,
|
|
277
277
|
existsSync: (p) => p !== path.join("/hq", "personal", "knowledge"),
|
|
@@ -280,7 +280,7 @@ describe("reindexAfterSync", () => {
|
|
|
280
280
|
expect(absentResult.collectionsAdded).toEqual([]);
|
|
281
281
|
expect(absent.calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
282
282
|
const empty = fakeExec({ listStdout: "" });
|
|
283
|
-
const emptyResult =
|
|
283
|
+
const emptyResult = reindexAfterSync("/hq", {
|
|
284
284
|
exec: empty.exec,
|
|
285
285
|
acquireReindexLock: noLock,
|
|
286
286
|
existsSync: () => true,
|
|
@@ -290,7 +290,7 @@ describe("reindexAfterSync", () => {
|
|
|
290
290
|
expect(emptyResult.collectionsAdded).toEqual([]);
|
|
291
291
|
expect(empty.calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
292
292
|
});
|
|
293
|
-
it("does not add projects or personal knowledge a second time",
|
|
293
|
+
it("does not add projects or personal knowledge a second time", () => {
|
|
294
294
|
let listCount = 0;
|
|
295
295
|
const { exec, calls } = fakeExec({
|
|
296
296
|
resultFor: (args) => {
|
|
@@ -314,15 +314,15 @@ describe("reindexAfterSync", () => {
|
|
|
314
314
|
hasIndexableProjectContent: () => true,
|
|
315
315
|
realpathSync: identityRealpath,
|
|
316
316
|
};
|
|
317
|
-
expect(
|
|
317
|
+
expect(reindexAfterSync("/hq", options).collectionsAdded).toEqual([
|
|
318
318
|
"acme",
|
|
319
319
|
"acme-projects",
|
|
320
320
|
"personal-knowledge",
|
|
321
321
|
]);
|
|
322
|
-
expect(
|
|
322
|
+
expect(reindexAfterSync("/hq", options).collectionsAdded).toEqual([]);
|
|
323
323
|
expect(calls.filter((c) => c[0] === "collection" && c[1] === "add")).toHaveLength(3);
|
|
324
324
|
});
|
|
325
|
-
it("emits the same collection-add commands as the shell for a fixture tree",
|
|
325
|
+
it("emits the same collection-add commands as the shell for a fixture tree", () => {
|
|
326
326
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-parity-"));
|
|
327
327
|
try {
|
|
328
328
|
const hq = path.join(dir, "hq");
|
|
@@ -340,7 +340,7 @@ describe("reindexAfterSync", () => {
|
|
|
340
340
|
fs.writeFileSync(path.join(acmeProjects, "prd.json"), "{}\n");
|
|
341
341
|
fs.writeFileSync(path.join(personalKnowledge, "note.md"), "# personal\n");
|
|
342
342
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
343
|
-
|
|
343
|
+
reindexAfterSync(hq, { exec, acquireReindexLock: noLock });
|
|
344
344
|
expect(calls.filter((c) => c[0] === "collection" && c[1] === "add")).toEqual([
|
|
345
345
|
["collection", "add", acmeKnowledge, "--name", "acme", "--mask", "**/*.md"],
|
|
346
346
|
[
|
|
@@ -367,9 +367,9 @@ describe("reindexAfterSync", () => {
|
|
|
367
367
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
368
368
|
}
|
|
369
369
|
});
|
|
370
|
-
it("skips qmd entirely when provided changed paths do not touch knowledge markdown",
|
|
370
|
+
it("skips qmd entirely when provided changed paths do not touch knowledge markdown", () => {
|
|
371
371
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
372
|
-
const r =
|
|
372
|
+
const r = reindexAfterSync("/hq", {
|
|
373
373
|
exec,
|
|
374
374
|
acquireReindexLock: noLock,
|
|
375
375
|
existsSync: () => true,
|
|
@@ -379,12 +379,12 @@ describe("reindexAfterSync", () => {
|
|
|
379
379
|
expect(r.updated).toBe(false);
|
|
380
380
|
expect(calls).toEqual([]);
|
|
381
381
|
});
|
|
382
|
-
it("debounces dirty updates with a pending marker and flushes the final update later",
|
|
382
|
+
it("debounces dirty updates with a pending marker and flushes the final update later", () => {
|
|
383
383
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-debounce-"));
|
|
384
384
|
try {
|
|
385
385
|
const statePath = path.join(dir, "qmd-state.json");
|
|
386
386
|
const { exec, calls } = fakeExec({ listStdout: "acme (qmd://acme/)\n" });
|
|
387
|
-
const first =
|
|
387
|
+
const first = reindexAfterSync("/hq", {
|
|
388
388
|
exec,
|
|
389
389
|
acquireReindexLock: noLock,
|
|
390
390
|
existsSync: () => true,
|
|
@@ -403,7 +403,7 @@ describe("reindexAfterSync", () => {
|
|
|
403
403
|
["collection", "list"],
|
|
404
404
|
["collection", "show", "acme"],
|
|
405
405
|
]);
|
|
406
|
-
const second =
|
|
406
|
+
const second = reindexAfterSync("/hq", {
|
|
407
407
|
exec,
|
|
408
408
|
acquireReindexLock: noLock,
|
|
409
409
|
existsSync: () => true,
|
|
@@ -427,9 +427,9 @@ describe("reindexAfterSync", () => {
|
|
|
427
427
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
428
428
|
}
|
|
429
429
|
});
|
|
430
|
-
it("runs embed only when embed:true",
|
|
430
|
+
it("runs embed only when embed:true", () => {
|
|
431
431
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
432
|
-
const r =
|
|
432
|
+
const r = reindexAfterSync("/hq", {
|
|
433
433
|
exec,
|
|
434
434
|
acquireReindexLock: noLock,
|
|
435
435
|
existsSync: () => true,
|
|
@@ -439,13 +439,13 @@ describe("reindexAfterSync", () => {
|
|
|
439
439
|
expect(r.embedded).toBe(true);
|
|
440
440
|
expect(calls).toEqual([["status"], ["collection", "list"], ["update"], ["embed"]]);
|
|
441
441
|
});
|
|
442
|
-
it("never throws even if exec misbehaves",
|
|
443
|
-
const exec =
|
|
442
|
+
it("never throws even if exec misbehaves", () => {
|
|
443
|
+
const exec = () => {
|
|
444
444
|
throw new Error("boom");
|
|
445
445
|
};
|
|
446
|
-
|
|
446
|
+
expect(() => reindexAfterSync("/hq", { exec, acquireReindexLock: noLock, existsSync: () => true })).not.toThrow();
|
|
447
447
|
});
|
|
448
|
-
it("hasIndexableMarkdown finds a real .md and ignores INDEX.md",
|
|
448
|
+
it("hasIndexableMarkdown finds a real .md and ignores INDEX.md", () => {
|
|
449
449
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-test-"));
|
|
450
450
|
try {
|
|
451
451
|
const hq = path.join(dir, "hq");
|
|
@@ -456,7 +456,7 @@ describe("reindexAfterSync", () => {
|
|
|
456
456
|
fs.writeFileSync(path.join(kdir, "INDEX.md"), "# index\n");
|
|
457
457
|
fs.writeFileSync(path.join(kdir, "note.md"), "# real\n");
|
|
458
458
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
459
|
-
const r =
|
|
459
|
+
const r = reindexAfterSync(hq, { exec, acquireReindexLock: noLock }); // real fs walk
|
|
460
460
|
expect(r.collectionsAdded).toEqual(["acme"]);
|
|
461
461
|
expect(calls.some((c) => c[0] === "collection" && c[1] === "add")).toBe(true);
|
|
462
462
|
}
|
|
@@ -465,12 +465,12 @@ describe("reindexAfterSync", () => {
|
|
|
465
465
|
}
|
|
466
466
|
});
|
|
467
467
|
// ── Corruption safety: lock serialization (feedback_b9a369ff) ──────────────
|
|
468
|
-
it("skips the cycle after status when another writer holds the lock",
|
|
468
|
+
it("skips the cycle after status when another writer holds the lock", () => {
|
|
469
469
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-lockbusy-"));
|
|
470
470
|
try {
|
|
471
471
|
const statePath = path.join(dir, "qmd-state.json");
|
|
472
472
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
473
|
-
const r =
|
|
473
|
+
const r = reindexAfterSync("/hq", {
|
|
474
474
|
exec,
|
|
475
475
|
existsSync: () => true,
|
|
476
476
|
statePath,
|
|
@@ -489,14 +489,14 @@ describe("reindexAfterSync", () => {
|
|
|
489
489
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
490
490
|
}
|
|
491
491
|
});
|
|
492
|
-
it("always releases the lock, even when a qmd command throws after status",
|
|
492
|
+
it("always releases the lock, even when a qmd command throws after status", () => {
|
|
493
493
|
let released = false;
|
|
494
|
-
const exec =
|
|
494
|
+
const exec = (args) => {
|
|
495
495
|
if (args[0] === "status")
|
|
496
496
|
return { status: 0, stdout: "" };
|
|
497
497
|
throw new Error("boom");
|
|
498
498
|
};
|
|
499
|
-
|
|
499
|
+
reindexAfterSync("/hq", {
|
|
500
500
|
exec,
|
|
501
501
|
existsSync: () => true,
|
|
502
502
|
acquireReindexLock: () => ({
|
|
@@ -508,7 +508,7 @@ describe("reindexAfterSync", () => {
|
|
|
508
508
|
expect(released).toBe(true);
|
|
509
509
|
});
|
|
510
510
|
// ── Corruption safety: timeout-kill handling ───────────────────────────────
|
|
511
|
-
it("treats a timed-out update as not-done, marks dirty, and does NOT embed",
|
|
511
|
+
it("treats a timed-out update as not-done, marks dirty, and does NOT embed", () => {
|
|
512
512
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-timeout-"));
|
|
513
513
|
try {
|
|
514
514
|
const statePath = path.join(dir, "qmd-state.json");
|
|
@@ -516,7 +516,7 @@ describe("reindexAfterSync", () => {
|
|
|
516
516
|
listStdout: "",
|
|
517
517
|
resultFor: (args) => args[0] === "update" ? { status: null, timedOut: true } : undefined,
|
|
518
518
|
});
|
|
519
|
-
const r =
|
|
519
|
+
const r = reindexAfterSync("/hq", {
|
|
520
520
|
exec,
|
|
521
521
|
acquireReindexLock: noLock,
|
|
522
522
|
existsSync: () => true,
|
|
@@ -536,12 +536,12 @@ describe("reindexAfterSync", () => {
|
|
|
536
536
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
537
537
|
}
|
|
538
538
|
});
|
|
539
|
-
it("treats a timed-out embed as not-embedded and marks dirty",
|
|
539
|
+
it("treats a timed-out embed as not-embedded and marks dirty", () => {
|
|
540
540
|
const { exec } = fakeExec({
|
|
541
541
|
listStdout: "",
|
|
542
542
|
resultFor: (args) => args[0] === "embed" ? { status: null, timedOut: true } : undefined,
|
|
543
543
|
});
|
|
544
|
-
const r =
|
|
544
|
+
const r = reindexAfterSync("/hq", {
|
|
545
545
|
exec,
|
|
546
546
|
acquireReindexLock: noLock,
|
|
547
547
|
existsSync: () => true,
|
|
@@ -554,7 +554,7 @@ describe("reindexAfterSync", () => {
|
|
|
554
554
|
expect(r.pendingDirty).toBe(true);
|
|
555
555
|
});
|
|
556
556
|
// ── Corruption safety: SQLITE_CORRUPT detection + quarantine ────────────────
|
|
557
|
-
it("quarantines the resolved index dir when update reports SQLITE_CORRUPT",
|
|
557
|
+
it("quarantines the resolved index dir when update reports SQLITE_CORRUPT", () => {
|
|
558
558
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-corrupt-"));
|
|
559
559
|
try {
|
|
560
560
|
const statePath = path.join(dir, "qmd-state.json");
|
|
@@ -571,7 +571,7 @@ describe("reindexAfterSync", () => {
|
|
|
571
571
|
? { status: 1, stderr: "SqliteError: database disk image is malformed (SQLITE_CORRUPT)" }
|
|
572
572
|
: undefined,
|
|
573
573
|
});
|
|
574
|
-
const r =
|
|
574
|
+
const r = reindexAfterSync("/hq", {
|
|
575
575
|
exec,
|
|
576
576
|
acquireReindexLock: noLock,
|
|
577
577
|
existsSync: () => true,
|
|
@@ -593,14 +593,14 @@ describe("reindexAfterSync", () => {
|
|
|
593
593
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
594
594
|
}
|
|
595
595
|
});
|
|
596
|
-
it("reports quarantine failure when corruption is detected but nothing moves",
|
|
596
|
+
it("reports quarantine failure when corruption is detected but nothing moves", () => {
|
|
597
597
|
const quarantined = [];
|
|
598
598
|
const { exec, calls } = fakeExec({
|
|
599
599
|
resultFor: (args) => args[0] === "collection" && args[1] === "list"
|
|
600
600
|
? { status: 0, stdout: "database disk image is malformed" }
|
|
601
601
|
: undefined,
|
|
602
602
|
});
|
|
603
|
-
const r =
|
|
603
|
+
const r = reindexAfterSync("/hq", {
|
|
604
604
|
exec,
|
|
605
605
|
acquireReindexLock: noLock,
|
|
606
606
|
existsSync: () => true,
|
|
@@ -615,7 +615,7 @@ describe("reindexAfterSync", () => {
|
|
|
615
615
|
expect(calls).toEqual([["status"], ["collection", "list"]]);
|
|
616
616
|
expect(quarantined).toEqual([path.join("/hq", ".qmd")]);
|
|
617
617
|
});
|
|
618
|
-
it("quarantines when embed reports SQLITE_CORRUPT",
|
|
618
|
+
it("quarantines when embed reports SQLITE_CORRUPT", () => {
|
|
619
619
|
const quarantined = [];
|
|
620
620
|
const { exec, calls } = fakeExec({
|
|
621
621
|
listStdout: "",
|
|
@@ -623,7 +623,7 @@ describe("reindexAfterSync", () => {
|
|
|
623
623
|
? { status: 1, stderr: "code SQLITE_CORRUPT at store.js:1546" }
|
|
624
624
|
: undefined,
|
|
625
625
|
});
|
|
626
|
-
const r =
|
|
626
|
+
const r = reindexAfterSync("/hq", {
|
|
627
627
|
exec,
|
|
628
628
|
acquireReindexLock: noLock,
|
|
629
629
|
existsSync: () => true,
|
|
@@ -642,7 +642,7 @@ describe("reindexAfterSync", () => {
|
|
|
642
642
|
});
|
|
643
643
|
// ── The default reindex lock (real filesystem) ───────────────────────────────
|
|
644
644
|
describe("default reindex lock", () => {
|
|
645
|
-
it("holds the lock for the duration of the cycle and releases it after",
|
|
645
|
+
it("holds the lock for the duration of the cycle and releases it after", () => {
|
|
646
646
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-lock-"));
|
|
647
647
|
try {
|
|
648
648
|
const hq = path.join(dir, "hq");
|
|
@@ -654,7 +654,7 @@ describe("default reindex lock", () => {
|
|
|
654
654
|
const lockPath = path.join(hq, ".qmd", ".reindex.lock");
|
|
655
655
|
let reAcquireBlockedWhileHeld = false;
|
|
656
656
|
const { exec } = fakeExec({ listStdout: "" });
|
|
657
|
-
const observingExec =
|
|
657
|
+
const observingExec = (args) => {
|
|
658
658
|
if (args[0] === "update") {
|
|
659
659
|
try {
|
|
660
660
|
fs.closeSync(fs.openSync(lockPath, "wx"));
|
|
@@ -665,9 +665,9 @@ describe("default reindex lock", () => {
|
|
|
665
665
|
err.code === "EEXIST";
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
|
-
return
|
|
668
|
+
return exec(args);
|
|
669
669
|
};
|
|
670
|
-
const r =
|
|
670
|
+
const r = reindexAfterSync(hq, { exec: observingExec });
|
|
671
671
|
expect(r.updated).toBe(true);
|
|
672
672
|
expect(reAcquireBlockedWhileHeld).toBe(true);
|
|
673
673
|
expect(fs.existsSync(lockPath)).toBe(false);
|
|
@@ -676,7 +676,7 @@ describe("default reindex lock", () => {
|
|
|
676
676
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
677
677
|
}
|
|
678
678
|
});
|
|
679
|
-
it("reclaims a lock held by a dead PID (stale takeover)",
|
|
679
|
+
it("reclaims a lock held by a dead PID (stale takeover)", () => {
|
|
680
680
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-lock-stale-"));
|
|
681
681
|
try {
|
|
682
682
|
const hq = path.join(dir, "hq");
|
|
@@ -689,7 +689,7 @@ describe("default reindex lock", () => {
|
|
|
689
689
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
690
690
|
fs.writeFileSync(lockPath, JSON.stringify({ pid: 2147483646, startedAt: "2000-01-01T00:00:00.000Z", command: "dead" }));
|
|
691
691
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
692
|
-
const r =
|
|
692
|
+
const r = reindexAfterSync(hq, { exec });
|
|
693
693
|
expect(r.updated).toBe(true);
|
|
694
694
|
expect(r.lockBusy).toBe(false);
|
|
695
695
|
expect(calls.some((c) => c[0] === "update")).toBe(true);
|
|
@@ -700,7 +700,7 @@ describe("default reindex lock", () => {
|
|
|
700
700
|
});
|
|
701
701
|
});
|
|
702
702
|
describe("acquireDualReindexLocks", () => {
|
|
703
|
-
it("dedupes identical paths so a process cannot lock itself out",
|
|
703
|
+
it("dedupes identical paths so a process cannot lock itself out", () => {
|
|
704
704
|
let acquires = 0;
|
|
705
705
|
const acquire = () => {
|
|
706
706
|
acquires++;
|
|
@@ -712,7 +712,7 @@ describe("acquireDualReindexLocks", () => {
|
|
|
712
712
|
expect(locks).toHaveLength(1);
|
|
713
713
|
expect(acquires).toBe(1);
|
|
714
714
|
});
|
|
715
|
-
it("releases the first lock when the second is busy",
|
|
715
|
+
it("releases the first lock when the second is busy", () => {
|
|
716
716
|
const released = [];
|
|
717
717
|
let n = 0;
|
|
718
718
|
const acquire = (lockPath) => {
|
|
@@ -736,79 +736,46 @@ describe("acquireDualReindexLocks", () => {
|
|
|
736
736
|
});
|
|
737
737
|
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
738
738
|
describe("resolveExecTimeoutMs", () => {
|
|
739
|
-
it("defaults to a generous bound, not the old 120s that killed writes",
|
|
739
|
+
it("defaults to a generous bound, not the old 120s that killed writes", () => {
|
|
740
740
|
expect(resolveExecTimeoutMs({})).toBe(DEFAULT_QMD_EXEC_TIMEOUT_MS);
|
|
741
741
|
expect(DEFAULT_QMD_EXEC_TIMEOUT_MS).toBeGreaterThan(120_000);
|
|
742
742
|
});
|
|
743
|
-
it("honors a valid HQ_QMD_EXEC_TIMEOUT_MS override",
|
|
743
|
+
it("honors a valid HQ_QMD_EXEC_TIMEOUT_MS override", () => {
|
|
744
744
|
expect(resolveExecTimeoutMs({ HQ_QMD_EXEC_TIMEOUT_MS: "300000" })).toBe(300_000);
|
|
745
745
|
});
|
|
746
|
-
it("ignores an invalid override",
|
|
746
|
+
it("ignores an invalid override", () => {
|
|
747
747
|
expect(resolveExecTimeoutMs({ HQ_QMD_EXEC_TIMEOUT_MS: "nope" })).toBe(DEFAULT_QMD_EXEC_TIMEOUT_MS);
|
|
748
748
|
expect(resolveExecTimeoutMs({ HQ_QMD_EXEC_TIMEOUT_MS: "-5" })).toBe(DEFAULT_QMD_EXEC_TIMEOUT_MS);
|
|
749
749
|
expect(resolveExecTimeoutMs({ HQ_QMD_EXEC_TIMEOUT_MS: "0" })).toBe(DEFAULT_QMD_EXEC_TIMEOUT_MS);
|
|
750
750
|
});
|
|
751
751
|
});
|
|
752
|
-
describe("createQmdExec", () => {
|
|
753
|
-
it("captures output and sends SIGINT when the configured timeout expires", async () => {
|
|
754
|
-
const exec = createQmdExec({
|
|
755
|
-
command: process.execPath,
|
|
756
|
-
timeoutMs: 250,
|
|
757
|
-
sigintGraceMs: 100,
|
|
758
|
-
});
|
|
759
|
-
const result = await exec([
|
|
760
|
-
"-e",
|
|
761
|
-
"process.on('SIGINT', () => { process.stdout.write('sigint'); process.exit(0); }); setInterval(() => {}, 1000);",
|
|
762
|
-
]);
|
|
763
|
-
expect(result).toMatchObject({ status: 0, timedOut: true });
|
|
764
|
-
expect(result.stdout).toContain("sigint");
|
|
765
|
-
});
|
|
766
|
-
it("sends SIGINT to an in-flight child on shutdown and escalates after the grace period", async () => {
|
|
767
|
-
const controller = new AbortController();
|
|
768
|
-
const exec = createQmdExec({
|
|
769
|
-
command: process.execPath,
|
|
770
|
-
timeoutMs: 10_000,
|
|
771
|
-
sigintGraceMs: 50,
|
|
772
|
-
});
|
|
773
|
-
const resultPromise = exec([
|
|
774
|
-
"-e",
|
|
775
|
-
"process.on('SIGINT', () => process.stdout.write('sigint')); setInterval(() => {}, 1000);",
|
|
776
|
-
], { signal: controller.signal });
|
|
777
|
-
const abortTimer = setTimeout(() => controller.abort(), 250);
|
|
778
|
-
const result = await resultPromise;
|
|
779
|
-
clearTimeout(abortTimer);
|
|
780
|
-
expect(result.timedOut).toBe(false);
|
|
781
|
-
expect(result.stdout).toContain("sigint");
|
|
782
|
-
expect(result.status).toBeNull();
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
752
|
describe("looksCorrupt", () => {
|
|
786
|
-
it("matches the SQLITE_CORRUPT signature on stdout or stderr",
|
|
753
|
+
it("matches the SQLITE_CORRUPT signature on stdout or stderr", () => {
|
|
787
754
|
expect(looksCorrupt({ stderr: "SqliteError: ... code SQLITE_CORRUPT" })).toBe(true);
|
|
788
755
|
expect(looksCorrupt({ stdout: "database disk image is malformed" })).toBe(true);
|
|
789
756
|
expect(looksCorrupt({ stdout: "Database disk image IS MALFORMED" })).toBe(true);
|
|
790
757
|
});
|
|
791
|
-
it("does not match clean output",
|
|
758
|
+
it("does not match clean output", () => {
|
|
792
759
|
expect(looksCorrupt({ stdout: "Updated 688 documents", stderr: "" })).toBe(false);
|
|
793
760
|
expect(looksCorrupt({})).toBe(false);
|
|
794
761
|
});
|
|
795
762
|
});
|
|
796
763
|
describe("parseIndexDirFromStatus / parseCollectionPathFromShow", () => {
|
|
797
|
-
it("parses Index file path into its directory",
|
|
764
|
+
it("parses Index file path into its directory", () => {
|
|
798
765
|
expect(parseIndexDirFromStatus("Index: C:/Users/me/.cache/qmd/index.sqlite\nSize: 1.3 GB\n")).toBe(path.dirname("C:/Users/me/.cache/qmd/index.sqlite"));
|
|
799
766
|
});
|
|
800
|
-
it("returns null when Index line is missing",
|
|
767
|
+
it("returns null when Index line is missing", () => {
|
|
801
768
|
expect(parseIndexDirFromStatus("Collections: 2\n")).toBeNull();
|
|
802
769
|
});
|
|
803
|
-
it("parses Path from collection show",
|
|
770
|
+
it("parses Path from collection show", () => {
|
|
804
771
|
expect(parseCollectionPathFromShow("Collection: indigo\n Path: /hq/companies/indigo/knowledge\n")).toBe("/hq/companies/indigo/knowledge");
|
|
805
772
|
});
|
|
806
773
|
});
|
|
807
774
|
describe("pathsEquivalent / repairPathDriftEnabled", () => {
|
|
808
|
-
it("treats case and trailing separators as equal",
|
|
775
|
+
it("treats case and trailing separators as equal", () => {
|
|
809
776
|
expect(pathsEquivalent("/HQ/Companies/Acme/knowledge/", "/hq/companies/acme/knowledge", (p) => p)).toBe(true);
|
|
810
777
|
});
|
|
811
|
-
it("enables repair only for 1/true/yes",
|
|
778
|
+
it("enables repair only for 1/true/yes", () => {
|
|
812
779
|
expect(repairPathDriftEnabled({})).toBe(false);
|
|
813
780
|
expect(repairPathDriftEnabled({ HQ_QMD_REPAIR_PATH_DRIFT: "1" })).toBe(true);
|
|
814
781
|
expect(repairPathDriftEnabled({ HQ_QMD_REPAIR_PATH_DRIFT: "true" })).toBe(true);
|