@indigoai-us/hq-cloud 6.15.19 → 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 +2 -2
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +43 -8
- 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/conflict.d.ts +25 -2
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +29 -4
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +90 -6
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +434 -104
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +9 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +95 -46
- 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,13 +532,50 @@ 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
|
|
523
539
|
// delete cannot fire on a journal that lies about local==remote.
|
|
524
540
|
expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
|
|
525
541
|
});
|
|
542
|
+
// `publish-local` reaches the PULL leg too, because the strategy is set once
|
|
543
|
+
// for a bidirectional run. It has no meaning here — there is nothing to
|
|
544
|
+
// publish while deciding what to do with an incoming object — so it MUST
|
|
545
|
+
// degrade to `keep`. If it instead fell through to the download branch it
|
|
546
|
+
// would overwrite the very local body the operator elected to publish, which
|
|
547
|
+
// is precisely the data loss the strategy exists to prevent.
|
|
548
|
+
it("conflict publish-local on the pull leg keeps local and never downloads over it", async () => {
|
|
549
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
550
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
551
|
+
fs.writeFileSync(path.join(companyDocs, "handoff.md"), "my elected local version");
|
|
552
|
+
fs.writeFileSync(journalPath, JSON.stringify({
|
|
553
|
+
version: "1",
|
|
554
|
+
lastSync: new Date().toISOString(),
|
|
555
|
+
files: {
|
|
556
|
+
"docs/handoff.md": {
|
|
557
|
+
hash: "old-hash-from-last-sync",
|
|
558
|
+
size: 20,
|
|
559
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
560
|
+
direction: "down",
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
}));
|
|
564
|
+
const result = await sync({
|
|
565
|
+
company: "acme",
|
|
566
|
+
onConflict: "publish-local",
|
|
567
|
+
vaultConfig: mockConfig,
|
|
568
|
+
hqRoot: tmpDir,
|
|
569
|
+
});
|
|
570
|
+
expect(result.conflicts).toBe(1);
|
|
571
|
+
expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("my elected local version");
|
|
572
|
+
// Identical bookkeeping to `keep`: the etag is stamped so the conflict
|
|
573
|
+
// cannot re-fire, and the honesty flag is armed so the push leg knows this
|
|
574
|
+
// is the divergent body it may promote under an If-Match fence.
|
|
575
|
+
const journal = journalAt(journalPath);
|
|
576
|
+
expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
|
|
577
|
+
expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
|
|
578
|
+
});
|
|
526
579
|
it("a genuine download clears a stale localDiverges flag (entry is now truly local==remote)", async () => {
|
|
527
580
|
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
528
581
|
fs.mkdirSync(companyDocs, { recursive: true });
|
|
@@ -548,7 +601,7 @@ describe("sync", () => {
|
|
|
548
601
|
]);
|
|
549
602
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
550
603
|
expect(fs.existsSync(path.join(companyDocs, "handoff.md"))).toBe(true);
|
|
551
|
-
const journal =
|
|
604
|
+
const journal = journalAt(journalPath);
|
|
552
605
|
expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
|
|
553
606
|
});
|
|
554
607
|
it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
|
|
@@ -587,7 +640,7 @@ describe("sync", () => {
|
|
|
587
640
|
samplePaths: [linkKey],
|
|
588
641
|
});
|
|
589
642
|
expect(events.some((event) => event.type === "error")).toBe(false);
|
|
590
|
-
const journal =
|
|
643
|
+
const journal = journalAt(journalPath);
|
|
591
644
|
expect(journal.files[linkKey]).toBeUndefined();
|
|
592
645
|
}
|
|
593
646
|
finally {
|
|
@@ -685,7 +738,7 @@ describe("sync", () => {
|
|
|
685
738
|
expect(litter).toEqual([]);
|
|
686
739
|
// Journal re-stamped off the stale baseline (hash now matches local,
|
|
687
740
|
// remoteEtag recorded) so the phantom conflict can't re-fire.
|
|
688
|
-
const journalAfter =
|
|
741
|
+
const journalAfter = journalAt(journalPath);
|
|
689
742
|
expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
|
|
690
743
|
expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
|
|
691
744
|
});
|
|
@@ -743,7 +796,7 @@ describe("sync", () => {
|
|
|
743
796
|
});
|
|
744
797
|
expect(events.some((event) => event.type === "error")).toBe(false);
|
|
745
798
|
expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
|
|
746
|
-
const journal =
|
|
799
|
+
const journal = journalAt(journalPath);
|
|
747
800
|
expect(journal.files[linkKey].hash).toBe("stale-hash");
|
|
748
801
|
}
|
|
749
802
|
finally {
|
|
@@ -1131,7 +1184,7 @@ describe("sync", () => {
|
|
|
1131
1184
|
.filter((f) => f.includes(".conflict-"));
|
|
1132
1185
|
expect(litter).toHaveLength(0);
|
|
1133
1186
|
// Journal stamped, so the next pass is a plain unchanged-skip.
|
|
1134
|
-
const journal =
|
|
1187
|
+
const journal = journalAt(journalPath);
|
|
1135
1188
|
expect(journal.files["docs/handoff.md"]).toBeDefined();
|
|
1136
1189
|
});
|
|
1137
1190
|
it("RF-F02EXEC: refuses a download whose parent symlink appears after planning", async () => {
|
|
@@ -1291,7 +1344,7 @@ describe("sync", () => {
|
|
|
1291
1344
|
});
|
|
1292
1345
|
expect(fs.readFileSync(untrackedPath, "utf-8")).toBe("brand new local work");
|
|
1293
1346
|
expect(fs.existsSync(trackedPath)).toBe(false);
|
|
1294
|
-
const journal =
|
|
1347
|
+
const journal = journalAt(journalPath);
|
|
1295
1348
|
expect(journal.files[untrackedKey]).toBeUndefined();
|
|
1296
1349
|
expect(journal.files[trackedKey]).toBeUndefined();
|
|
1297
1350
|
});
|
|
@@ -1364,7 +1417,7 @@ describe("sync", () => {
|
|
|
1364
1417
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1365
1418
|
expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
|
|
1366
1419
|
expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
|
|
1367
|
-
const journal =
|
|
1420
|
+
const journal = journalAt(journalPath);
|
|
1368
1421
|
expect(journal.files[key]?.removedAt).toBeUndefined();
|
|
1369
1422
|
});
|
|
1370
1423
|
it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
|
|
@@ -1412,7 +1465,7 @@ describe("sync", () => {
|
|
|
1412
1465
|
// Equivalence: an ignored key is never tombstoned — the local file and its
|
|
1413
1466
|
// journal entry both survive, exactly as before the fix.
|
|
1414
1467
|
expect(fs.existsSync(ignoredLocal)).toBe(true);
|
|
1415
|
-
const sweptJournal =
|
|
1468
|
+
const sweptJournal = journalAt(journalPath);
|
|
1416
1469
|
expect(sweptJournal.files[ignoredKey]).toBeDefined();
|
|
1417
1470
|
});
|
|
1418
1471
|
it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
|
|
@@ -1446,7 +1499,7 @@ describe("sync", () => {
|
|
|
1446
1499
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1447
1500
|
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
|
|
1448
1501
|
expect(fs.existsSync(localPath)).toBe(false);
|
|
1449
|
-
expect(
|
|
1502
|
+
expect(journalAt(journalPath).files[key]
|
|
1450
1503
|
?.removedReason).toBe("local-delete");
|
|
1451
1504
|
});
|
|
1452
1505
|
it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
|
|
@@ -1482,7 +1535,7 @@ describe("sync", () => {
|
|
|
1482
1535
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1483
1536
|
// All restored, none tombstoned.
|
|
1484
1537
|
expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
|
|
1485
|
-
const journal =
|
|
1538
|
+
const journal = journalAt(journalPath);
|
|
1486
1539
|
for (const k of keys) {
|
|
1487
1540
|
expect(journal.files[k]?.removedAt).toBeUndefined();
|
|
1488
1541
|
}
|
|
@@ -1523,7 +1576,7 @@ describe("sync", () => {
|
|
|
1523
1576
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1524
1577
|
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
|
|
1525
1578
|
expect(fs.existsSync(localPath)).toBe(false);
|
|
1526
|
-
const journal =
|
|
1579
|
+
const journal = journalAt(journalPath);
|
|
1527
1580
|
// Entry PRESERVED as a tombstone, not removed.
|
|
1528
1581
|
expect(journal.files[key]?.removedAt).toBeTruthy();
|
|
1529
1582
|
expect(journal.files[key]?.removedReason).toBe("local-delete");
|
|
@@ -1555,7 +1608,7 @@ describe("sync", () => {
|
|
|
1555
1608
|
it("stamps journal.lastSync on every successful run, even when nothing transferred", async () => {
|
|
1556
1609
|
// First run downloads both remote files and stamps lastSync.
|
|
1557
1610
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1558
|
-
const firstStamp =
|
|
1611
|
+
const firstStamp = journalAt(journalPath).lastSync;
|
|
1559
1612
|
expect(firstStamp).not.toBe("");
|
|
1560
1613
|
// Second run: same remote, same local — Stage-2 plan is all-skip, so
|
|
1561
1614
|
// updateEntry never fires. lastSync must still advance, otherwise the
|
|
@@ -1563,7 +1616,7 @@ describe("sync", () => {
|
|
|
1563
1616
|
await new Promise((r) => setTimeout(r, 5));
|
|
1564
1617
|
const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1565
1618
|
expect(result.filesDownloaded).toBe(0);
|
|
1566
|
-
const secondStamp =
|
|
1619
|
+
const secondStamp = journalAt(journalPath).lastSync;
|
|
1567
1620
|
expect(new Date(secondStamp).getTime()).toBeGreaterThan(new Date(firstStamp).getTime());
|
|
1568
1621
|
});
|
|
1569
1622
|
it("journalSlug: 'personal' routes journal I/O to sync-journal.personal.json", async () => {
|
|
@@ -1654,13 +1707,9 @@ describe("sync", () => {
|
|
|
1654
1707
|
expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "bar.md"))).toBe(false);
|
|
1655
1708
|
// The whole point: it now gets a journal baseline, so the push side stops
|
|
1656
1709
|
// re-firing a transient conflict every sync (the bug this fix closes).
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
.some((f) => {
|
|
1661
|
-
const j = JSON.parse(fs.readFileSync(path.join(stateDir, f), "utf8"));
|
|
1662
|
-
return j.files?.["companies/manifest.yaml"] != null;
|
|
1663
|
-
});
|
|
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);
|
|
1664
1713
|
expect(journaledManifest).toBe(true);
|
|
1665
1714
|
});
|
|
1666
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 () => {
|
|
@@ -1913,7 +1962,7 @@ describe("sync", () => {
|
|
|
1913
1962
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
1914
1963
|
// Journal entry for the deleted path must be gone too — otherwise
|
|
1915
1964
|
// the next sync would tombstone the same key on every run.
|
|
1916
|
-
const journal =
|
|
1965
|
+
const journal = journalAt(journalPath);
|
|
1917
1966
|
expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
|
|
1918
1967
|
expect(journal.files["docs/kept.md"]).toBeDefined();
|
|
1919
1968
|
});
|
|
@@ -1978,7 +2027,7 @@ describe("sync", () => {
|
|
|
1978
2027
|
expect(result.filesTombstoned).toBe(0);
|
|
1979
2028
|
// The journal is healed: the malformed key is gone, the canonical POSIX key
|
|
1980
2029
|
// points at the live file (so a future pull stays converged, not poisoned).
|
|
1981
|
-
const journal =
|
|
2030
|
+
const journal = journalAt(journalPath);
|
|
1982
2031
|
expect(journal.files[backslashKey]).toBeUndefined();
|
|
1983
2032
|
expect(journal.files[posixKey]).toBeDefined();
|
|
1984
2033
|
expect(Object.keys(journal.files).some((k) => k.includes("\\"))).toBe(false);
|
|
@@ -2031,7 +2080,7 @@ describe("sync", () => {
|
|
|
2031
2080
|
expect(result.filesTombstoned).toBe(0);
|
|
2032
2081
|
// Journal entry retained — next sync (push) will re-evaluate and
|
|
2033
2082
|
// either re-upload the divergent local or surface as a conflict.
|
|
2034
|
-
const journal =
|
|
2083
|
+
const journal = journalAt(journalPath);
|
|
2035
2084
|
expect(journal.files["docs/edited-locally.md"]).toBeDefined();
|
|
2036
2085
|
expect(journal.files["docs/edited-locally.md"].hash).toBe(baselineHash);
|
|
2037
2086
|
});
|
|
@@ -2075,7 +2124,7 @@ describe("sync", () => {
|
|
|
2075
2124
|
expect(result.filesTombstoned).toBe(0);
|
|
2076
2125
|
expect(fs.existsSync(racedPath)).toBe(true);
|
|
2077
2126
|
expect(fs.readFileSync(racedPath, "utf-8")).toBe("concurrent local edit");
|
|
2078
|
-
const journal =
|
|
2127
|
+
const journal = journalAt(journalPath);
|
|
2079
2128
|
expect(journal.files["docs/racy-delete.md"]).toBeDefined();
|
|
2080
2129
|
expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
|
|
2081
2130
|
});
|
|
@@ -2112,7 +2161,7 @@ describe("sync", () => {
|
|
|
2112
2161
|
expect(result.filesTombstoned).toBe(0);
|
|
2113
2162
|
expect(result.conflicts).toBe(1);
|
|
2114
2163
|
expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
|
|
2115
|
-
const journal =
|
|
2164
|
+
const journal = journalAt(journalPath);
|
|
2116
2165
|
expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
|
|
2117
2166
|
});
|
|
2118
2167
|
it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
|
|
@@ -2196,7 +2245,7 @@ describe("sync", () => {
|
|
|
2196
2245
|
propagateDeletePolicy: "all",
|
|
2197
2246
|
});
|
|
2198
2247
|
expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
|
|
2199
|
-
const journal =
|
|
2248
|
+
const journal = journalAt(personalJournalPath);
|
|
2200
2249
|
expect(journal.files["core/core.yaml"]).toBeDefined();
|
|
2201
2250
|
expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
|
|
2202
2251
|
});
|
|
@@ -2230,7 +2279,7 @@ describe("sync", () => {
|
|
|
2230
2279
|
});
|
|
2231
2280
|
expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
|
|
2232
2281
|
expect(result.filesRefusedStale).toBe(2);
|
|
2233
|
-
const journal =
|
|
2282
|
+
const journal = journalAt(journalPath);
|
|
2234
2283
|
expect(journal.files["docs/a.md"]).toBeDefined();
|
|
2235
2284
|
expect(journal.files["docs/b.md"]).toBeDefined();
|
|
2236
2285
|
});
|
|
@@ -2322,7 +2371,7 @@ describe("sync", () => {
|
|
|
2322
2371
|
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
|
2323
2372
|
expect(fs.readlinkSync(linkPath)).toBe("new-target.md");
|
|
2324
2373
|
expect(result.filesTombstoned).toBe(0);
|
|
2325
|
-
const journal =
|
|
2374
|
+
const journal = journalAt(journalPath);
|
|
2326
2375
|
expect(journal.files["docs/link.md"]).toBeDefined();
|
|
2327
2376
|
expect(journal.files["docs/link.md"].hash).toBe(baselineHash);
|
|
2328
2377
|
});
|
|
@@ -2389,7 +2438,7 @@ describe("sync", () => {
|
|
|
2389
2438
|
// The really-deleted file MUST be tombstoned.
|
|
2390
2439
|
expect(fs.existsSync(reallyDeletedPath)).toBe(false);
|
|
2391
2440
|
expect(result.filesTombstoned).toBe(1);
|
|
2392
|
-
const journal =
|
|
2441
|
+
const journal = journalAt(journalPath);
|
|
2393
2442
|
// Journal entry retained for the invisible key (so next sync at
|
|
2394
2443
|
// broader scope can re-evaluate).
|
|
2395
2444
|
expect(journal.files["docs/invisible-to-list.md"]).toBeDefined();
|
|
@@ -2463,7 +2512,7 @@ describe("sync", () => {
|
|
|
2463
2512
|
expect(result.filesDownloaded).toBe(0);
|
|
2464
2513
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
2465
2514
|
// Journal entry dropped so it converges (no re-tombstone next run).
|
|
2466
|
-
const journal =
|
|
2515
|
+
const journal = journalAt(journalPath);
|
|
2467
2516
|
expect(journal.files["docs/shared.md"]).toBeUndefined();
|
|
2468
2517
|
});
|
|
2469
2518
|
it("delete-resync: a re-created object NEWER than the tombstone DOES sync (tombstone is not permanent)", async () => {
|
|
@@ -2627,7 +2676,7 @@ describe("sync", () => {
|
|
|
2627
2676
|
});
|
|
2628
2677
|
expect(fs.existsSync(deniedPath)).toBe(true);
|
|
2629
2678
|
expect(result.filesTombstoned).toBe(0);
|
|
2630
|
-
const journal =
|
|
2679
|
+
const journal = journalAt(journalPath);
|
|
2631
2680
|
expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
|
|
2632
2681
|
});
|
|
2633
2682
|
it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
|
|
@@ -2675,7 +2724,7 @@ describe("sync", () => {
|
|
|
2675
2724
|
// Tombstone NOT counted — the delete wasn't honored.
|
|
2676
2725
|
expect(result.filesTombstoned).toBe(0);
|
|
2677
2726
|
// Journal entry MUST be retained so the next sync retries.
|
|
2678
|
-
const journal =
|
|
2727
|
+
const journal = journalAt(journalPath);
|
|
2679
2728
|
expect(journal.files["docs/locked-by-peer.md"]).toBeDefined();
|
|
2680
2729
|
expect(journal.files["docs/locked-by-peer.md"].hash).toBe("locked-hash");
|
|
2681
2730
|
}
|
|
@@ -2902,7 +2951,7 @@ describe("sync", () => {
|
|
|
2902
2951
|
expect(s3Module.downloadFile).not.toHaveBeenCalled();
|
|
2903
2952
|
expect(fs.existsSync(escapedPath)).toBe(false);
|
|
2904
2953
|
expect(fs.existsSync(path.join(companyRoot, traversalKey))).toBe(false);
|
|
2905
|
-
const journal =
|
|
2954
|
+
const journal = journalAt(journalPath);
|
|
2906
2955
|
expect(journal.files[traversalKey]).toBeUndefined();
|
|
2907
2956
|
}
|
|
2908
2957
|
finally {
|
|
@@ -2936,7 +2985,7 @@ describe("sync", () => {
|
|
|
2936
2985
|
expect(s3Module.downloadFile).not.toHaveBeenCalled();
|
|
2937
2986
|
expect(fs.existsSync(escapedPath)).toBe(false);
|
|
2938
2987
|
expect(fs.existsSync(path.join(companyRoot, remoteKey))).toBe(false);
|
|
2939
|
-
const journal =
|
|
2988
|
+
const journal = journalAt(journalPath);
|
|
2940
2989
|
expect(journal.files[remoteKey]).toBeUndefined();
|
|
2941
2990
|
}
|
|
2942
2991
|
finally {
|
|
@@ -3011,7 +3060,7 @@ describe("sync", () => {
|
|
|
3011
3060
|
vaultConfig: mockConfig,
|
|
3012
3061
|
hqRoot: tmpDir,
|
|
3013
3062
|
});
|
|
3014
|
-
const journal =
|
|
3063
|
+
const journal = journalAt(journalPath);
|
|
3015
3064
|
expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
|
|
3016
3065
|
expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
|
|
3017
3066
|
});
|
|
@@ -3032,7 +3081,7 @@ describe("sync", () => {
|
|
|
3032
3081
|
if (key === "bulk/file-8.md") {
|
|
3033
3082
|
// This models the next transfer crashing the process. The first
|
|
3034
3083
|
// eight entries must already be durable before the final writer.
|
|
3035
|
-
const checkpoint =
|
|
3084
|
+
const checkpoint = journalAt(journalPath);
|
|
3036
3085
|
expect(Object.keys(checkpoint.files)).toHaveLength(8);
|
|
3037
3086
|
throw new Error("simulated crash after checkpoint");
|
|
3038
3087
|
}
|
|
@@ -3046,7 +3095,7 @@ describe("sync", () => {
|
|
|
3046
3095
|
onEvent: () => undefined,
|
|
3047
3096
|
});
|
|
3048
3097
|
expect(first.filesDownloaded).toBe(8);
|
|
3049
|
-
const checkpoint =
|
|
3098
|
+
const checkpoint = journalAt(journalPath);
|
|
3050
3099
|
expect(Object.keys(checkpoint.files)).toHaveLength(8);
|
|
3051
3100
|
vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
|
|
3052
3101
|
vi.mocked(s3Module.downloadFile).mockClear();
|
|
@@ -3497,7 +3546,7 @@ describe("sync", () => {
|
|
|
3497
3546
|
// skip-unchanged works on the next tick.
|
|
3498
3547
|
const journalMod = await import("../journal.js");
|
|
3499
3548
|
const expectedHash = journalMod.hashSymlinkTarget(linkTarget);
|
|
3500
|
-
const journal =
|
|
3549
|
+
const journal = journalAt(journalPath);
|
|
3501
3550
|
expect(journal.files[linkKey]).toMatchObject({
|
|
3502
3551
|
hash: expectedHash,
|
|
3503
3552
|
size: 0,
|
|
@@ -4144,7 +4193,7 @@ describe("sync", () => {
|
|
|
4144
4193
|
// The journal MUST be updated to reflect the user's keep
|
|
4145
4194
|
// decision. Pre-fix: catch swallowed the ENOENT, journal stayed
|
|
4146
4195
|
// at oldHash + old etag, every subsequent sync re-fired.
|
|
4147
|
-
const journal =
|
|
4196
|
+
const journal = journalAt(journalPath);
|
|
4148
4197
|
const entry = journal.files[linkKey];
|
|
4149
4198
|
expect(entry.hash).toBe(expectedNewHash);
|
|
4150
4199
|
expect(entry.size).toBe(0);
|
|
@@ -4443,7 +4492,7 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4443
4492
|
// Cleaned, not re-uploaded: local doubled-tree copy gone, journal dropped.
|
|
4444
4493
|
expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
|
|
4445
4494
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
4446
|
-
const journal =
|
|
4495
|
+
const journal = journalAt(journalPath);
|
|
4447
4496
|
expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
|
|
4448
4497
|
// No HEAD verify against the invalid key — the server validator would
|
|
4449
4498
|
// reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
|
|
@@ -4571,7 +4620,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4571
4620
|
expect(events.filter((e) => e.type === "error")).toHaveLength(0);
|
|
4572
4621
|
expect(result.aborted).toBe(false);
|
|
4573
4622
|
expect(result.filesSkipped).toBe(1);
|
|
4574
|
-
const journalAfter =
|
|
4623
|
+
const journalAfter = journalAt(journalPath);
|
|
4575
4624
|
expect(Object.keys(journalAfter.files)).not.toContain(junkKey);
|
|
4576
4625
|
});
|
|
4577
4626
|
it("still pulls a junk-spelling key that is ALREADY journaled under that exact spelling", async () => {
|
|
@@ -4616,7 +4665,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4616
4665
|
// Junk twin still gated; the canonical journal entry tombstones out.
|
|
4617
4666
|
expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
|
|
4618
4667
|
expect(result.filesTombstoned).toBe(1);
|
|
4619
|
-
const journalAfter =
|
|
4668
|
+
const journalAfter = journalAt(journalPath);
|
|
4620
4669
|
expect(Object.keys(journalAfter.files)).not.toContain(canonicalKey);
|
|
4621
4670
|
});
|
|
4622
4671
|
it("always downloads a CANONICAL remote key even when the journal holds only its junk twin (post-doctor convergence)", async () => {
|
|
@@ -4633,7 +4682,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4633
4682
|
expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
|
|
4634
4683
|
expect(vi.mocked(s3Module.downloadFile).mock.calls[0][1]).toBe(canonicalKey);
|
|
4635
4684
|
expect(result.filesDownloaded).toBe(1);
|
|
4636
|
-
const journalAfter =
|
|
4685
|
+
const journalAfter = journalAt(journalPath);
|
|
4637
4686
|
expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
|
|
4638
4687
|
});
|
|
4639
4688
|
it("does not tombstone a journaled canonical key when the LIST holds only its junk twin", async () => {
|
|
@@ -4649,7 +4698,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
|
|
|
4649
4698
|
const { events, result } = await runSync();
|
|
4650
4699
|
// Junk twin skipped, canonical entry NOT tombstoned.
|
|
4651
4700
|
expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
|
|
4652
|
-
const journalAfter =
|
|
4701
|
+
const journalAfter = journalAt(journalPath);
|
|
4653
4702
|
expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
|
|
4654
4703
|
expect(result.filesTombstoned ?? 0).toBe(0);
|
|
4655
4704
|
expect(events.filter((e) => e.type === "error")).toHaveLength(0);
|