@indigoai-us/hq-cloud 6.15.20 → 6.15.21
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.d.ts +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +37 -5
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +77 -4
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.test.js +140 -108
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.test.js +59 -47
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-delta-writes.test.d.ts +2 -0
- package/dist/journal-delta-writes.test.d.ts.map +1 -0
- package/dist/journal-delta-writes.test.js +140 -0
- package/dist/journal-delta-writes.test.js.map +1 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +151 -24
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +29 -11
- package/dist/journal.test.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +7 -1
- package/dist/outcome-telemetry.d.ts.map +1 -1
- package/dist/outcome-telemetry.js +15 -9
- package/dist/outcome-telemetry.js.map +1 -1
- package/dist/outcome-telemetry.test.js +15 -0
- package/dist/outcome-telemetry.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -8
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +43 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/telemetry.d.ts +7 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +67 -60
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +12 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -118,7 +118,23 @@ import * as s3Module from "../s3.js";
|
|
|
118
118
|
import { reindex } from "./reindex.js";
|
|
119
119
|
import { readSyncProgress } from "../sync-progress.js";
|
|
120
120
|
import { VaultAuthError } from "../vault-client.js";
|
|
121
|
-
import { readJournal, getEntry } from "../journal.js";
|
|
121
|
+
import { readJournal, getEntry, listJournals } from "../journal.js";
|
|
122
|
+
/**
|
|
123
|
+
* Read a journal the way production does.
|
|
124
|
+
*
|
|
125
|
+
* These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
|
|
126
|
+
* when that file held a full copy of the journal. It is now a small locator —
|
|
127
|
+
* the authority is the v3 state store — so the assertions below go through
|
|
128
|
+
* `readJournal`, which is what the sync engine itself uses. The slug is
|
|
129
|
+
* recovered from the path so each call site keeps naming the journal it means.
|
|
130
|
+
*/
|
|
131
|
+
function journalAt(journalPath) {
|
|
132
|
+
const slug = path
|
|
133
|
+
.basename(journalPath)
|
|
134
|
+
.replace(/^sync-journal\./, "")
|
|
135
|
+
.replace(/\.json$/, "");
|
|
136
|
+
return readJournal(slug);
|
|
137
|
+
}
|
|
122
138
|
const mockConfig = {
|
|
123
139
|
apiUrl: "https://vault-api.test",
|
|
124
140
|
authToken: "test-jwt-token",
|
|
@@ -516,7 +532,7 @@ describe("sync", () => {
|
|
|
516
532
|
hqRoot: tmpDir,
|
|
517
533
|
});
|
|
518
534
|
expect(result.conflicts).toBe(1);
|
|
519
|
-
const journal =
|
|
535
|
+
const journal = journalAt(journalPath);
|
|
520
536
|
// remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
|
|
521
537
|
expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
|
|
522
538
|
// …but the kept-local copy is flagged divergent so a later currency-gated
|
|
@@ -556,7 +572,7 @@ describe("sync", () => {
|
|
|
556
572
|
// Identical bookkeeping to `keep`: the etag is stamped so the conflict
|
|
557
573
|
// cannot re-fire, and the honesty flag is armed so the push leg knows this
|
|
558
574
|
// is the divergent body it may promote under an If-Match fence.
|
|
559
|
-
const journal =
|
|
575
|
+
const journal = journalAt(journalPath);
|
|
560
576
|
expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
|
|
561
577
|
expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
|
|
562
578
|
});
|
|
@@ -585,7 +601,7 @@ describe("sync", () => {
|
|
|
585
601
|
]);
|
|
586
602
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
587
603
|
expect(fs.existsSync(path.join(companyDocs, "handoff.md"))).toBe(true);
|
|
588
|
-
const journal =
|
|
604
|
+
const journal = journalAt(journalPath);
|
|
589
605
|
expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
|
|
590
606
|
});
|
|
591
607
|
it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
|
|
@@ -624,7 +640,7 @@ describe("sync", () => {
|
|
|
624
640
|
samplePaths: [linkKey],
|
|
625
641
|
});
|
|
626
642
|
expect(events.some((event) => event.type === "error")).toBe(false);
|
|
627
|
-
const journal =
|
|
643
|
+
const journal = journalAt(journalPath);
|
|
628
644
|
expect(journal.files[linkKey]).toBeUndefined();
|
|
629
645
|
}
|
|
630
646
|
finally {
|
|
@@ -722,7 +738,7 @@ describe("sync", () => {
|
|
|
722
738
|
expect(litter).toEqual([]);
|
|
723
739
|
// Journal re-stamped off the stale baseline (hash now matches local,
|
|
724
740
|
// remoteEtag recorded) so the phantom conflict can't re-fire.
|
|
725
|
-
const journalAfter =
|
|
741
|
+
const journalAfter = journalAt(journalPath);
|
|
726
742
|
expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
|
|
727
743
|
expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
|
|
728
744
|
});
|
|
@@ -780,7 +796,7 @@ describe("sync", () => {
|
|
|
780
796
|
});
|
|
781
797
|
expect(events.some((event) => event.type === "error")).toBe(false);
|
|
782
798
|
expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
|
|
783
|
-
const journal =
|
|
799
|
+
const journal = journalAt(journalPath);
|
|
784
800
|
expect(journal.files[linkKey].hash).toBe("stale-hash");
|
|
785
801
|
}
|
|
786
802
|
finally {
|
|
@@ -1168,7 +1184,7 @@ describe("sync", () => {
|
|
|
1168
1184
|
.filter((f) => f.includes(".conflict-"));
|
|
1169
1185
|
expect(litter).toHaveLength(0);
|
|
1170
1186
|
// Journal stamped, so the next pass is a plain unchanged-skip.
|
|
1171
|
-
const journal =
|
|
1187
|
+
const journal = journalAt(journalPath);
|
|
1172
1188
|
expect(journal.files["docs/handoff.md"]).toBeDefined();
|
|
1173
1189
|
});
|
|
1174
1190
|
it("RF-F02EXEC: refuses a download whose parent symlink appears after planning", async () => {
|
|
@@ -1328,7 +1344,7 @@ describe("sync", () => {
|
|
|
1328
1344
|
});
|
|
1329
1345
|
expect(fs.readFileSync(untrackedPath, "utf-8")).toBe("brand new local work");
|
|
1330
1346
|
expect(fs.existsSync(trackedPath)).toBe(false);
|
|
1331
|
-
const journal =
|
|
1347
|
+
const journal = journalAt(journalPath);
|
|
1332
1348
|
expect(journal.files[untrackedKey]).toBeUndefined();
|
|
1333
1349
|
expect(journal.files[trackedKey]).toBeUndefined();
|
|
1334
1350
|
});
|
|
@@ -1401,7 +1417,7 @@ describe("sync", () => {
|
|
|
1401
1417
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1402
1418
|
expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
|
|
1403
1419
|
expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
|
|
1404
|
-
const journal =
|
|
1420
|
+
const journal = journalAt(journalPath);
|
|
1405
1421
|
expect(journal.files[key]?.removedAt).toBeUndefined();
|
|
1406
1422
|
});
|
|
1407
1423
|
it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
|
|
@@ -1449,7 +1465,7 @@ describe("sync", () => {
|
|
|
1449
1465
|
// Equivalence: an ignored key is never tombstoned — the local file and its
|
|
1450
1466
|
// journal entry both survive, exactly as before the fix.
|
|
1451
1467
|
expect(fs.existsSync(ignoredLocal)).toBe(true);
|
|
1452
|
-
const sweptJournal =
|
|
1468
|
+
const sweptJournal = journalAt(journalPath);
|
|
1453
1469
|
expect(sweptJournal.files[ignoredKey]).toBeDefined();
|
|
1454
1470
|
});
|
|
1455
1471
|
it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
|
|
@@ -1483,7 +1499,7 @@ describe("sync", () => {
|
|
|
1483
1499
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1484
1500
|
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
|
|
1485
1501
|
expect(fs.existsSync(localPath)).toBe(false);
|
|
1486
|
-
expect(
|
|
1502
|
+
expect(journalAt(journalPath).files[key]
|
|
1487
1503
|
?.removedReason).toBe("local-delete");
|
|
1488
1504
|
});
|
|
1489
1505
|
it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
|
|
@@ -1519,7 +1535,7 @@ describe("sync", () => {
|
|
|
1519
1535
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1520
1536
|
// All restored, none tombstoned.
|
|
1521
1537
|
expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
|
|
1522
|
-
const journal =
|
|
1538
|
+
const journal = journalAt(journalPath);
|
|
1523
1539
|
for (const k of keys) {
|
|
1524
1540
|
expect(journal.files[k]?.removedAt).toBeUndefined();
|
|
1525
1541
|
}
|
|
@@ -1560,7 +1576,7 @@ describe("sync", () => {
|
|
|
1560
1576
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1561
1577
|
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
|
|
1562
1578
|
expect(fs.existsSync(localPath)).toBe(false);
|
|
1563
|
-
const journal =
|
|
1579
|
+
const journal = journalAt(journalPath);
|
|
1564
1580
|
// Entry PRESERVED as a tombstone, not removed.
|
|
1565
1581
|
expect(journal.files[key]?.removedAt).toBeTruthy();
|
|
1566
1582
|
expect(journal.files[key]?.removedReason).toBe("local-delete");
|
|
@@ -1592,7 +1608,7 @@ describe("sync", () => {
|
|
|
1592
1608
|
it("stamps journal.lastSync on every successful run, even when nothing transferred", async () => {
|
|
1593
1609
|
// First run downloads both remote files and stamps lastSync.
|
|
1594
1610
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1595
|
-
const firstStamp =
|
|
1611
|
+
const firstStamp = journalAt(journalPath).lastSync;
|
|
1596
1612
|
expect(firstStamp).not.toBe("");
|
|
1597
1613
|
// Second run: same remote, same local — Stage-2 plan is all-skip, so
|
|
1598
1614
|
// updateEntry never fires. lastSync must still advance, otherwise the
|
|
@@ -1600,7 +1616,7 @@ describe("sync", () => {
|
|
|
1600
1616
|
await new Promise((r) => setTimeout(r, 5));
|
|
1601
1617
|
const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1602
1618
|
expect(result.filesDownloaded).toBe(0);
|
|
1603
|
-
const secondStamp =
|
|
1619
|
+
const secondStamp = journalAt(journalPath).lastSync;
|
|
1604
1620
|
expect(new Date(secondStamp).getTime()).toBeGreaterThan(new Date(firstStamp).getTime());
|
|
1605
1621
|
});
|
|
1606
1622
|
it("journalSlug: 'personal' routes journal I/O to sync-journal.personal.json", async () => {
|
|
@@ -1691,13 +1707,9 @@ describe("sync", () => {
|
|
|
1691
1707
|
expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "bar.md"))).toBe(false);
|
|
1692
1708
|
// The whole point: it now gets a journal baseline, so the push side stops
|
|
1693
1709
|
// re-firing a transient conflict every sync (the bug this fix closes).
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
.some((f) => {
|
|
1698
|
-
const j = JSON.parse(fs.readFileSync(path.join(stateDir, f), "utf8"));
|
|
1699
|
-
return j.files?.["companies/manifest.yaml"] != null;
|
|
1700
|
-
});
|
|
1710
|
+
// Enumerate through the API: the per-slug JSON files are locators now, so
|
|
1711
|
+
// reading them directly finds no `files` map at all.
|
|
1712
|
+
const journaledManifest = listJournals().some((j) => j.journal.files?.["companies/manifest.yaml"] != null);
|
|
1701
1713
|
expect(journaledManifest).toBe(true);
|
|
1702
1714
|
});
|
|
1703
1715
|
it("personalMode pull lands the session-continuity pointer + active thread file under <hqRoot>/workspace/threads/ so a handoff resumes on machine B (DEV-1778)", async () => {
|
|
@@ -1950,7 +1962,7 @@ describe("sync", () => {
|
|
|
1950
1962
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
1951
1963
|
// Journal entry for the deleted path must be gone too — otherwise
|
|
1952
1964
|
// the next sync would tombstone the same key on every run.
|
|
1953
|
-
const journal =
|
|
1965
|
+
const journal = journalAt(journalPath);
|
|
1954
1966
|
expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
|
|
1955
1967
|
expect(journal.files["docs/kept.md"]).toBeDefined();
|
|
1956
1968
|
});
|
|
@@ -2015,7 +2027,7 @@ describe("sync", () => {
|
|
|
2015
2027
|
expect(result.filesTombstoned).toBe(0);
|
|
2016
2028
|
// The journal is healed: the malformed key is gone, the canonical POSIX key
|
|
2017
2029
|
// points at the live file (so a future pull stays converged, not poisoned).
|
|
2018
|
-
const journal =
|
|
2030
|
+
const journal = journalAt(journalPath);
|
|
2019
2031
|
expect(journal.files[backslashKey]).toBeUndefined();
|
|
2020
2032
|
expect(journal.files[posixKey]).toBeDefined();
|
|
2021
2033
|
expect(Object.keys(journal.files).some((k) => k.includes("\\"))).toBe(false);
|
|
@@ -2068,7 +2080,7 @@ describe("sync", () => {
|
|
|
2068
2080
|
expect(result.filesTombstoned).toBe(0);
|
|
2069
2081
|
// Journal entry retained — next sync (push) will re-evaluate and
|
|
2070
2082
|
// either re-upload the divergent local or surface as a conflict.
|
|
2071
|
-
const journal =
|
|
2083
|
+
const journal = journalAt(journalPath);
|
|
2072
2084
|
expect(journal.files["docs/edited-locally.md"]).toBeDefined();
|
|
2073
2085
|
expect(journal.files["docs/edited-locally.md"].hash).toBe(baselineHash);
|
|
2074
2086
|
});
|
|
@@ -2112,7 +2124,7 @@ describe("sync", () => {
|
|
|
2112
2124
|
expect(result.filesTombstoned).toBe(0);
|
|
2113
2125
|
expect(fs.existsSync(racedPath)).toBe(true);
|
|
2114
2126
|
expect(fs.readFileSync(racedPath, "utf-8")).toBe("concurrent local edit");
|
|
2115
|
-
const journal =
|
|
2127
|
+
const journal = journalAt(journalPath);
|
|
2116
2128
|
expect(journal.files["docs/racy-delete.md"]).toBeDefined();
|
|
2117
2129
|
expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
|
|
2118
2130
|
});
|
|
@@ -2149,7 +2161,7 @@ describe("sync", () => {
|
|
|
2149
2161
|
expect(result.filesTombstoned).toBe(0);
|
|
2150
2162
|
expect(result.conflicts).toBe(1);
|
|
2151
2163
|
expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
|
|
2152
|
-
const journal =
|
|
2164
|
+
const journal = journalAt(journalPath);
|
|
2153
2165
|
expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
|
|
2154
2166
|
});
|
|
2155
2167
|
it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
|
|
@@ -2233,7 +2245,7 @@ describe("sync", () => {
|
|
|
2233
2245
|
propagateDeletePolicy: "all",
|
|
2234
2246
|
});
|
|
2235
2247
|
expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
|
|
2236
|
-
const journal =
|
|
2248
|
+
const journal = journalAt(personalJournalPath);
|
|
2237
2249
|
expect(journal.files["core/core.yaml"]).toBeDefined();
|
|
2238
2250
|
expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
|
|
2239
2251
|
});
|
|
@@ -2267,7 +2279,7 @@ describe("sync", () => {
|
|
|
2267
2279
|
});
|
|
2268
2280
|
expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
|
|
2269
2281
|
expect(result.filesRefusedStale).toBe(2);
|
|
2270
|
-
const journal =
|
|
2282
|
+
const journal = journalAt(journalPath);
|
|
2271
2283
|
expect(journal.files["docs/a.md"]).toBeDefined();
|
|
2272
2284
|
expect(journal.files["docs/b.md"]).toBeDefined();
|
|
2273
2285
|
});
|
|
@@ -2359,7 +2371,7 @@ describe("sync", () => {
|
|
|
2359
2371
|
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
|
2360
2372
|
expect(fs.readlinkSync(linkPath)).toBe("new-target.md");
|
|
2361
2373
|
expect(result.filesTombstoned).toBe(0);
|
|
2362
|
-
const journal =
|
|
2374
|
+
const journal = journalAt(journalPath);
|
|
2363
2375
|
expect(journal.files["docs/link.md"]).toBeDefined();
|
|
2364
2376
|
expect(journal.files["docs/link.md"].hash).toBe(baselineHash);
|
|
2365
2377
|
});
|
|
@@ -2426,7 +2438,7 @@ describe("sync", () => {
|
|
|
2426
2438
|
// The really-deleted file MUST be tombstoned.
|
|
2427
2439
|
expect(fs.existsSync(reallyDeletedPath)).toBe(false);
|
|
2428
2440
|
expect(result.filesTombstoned).toBe(1);
|
|
2429
|
-
const journal =
|
|
2441
|
+
const journal = journalAt(journalPath);
|
|
2430
2442
|
// Journal entry retained for the invisible key (so next sync at
|
|
2431
2443
|
// broader scope can re-evaluate).
|
|
2432
2444
|
expect(journal.files["docs/invisible-to-list.md"]).toBeDefined();
|
|
@@ -2500,7 +2512,7 @@ describe("sync", () => {
|
|
|
2500
2512
|
expect(result.filesDownloaded).toBe(0);
|
|
2501
2513
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
2502
2514
|
// Journal entry dropped so it converges (no re-tombstone next run).
|
|
2503
|
-
const journal =
|
|
2515
|
+
const journal = journalAt(journalPath);
|
|
2504
2516
|
expect(journal.files["docs/shared.md"]).toBeUndefined();
|
|
2505
2517
|
});
|
|
2506
2518
|
it("delete-resync: a re-created object NEWER than the tombstone DOES sync (tombstone is not permanent)", async () => {
|
|
@@ -2664,7 +2676,7 @@ describe("sync", () => {
|
|
|
2664
2676
|
});
|
|
2665
2677
|
expect(fs.existsSync(deniedPath)).toBe(true);
|
|
2666
2678
|
expect(result.filesTombstoned).toBe(0);
|
|
2667
|
-
const journal =
|
|
2679
|
+
const journal = journalAt(journalPath);
|
|
2668
2680
|
expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
|
|
2669
2681
|
});
|
|
2670
2682
|
it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
|
|
@@ -2712,7 +2724,7 @@ describe("sync", () => {
|
|
|
2712
2724
|
// Tombstone NOT counted — the delete wasn't honored.
|
|
2713
2725
|
expect(result.filesTombstoned).toBe(0);
|
|
2714
2726
|
// Journal entry MUST be retained so the next sync retries.
|
|
2715
|
-
const journal =
|
|
2727
|
+
const journal = journalAt(journalPath);
|
|
2716
2728
|
expect(journal.files["docs/locked-by-peer.md"]).toBeDefined();
|
|
2717
2729
|
expect(journal.files["docs/locked-by-peer.md"].hash).toBe("locked-hash");
|
|
2718
2730
|
}
|
|
@@ -2939,7 +2951,7 @@ describe("sync", () => {
|
|
|
2939
2951
|
expect(s3Module.downloadFile).not.toHaveBeenCalled();
|
|
2940
2952
|
expect(fs.existsSync(escapedPath)).toBe(false);
|
|
2941
2953
|
expect(fs.existsSync(path.join(companyRoot, traversalKey))).toBe(false);
|
|
2942
|
-
const journal =
|
|
2954
|
+
const journal = journalAt(journalPath);
|
|
2943
2955
|
expect(journal.files[traversalKey]).toBeUndefined();
|
|
2944
2956
|
}
|
|
2945
2957
|
finally {
|
|
@@ -2973,7 +2985,7 @@ describe("sync", () => {
|
|
|
2973
2985
|
expect(s3Module.downloadFile).not.toHaveBeenCalled();
|
|
2974
2986
|
expect(fs.existsSync(escapedPath)).toBe(false);
|
|
2975
2987
|
expect(fs.existsSync(path.join(companyRoot, remoteKey))).toBe(false);
|
|
2976
|
-
const journal =
|
|
2988
|
+
const journal = journalAt(journalPath);
|
|
2977
2989
|
expect(journal.files[remoteKey]).toBeUndefined();
|
|
2978
2990
|
}
|
|
2979
2991
|
finally {
|
|
@@ -3048,7 +3060,7 @@ describe("sync", () => {
|
|
|
3048
3060
|
vaultConfig: mockConfig,
|
|
3049
3061
|
hqRoot: tmpDir,
|
|
3050
3062
|
});
|
|
3051
|
-
const journal =
|
|
3063
|
+
const journal = journalAt(journalPath);
|
|
3052
3064
|
expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
|
|
3053
3065
|
expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
|
|
3054
3066
|
});
|
|
@@ -3069,7 +3081,7 @@ describe("sync", () => {
|
|
|
3069
3081
|
if (key === "bulk/file-8.md") {
|
|
3070
3082
|
// This models the next transfer crashing the process. The first
|
|
3071
3083
|
// eight entries must already be durable before the final writer.
|
|
3072
|
-
const checkpoint =
|
|
3084
|
+
const checkpoint = journalAt(journalPath);
|
|
3073
3085
|
expect(Object.keys(checkpoint.files)).toHaveLength(8);
|
|
3074
3086
|
throw new Error("simulated crash after checkpoint");
|
|
3075
3087
|
}
|
|
@@ -3083,7 +3095,7 @@ describe("sync", () => {
|
|
|
3083
3095
|
onEvent: () => undefined,
|
|
3084
3096
|
});
|
|
3085
3097
|
expect(first.filesDownloaded).toBe(8);
|
|
3086
|
-
const checkpoint =
|
|
3098
|
+
const checkpoint = journalAt(journalPath);
|
|
3087
3099
|
expect(Object.keys(checkpoint.files)).toHaveLength(8);
|
|
3088
3100
|
vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
|
|
3089
3101
|
vi.mocked(s3Module.downloadFile).mockClear();
|
|
@@ -3534,7 +3546,7 @@ describe("sync", () => {
|
|
|
3534
3546
|
// skip-unchanged works on the next tick.
|
|
3535
3547
|
const journalMod = await import("../journal.js");
|
|
3536
3548
|
const expectedHash = journalMod.hashSymlinkTarget(linkTarget);
|
|
3537
|
-
const journal =
|
|
3549
|
+
const journal = journalAt(journalPath);
|
|
3538
3550
|
expect(journal.files[linkKey]).toMatchObject({
|
|
3539
3551
|
hash: expectedHash,
|
|
3540
3552
|
size: 0,
|
|
@@ -4181,7 +4193,7 @@ describe("sync", () => {
|
|
|
4181
4193
|
// The journal MUST be updated to reflect the user's keep
|
|
4182
4194
|
// decision. Pre-fix: catch swallowed the ENOENT, journal stayed
|
|
4183
4195
|
// at oldHash + old etag, every subsequent sync re-fired.
|
|
4184
|
-
const journal =
|
|
4196
|
+
const journal = journalAt(journalPath);
|
|
4185
4197
|
const entry = journal.files[linkKey];
|
|
4186
4198
|
expect(entry.hash).toBe(expectedNewHash);
|
|
4187
4199
|
expect(entry.size).toBe(0);
|
|
@@ -4480,7 +4492,7 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4480
4492
|
// Cleaned, not re-uploaded: local doubled-tree copy gone, journal dropped.
|
|
4481
4493
|
expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
|
|
4482
4494
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
4483
|
-
const journal =
|
|
4495
|
+
const journal = journalAt(journalPath);
|
|
4484
4496
|
expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
|
|
4485
4497
|
// No HEAD verify against the invalid key — the server validator would
|
|
4486
4498
|
// reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
|
|
@@ -4608,7 +4620,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4608
4620
|
expect(events.filter((e) => e.type === "error")).toHaveLength(0);
|
|
4609
4621
|
expect(result.aborted).toBe(false);
|
|
4610
4622
|
expect(result.filesSkipped).toBe(1);
|
|
4611
|
-
const journalAfter =
|
|
4623
|
+
const journalAfter = journalAt(journalPath);
|
|
4612
4624
|
expect(Object.keys(journalAfter.files)).not.toContain(junkKey);
|
|
4613
4625
|
});
|
|
4614
4626
|
it("still pulls a junk-spelling key that is ALREADY journaled under that exact spelling", async () => {
|
|
@@ -4653,7 +4665,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4653
4665
|
// Junk twin still gated; the canonical journal entry tombstones out.
|
|
4654
4666
|
expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
|
|
4655
4667
|
expect(result.filesTombstoned).toBe(1);
|
|
4656
|
-
const journalAfter =
|
|
4668
|
+
const journalAfter = journalAt(journalPath);
|
|
4657
4669
|
expect(Object.keys(journalAfter.files)).not.toContain(canonicalKey);
|
|
4658
4670
|
});
|
|
4659
4671
|
it("always downloads a CANONICAL remote key even when the journal holds only its junk twin (post-doctor convergence)", async () => {
|
|
@@ -4670,7 +4682,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4670
4682
|
expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
|
|
4671
4683
|
expect(vi.mocked(s3Module.downloadFile).mock.calls[0][1]).toBe(canonicalKey);
|
|
4672
4684
|
expect(result.filesDownloaded).toBe(1);
|
|
4673
|
-
const journalAfter =
|
|
4685
|
+
const journalAfter = journalAt(journalPath);
|
|
4674
4686
|
expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
|
|
4675
4687
|
});
|
|
4676
4688
|
it("does not tombstone a journaled canonical key when the LIST holds only its junk twin", async () => {
|
|
@@ -4686,7 +4698,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4686
4698
|
const { events, result } = await runSync();
|
|
4687
4699
|
// Junk twin skipped, canonical entry NOT tombstoned.
|
|
4688
4700
|
expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
|
|
4689
|
-
const journalAfter =
|
|
4701
|
+
const journalAfter = journalAt(journalPath);
|
|
4690
4702
|
expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
|
|
4691
4703
|
expect(result.filesTombstoned ?? 0).toBe(0);
|
|
4692
4704
|
expect(events.filter((e) => e.type === "error")).toHaveLength(0);
|