@indigoai-us/hq-cloud 6.15.24 → 6.15.26
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/bandwidth.d.ts +128 -0
- package/dist/bandwidth.d.ts.map +1 -0
- package/dist/bandwidth.js +254 -0
- package/dist/bandwidth.js.map +1 -0
- package/dist/bandwidth.test.d.ts +2 -0
- package/dist/bandwidth.test.d.ts.map +1 -0
- package/dist/bandwidth.test.js +243 -0
- package/dist/bandwidth.test.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +10 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +17 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +58 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +5 -4
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +87 -15
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +125 -50
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +99 -20
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +327 -40
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +13 -2
- package/dist/object-io.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +7 -2
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-core.test.js +36 -0
- package/dist/sync-core.test.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -88,6 +88,14 @@ function journalAt(journalPath) {
|
|
|
88
88
|
.replace(/\.json$/, "");
|
|
89
89
|
return readJournal(slug);
|
|
90
90
|
}
|
|
91
|
+
function sidecarIn(dir) {
|
|
92
|
+
const found = fs.readdirSync(dir).filter((name) => name.includes(".conflict-"));
|
|
93
|
+
expect(found).toHaveLength(1);
|
|
94
|
+
return path.join(dir, found[0]);
|
|
95
|
+
}
|
|
96
|
+
function conflictIndexAt(hqRoot) {
|
|
97
|
+
return JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq-conflicts", "index.json"), "utf-8"));
|
|
98
|
+
}
|
|
91
99
|
const mockConfig = {
|
|
92
100
|
apiUrl: "https://vault-api.test",
|
|
93
101
|
authToken: "test-jwt-token",
|
|
@@ -743,7 +751,7 @@ describe("share", () => {
|
|
|
743
751
|
expect(entry.ctimeMs).toBe(stat.ctimeMs);
|
|
744
752
|
});
|
|
745
753
|
it("never lets the fast path skip a localDiverges entry", async () => {
|
|
746
|
-
// A pull-
|
|
754
|
+
// A pull-held divergent entry stores the LOCAL hash and local stat, so
|
|
747
755
|
// every field matches by construction. Skipping on that asserts "local
|
|
748
756
|
// matches remote", which is exactly what the flag exists to deny.
|
|
749
757
|
const abs = writeFile("divergent.md", CONTENT_A);
|
|
@@ -988,6 +996,10 @@ describe("share", () => {
|
|
|
988
996
|
etag: '"remote-new-etag"',
|
|
989
997
|
size: 99,
|
|
990
998
|
});
|
|
999
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
1000
|
+
fs.writeFileSync(dest, "remote edit");
|
|
1001
|
+
return undefined;
|
|
1002
|
+
});
|
|
991
1003
|
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
992
1004
|
seedJournalAt(journalPath, ({
|
|
993
1005
|
version: "1",
|
|
@@ -1020,6 +1032,24 @@ describe("share", () => {
|
|
|
1020
1032
|
direction: "push",
|
|
1021
1033
|
resolution: "keep",
|
|
1022
1034
|
});
|
|
1035
|
+
// `keep` is cloud-wins on the push leg too: remote takes the working
|
|
1036
|
+
// name, while the displaced local body remains recoverable beside it.
|
|
1037
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("remote edit");
|
|
1038
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local edit");
|
|
1039
|
+
const journal = journalAt(journalPath);
|
|
1040
|
+
expect(journal.files["drifted.md"]).toMatchObject({
|
|
1041
|
+
hash: hashFile(testFile),
|
|
1042
|
+
remoteEtag: "remote-new-etag",
|
|
1043
|
+
direction: "down",
|
|
1044
|
+
});
|
|
1045
|
+
const index = conflictIndexAt(tmpDir);
|
|
1046
|
+
expect(index.conflicts).toHaveLength(1);
|
|
1047
|
+
expect(index.conflicts[0]).toMatchObject({
|
|
1048
|
+
originalPath: "companies/acme/drifted.md",
|
|
1049
|
+
preserved: "local",
|
|
1050
|
+
});
|
|
1051
|
+
expect(fs.readFileSync(path.join(tmpDir, index.conflicts[0].conflictPath), "utf-8"))
|
|
1052
|
+
.toBe("local edit");
|
|
1023
1053
|
});
|
|
1024
1054
|
it("cloud-authoritative: never pushes a stale local ontology/.last-run over the server value (one-sided-change clobber guard)", async () => {
|
|
1025
1055
|
// Regression for the gardener watermark clobber. A second HQ root sharing this
|
|
@@ -1087,9 +1117,9 @@ describe("share", () => {
|
|
|
1087
1117
|
//
|
|
1088
1118
|
// Fix: even on first-time upload, if HEAD shows remote exists and
|
|
1089
1119
|
// its content (etag) differs from what we'd put, emit a conflict.
|
|
1090
|
-
// Under keep, the
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1120
|
+
// Under keep, cloud takes the original name and local moves to
|
|
1121
|
+
// \`<orig>.conflict-<ts>-<short>\` so both versions survive on disk;
|
|
1122
|
+
// under abort, the company aborts before PUT.
|
|
1093
1123
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1094
1124
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1095
1125
|
const testFile = path.join(companyRoot, "v7b-fresh.txt");
|
|
@@ -1101,6 +1131,10 @@ describe("share", () => {
|
|
|
1101
1131
|
etag: '"remote-version-etag"',
|
|
1102
1132
|
size: 40,
|
|
1103
1133
|
});
|
|
1134
|
+
vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
|
|
1135
|
+
fs.writeFileSync(dest, "peer remote content");
|
|
1136
|
+
return undefined;
|
|
1137
|
+
});
|
|
1104
1138
|
// No journal file written — first-time upload contract.
|
|
1105
1139
|
const events = [];
|
|
1106
1140
|
const result = await share({
|
|
@@ -1112,14 +1146,19 @@ describe("share", () => {
|
|
|
1112
1146
|
onEvent: (e) => events.push(e),
|
|
1113
1147
|
});
|
|
1114
1148
|
expect(result.conflictPaths).toEqual(["v7b-fresh.txt"]);
|
|
1115
|
-
// Under keep,
|
|
1116
|
-
// the
|
|
1149
|
+
// Under keep, no upload occurs: the remote bytes take the working name
|
|
1150
|
+
// and the local body is preserved beside them.
|
|
1117
1151
|
expect(result.filesUploaded).toBe(0);
|
|
1118
1152
|
const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
|
|
1119
1153
|
expect(conflicts).toHaveLength(1);
|
|
1120
1154
|
expect(conflicts[0].path).toBe("v7b-fresh.txt");
|
|
1121
|
-
|
|
1122
|
-
expect(fs.readFileSync(
|
|
1155
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote content");
|
|
1156
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("macos-fresh-collision-content");
|
|
1157
|
+
const index = conflictIndexAt(tmpDir);
|
|
1158
|
+
expect(index.conflicts[0]).toMatchObject({
|
|
1159
|
+
originalPath: "companies/acme/v7b-fresh.txt",
|
|
1160
|
+
preserved: "local",
|
|
1161
|
+
});
|
|
1123
1162
|
});
|
|
1124
1163
|
it("first-time-upload-with-cloud-collision: aborts company under --on-conflict abort (Bug #7)", async () => {
|
|
1125
1164
|
// Symmetric to the keep case: when --on-conflict abort is passed, a
|
|
@@ -1147,11 +1186,11 @@ describe("share", () => {
|
|
|
1147
1186
|
expect(result.filesUploaded).toBe(0);
|
|
1148
1187
|
});
|
|
1149
1188
|
it("HQ-DESKTOP-54: a privilege refusal while writing the push conflict mirror is a skip, not an error", async () => {
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1189
|
+
// Push keep also materializes the remote object (at the original name).
|
|
1190
|
+
// On an unprivileged Windows host that throws the typed privilege
|
|
1152
1191
|
// error; it must be downgraded to the deliberate skip so the push stays
|
|
1153
1192
|
// complete and the runner exits 0 — never type:error. The conflict itself
|
|
1154
|
-
// is still
|
|
1193
|
+
// is still surfaced and the local rename is rolled back.
|
|
1155
1194
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1156
1195
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1157
1196
|
const testFile = path.join(companyRoot, "v7b-priv.txt");
|
|
@@ -1162,7 +1201,7 @@ describe("share", () => {
|
|
|
1162
1201
|
size: 40,
|
|
1163
1202
|
});
|
|
1164
1203
|
// On a real unprivileged host BOTH the fresh-collision reconciled probe and
|
|
1165
|
-
// the keep
|
|
1204
|
+
// the keep adoption materialize the remote symlink, so both downloadFile calls
|
|
1166
1205
|
// refuse. The probe's catch returns "differs" (conflict) without erroring;
|
|
1167
1206
|
// the mirror must downgrade the refusal to the deliberate skip.
|
|
1168
1207
|
vi.mocked(downloadFile)
|
|
@@ -1183,7 +1222,7 @@ describe("share", () => {
|
|
|
1183
1222
|
target: "../target.md",
|
|
1184
1223
|
}));
|
|
1185
1224
|
expect(events.some((e) => e.type === "error")).toBe(false);
|
|
1186
|
-
// The conflict is still
|
|
1225
|
+
// The conflict is still surfaced; the failed adoption restores local.
|
|
1187
1226
|
expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
|
|
1188
1227
|
expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
|
|
1189
1228
|
});
|
|
@@ -1295,7 +1334,7 @@ describe("share", () => {
|
|
|
1295
1334
|
etag: '"0123456789abcdef0123456789abcdef-3"',
|
|
1296
1335
|
size: 32,
|
|
1297
1336
|
});
|
|
1298
|
-
// The probe (and the subsequent keep
|
|
1337
|
+
// The probe (and the subsequent keep adoption download) return DIFFERENT
|
|
1299
1338
|
// remote bytes.
|
|
1300
1339
|
vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
|
|
1301
1340
|
fs.writeFileSync(dest, "a-peers-different-version");
|
|
@@ -1314,8 +1353,8 @@ describe("share", () => {
|
|
|
1314
1353
|
expect(result.filesUploaded).toBe(0);
|
|
1315
1354
|
const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
|
|
1316
1355
|
expect(conflicts).toHaveLength(1);
|
|
1317
|
-
|
|
1318
|
-
expect(fs.readFileSync(
|
|
1356
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("a-peers-different-version");
|
|
1357
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my-local-version");
|
|
1319
1358
|
});
|
|
1320
1359
|
describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
|
|
1321
1360
|
const fence412 = () => Object.assign(new Error("Precondition failed"), {
|
|
@@ -1375,12 +1414,18 @@ describe("share", () => {
|
|
|
1375
1414
|
});
|
|
1376
1415
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
|
|
1377
1416
|
});
|
|
1378
|
-
it("412 under keep:
|
|
1417
|
+
it("412 under keep: adopts the racing remote, preserves local, and converges the journal", async () => {
|
|
1379
1418
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1380
1419
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1381
1420
|
const testFile = path.join(companyRoot, "raced.md");
|
|
1382
1421
|
fs.writeFileSync(testFile, "my local bytes");
|
|
1383
|
-
vi.mocked(headRemoteFile)
|
|
1422
|
+
vi.mocked(headRemoteFile)
|
|
1423
|
+
.mockResolvedValueOnce(null)
|
|
1424
|
+
.mockResolvedValueOnce({
|
|
1425
|
+
lastModified: new Date(),
|
|
1426
|
+
etag: '"racing-peer-etag"',
|
|
1427
|
+
size: 23,
|
|
1428
|
+
});
|
|
1384
1429
|
vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
|
|
1385
1430
|
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
1386
1431
|
fs.writeFileSync(dest, "the racing peer version");
|
|
@@ -1400,19 +1445,22 @@ describe("share", () => {
|
|
|
1400
1445
|
expect(result.filesUploaded).toBe(0);
|
|
1401
1446
|
expect(result.aborted).toBe(false);
|
|
1402
1447
|
expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
|
|
1403
|
-
// The racing remote
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
// Local copy untouched.
|
|
1409
|
-
expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
|
|
1410
|
-
// No journal stamp for the failed PUT — next pass re-evaluates fresh.
|
|
1448
|
+
// The racing remote wins the working name; local remains recoverable.
|
|
1449
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("the racing peer version");
|
|
1450
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my local bytes");
|
|
1451
|
+
// The pull-shaped result is journalled as the remote body, so the next
|
|
1452
|
+
// pass does not revive the same resolved conflict.
|
|
1411
1453
|
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1454
|
+
const journal = journalAt(journalPath);
|
|
1455
|
+
expect(journal.files["raced.md"]).toMatchObject({
|
|
1456
|
+
hash: hashFile(testFile),
|
|
1457
|
+
remoteEtag: "racing-peer-etag",
|
|
1458
|
+
direction: "down",
|
|
1459
|
+
});
|
|
1460
|
+
expect(conflictIndexAt(tmpDir).conflicts[0]).toMatchObject({
|
|
1461
|
+
originalPath: "companies/acme/raced.md",
|
|
1462
|
+
preserved: "local",
|
|
1463
|
+
});
|
|
1416
1464
|
});
|
|
1417
1465
|
it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
|
|
1418
1466
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -1491,6 +1539,10 @@ describe("share", () => {
|
|
|
1491
1539
|
}
|
|
1492
1540
|
return { etag: '"should-not-land"' };
|
|
1493
1541
|
});
|
|
1542
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
1543
|
+
fs.writeFileSync(dest, "reviewed peer body");
|
|
1544
|
+
return undefined;
|
|
1545
|
+
});
|
|
1494
1546
|
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1495
1547
|
seedJournalAt(journalPath, ({
|
|
1496
1548
|
version: "1",
|
|
@@ -1535,8 +1587,9 @@ describe("share", () => {
|
|
|
1535
1587
|
}
|
|
1536
1588
|
// No unfenced retry under keep.
|
|
1537
1589
|
expect(vi.mocked(uploadFile).mock.calls.some((c) => c[4] === undefined)).toBe(false);
|
|
1538
|
-
//
|
|
1539
|
-
expect(fs.readFileSync(testFile, "utf-8")).toBe(
|
|
1590
|
+
// Keep adopts the peer body but retains the stale local body beside it.
|
|
1591
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("reviewed peer body");
|
|
1592
|
+
expect(fs.readFileSync(sidecarIn(path.dirname(testFile)), "utf-8")).toBe(staleBytes);
|
|
1540
1593
|
});
|
|
1541
1594
|
it("legitimate local-ahead still uploads when remote remains at journal baseline", async () => {
|
|
1542
1595
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -1586,6 +1639,10 @@ describe("share", () => {
|
|
|
1586
1639
|
etag: '"remote-concurrent"',
|
|
1587
1640
|
size: 20,
|
|
1588
1641
|
});
|
|
1642
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
1643
|
+
fs.writeFileSync(dest, "remote concurrent");
|
|
1644
|
+
return undefined;
|
|
1645
|
+
});
|
|
1589
1646
|
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1590
1647
|
seedJournalAt(journalPath, ({
|
|
1591
1648
|
version: "1",
|
|
@@ -1610,12 +1667,14 @@ describe("share", () => {
|
|
|
1610
1667
|
expect(result.conflictPaths).toEqual(["both-changed.md"]);
|
|
1611
1668
|
expect(result.filesUploaded).toBe(0);
|
|
1612
1669
|
expect(uploadFile).not.toHaveBeenCalled();
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
//
|
|
1618
|
-
// the
|
|
1670
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("remote concurrent");
|
|
1671
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local concurrent");
|
|
1672
|
+
});
|
|
1673
|
+
it("localDiverges under keep: a pull-held divergent local yields to cloud without becoming write authority", async () => {
|
|
1674
|
+
// After a pull-side hold (`skip` or pull-leg `publish-local`), the journal
|
|
1675
|
+
// stamps the peer remoteEtag and localDiverges=true because local never
|
|
1676
|
+
// matched that etag. A later push must not treat "remote still at stamped
|
|
1677
|
+
// etag" as license to PUT the divergent local body.
|
|
1619
1678
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1620
1679
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1621
1680
|
const testFile = path.join(companyRoot, "kept-divergent.md");
|
|
@@ -1628,6 +1687,10 @@ describe("share", () => {
|
|
|
1628
1687
|
etag: `"${REMOTE}"`,
|
|
1629
1688
|
size: 40,
|
|
1630
1689
|
});
|
|
1690
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
1691
|
+
fs.writeFileSync(dest, "peer remote body");
|
|
1692
|
+
return undefined;
|
|
1693
|
+
});
|
|
1631
1694
|
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1632
1695
|
seedJournalAt(journalPath, ({
|
|
1633
1696
|
version: "1",
|
|
@@ -1659,7 +1722,10 @@ describe("share", () => {
|
|
|
1659
1722
|
expect(events.some((e) => e.type === "conflict" &&
|
|
1660
1723
|
e.path === "kept-divergent.md" &&
|
|
1661
1724
|
e.resolution === "keep")).toBe(true);
|
|
1662
|
-
expect(fs.readFileSync(testFile, "utf-8")).toBe(
|
|
1725
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote body");
|
|
1726
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localBytes);
|
|
1727
|
+
expect(journalAt(journalPath).files["kept-divergent.md"]?.localDiverges)
|
|
1728
|
+
.toBeFalsy();
|
|
1663
1729
|
});
|
|
1664
1730
|
it("localDiverges under overwrite: explicit intent may force-write", async () => {
|
|
1665
1731
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -1816,7 +1882,7 @@ describe("share", () => {
|
|
|
1816
1882
|
expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
|
|
1817
1883
|
});
|
|
1818
1884
|
// Codex review (PR #356, P1). The sync runner's push leg always calls
|
|
1819
|
-
// share() with `skipUnchanged: true`, and a pull-
|
|
1885
|
+
// share() with `skipUnchanged: true`, and a pull-held entry stores the
|
|
1820
1886
|
// CURRENT local hash — that is exactly what the journal-honesty stamp
|
|
1821
1887
|
// records. So `existing.hash === localHash` is true for every stuck file,
|
|
1822
1888
|
// classifyPushCandidates() returns `skip-unchanged`, and executeUploads()
|
|
@@ -1829,7 +1895,7 @@ describe("share", () => {
|
|
|
1829
1895
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1830
1896
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1831
1897
|
const testFile = path.join(companyRoot, "stuck-under-skip-unchanged.md");
|
|
1832
|
-
const localBytes = "the body a pull
|
|
1898
|
+
const localBytes = "the body a pull hold stamped and then refused to publish";
|
|
1833
1899
|
fs.writeFileSync(testFile, localBytes);
|
|
1834
1900
|
const localHash = hashFile(testFile);
|
|
1835
1901
|
const REMOTE = "etag-at-keep-time";
|
|
@@ -1845,7 +1911,7 @@ describe("share", () => {
|
|
|
1845
1911
|
lastSync: new Date().toISOString(),
|
|
1846
1912
|
files: {
|
|
1847
1913
|
"stuck-under-skip-unchanged.md": {
|
|
1848
|
-
// Deliberately the CURRENT local hash: this is what a pull
|
|
1914
|
+
// Deliberately the CURRENT local hash: this is what a pull hold
|
|
1849
1915
|
// writes, and it is what makes the fast path swallow the file.
|
|
1850
1916
|
hash: localHash,
|
|
1851
1917
|
size: localBytes.length,
|
|
@@ -2054,14 +2120,20 @@ describe("share", () => {
|
|
|
2054
2120
|
direction: "up",
|
|
2055
2121
|
});
|
|
2056
2122
|
});
|
|
2057
|
-
it("deleted journaled remote + concurrent create 412 under keep:
|
|
2123
|
+
it("deleted journaled remote + concurrent create 412 under keep: adopts the winner and preserves local", async () => {
|
|
2058
2124
|
// Same setup as recreation, but a peer creates the key between HEAD and
|
|
2059
|
-
// PUT → If-None-Match:* 412s → keep
|
|
2125
|
+
// PUT → If-None-Match:* 412s → keep adopts the peer's winning create.
|
|
2060
2126
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
2061
2127
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
2062
2128
|
const testFile = path.join(companyRoot, "race-recreate.md");
|
|
2063
2129
|
fs.writeFileSync(testFile, "my recreated local");
|
|
2064
|
-
vi.mocked(headRemoteFile)
|
|
2130
|
+
vi.mocked(headRemoteFile)
|
|
2131
|
+
.mockResolvedValueOnce(null)
|
|
2132
|
+
.mockResolvedValueOnce({
|
|
2133
|
+
lastModified: new Date(),
|
|
2134
|
+
etag: '"peer-created-etag"',
|
|
2135
|
+
size: 18,
|
|
2136
|
+
});
|
|
2065
2137
|
vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
|
|
2066
2138
|
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
2067
2139
|
fs.writeFileSync(dest, "peer created first");
|
|
@@ -2098,12 +2170,15 @@ describe("share", () => {
|
|
|
2098
2170
|
// First (only) attempt must be create-fenced; no unfenced retry under keep.
|
|
2099
2171
|
expect(uploadFile).toHaveBeenCalledTimes(1);
|
|
2100
2172
|
expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
|
|
2101
|
-
// Journal
|
|
2102
|
-
// the prior baseline — next pass re-evaluates).
|
|
2173
|
+
// Journal now describes the peer body at the original name.
|
|
2103
2174
|
const journal = journalAt(journalPath);
|
|
2104
|
-
expect(journal.files["race-recreate.md"]
|
|
2105
|
-
|
|
2106
|
-
|
|
2175
|
+
expect(journal.files["race-recreate.md"]).toMatchObject({
|
|
2176
|
+
remoteEtag: "peer-created-etag",
|
|
2177
|
+
hash: hashFile(testFile),
|
|
2178
|
+
direction: "down",
|
|
2179
|
+
});
|
|
2180
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("peer created first");
|
|
2181
|
+
expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my recreated local");
|
|
2107
2182
|
});
|
|
2108
2183
|
});
|
|
2109
2184
|
it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
|