@indigoai-us/hq-cloud 6.14.9 → 6.14.11

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.
Files changed (45) hide show
  1. package/dist/cli/share.d.ts.map +1 -1
  2. package/dist/cli/share.js +89 -14
  3. package/dist/cli/share.js.map +1 -1
  4. package/dist/cli/share.test.js +131 -3
  5. package/dist/cli/share.test.js.map +1 -1
  6. package/dist/cli/sync.d.ts +1 -1
  7. package/dist/cli/sync.d.ts.map +1 -1
  8. package/dist/cli/sync.js +138 -41
  9. package/dist/cli/sync.js.map +1 -1
  10. package/dist/cli/sync.test.js +271 -25
  11. package/dist/cli/sync.test.js.map +1 -1
  12. package/dist/journal.d.ts +7 -1
  13. package/dist/journal.d.ts.map +1 -1
  14. package/dist/journal.js +22 -1
  15. package/dist/journal.js.map +1 -1
  16. package/dist/personal-vault-exclusions.d.ts +20 -4
  17. package/dist/personal-vault-exclusions.d.ts.map +1 -1
  18. package/dist/personal-vault-exclusions.js +37 -3
  19. package/dist/personal-vault-exclusions.js.map +1 -1
  20. package/dist/personal-vault-exclusions.test.js +19 -0
  21. package/dist/personal-vault-exclusions.test.js.map +1 -1
  22. package/dist/personal-vault.d.ts +6 -0
  23. package/dist/personal-vault.d.ts.map +1 -1
  24. package/dist/personal-vault.js +12 -0
  25. package/dist/personal-vault.js.map +1 -1
  26. package/dist/s3.d.ts +23 -1
  27. package/dist/s3.d.ts.map +1 -1
  28. package/dist/s3.js +74 -3
  29. package/dist/s3.js.map +1 -1
  30. package/dist/s3.test.js +71 -1
  31. package/dist/s3.test.js.map +1 -1
  32. package/dist/types.d.ts +13 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/cli/share.test.ts +176 -3
  36. package/src/cli/share.ts +114 -15
  37. package/src/cli/sync.test.ts +330 -24
  38. package/src/cli/sync.ts +184 -50
  39. package/src/journal.ts +28 -0
  40. package/src/personal-vault-exclusions.test.ts +23 -0
  41. package/src/personal-vault-exclusions.ts +39 -3
  42. package/src/personal-vault.ts +15 -0
  43. package/src/s3.test.ts +91 -0
  44. package/src/s3.ts +88 -2
  45. package/src/types.ts +13 -0
@@ -26,9 +26,16 @@ vi.mock("../s3.js", async (importOriginal) => {
26
26
  ...actual,
27
27
  toPosixKey: (key: string) => key.split("\\").join("/"),
28
28
  uploadFile: innerVi.fn().mockResolvedValue(undefined),
29
- downloadFile: innerVi.fn().mockImplementation(async (_ctx: unknown, _key: string, localPath: string) => {
29
+ uploadSymlink: innerVi.fn().mockResolvedValue(undefined),
30
+ downloadFile: innerVi.fn().mockImplementation(async (
31
+ _ctx: unknown,
32
+ _key: string,
33
+ localPath: string,
34
+ options?: { beforeReplace?: () => void },
35
+ ) => {
30
36
  const dir = innerPath.dirname(localPath);
31
37
  if (!innerFs.existsSync(dir)) innerFs.mkdirSync(dir, { recursive: true });
38
+ options?.beforeReplace?.();
32
39
  innerFs.writeFileSync(localPath, "mock file content");
33
40
  return { metadata: {} };
34
41
  }),
@@ -36,6 +43,7 @@ vi.mock("../s3.js", async (importOriginal) => {
36
43
  deleteRemoteFile: innerVi.fn().mockResolvedValue(undefined),
37
44
  headRemoteFile: innerVi.fn().mockResolvedValue(null),
38
45
  primeObjectTransport: innerVi.fn().mockResolvedValue(undefined),
46
+ primeUploads: innerVi.fn().mockResolvedValue(undefined),
39
47
  };
40
48
  });
41
49
 
@@ -46,6 +54,7 @@ vi.mock("./reindex.js", () => ({
46
54
  }));
47
55
 
48
56
  import { sync, reportNewFilesToNotify } from "./sync.js";
57
+ import { share } from "./share.js";
49
58
  import * as s3Module from "../s3.js";
50
59
  import { reindex } from "./reindex.js";
51
60
  import { readSyncProgress } from "../sync-progress.js";
@@ -618,6 +627,8 @@ describe("sync", () => {
618
627
  const localContent = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`).join("\n") + "\n";
619
628
  fs.writeFileSync(localPath, localContent);
620
629
  const st = fs.statSync(localPath);
630
+ const crypto = await import("node:crypto");
631
+ const localHash = crypto.createHash("sha256").update(localContent).digest("hex");
621
632
 
622
633
  // Remote object is SMALLER (an older, truncated copy) with a CHANGED etag.
623
634
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
@@ -638,7 +649,7 @@ describe("sync", () => {
638
649
  lastSync: new Date().toISOString(),
639
650
  files: {
640
651
  "docs/handoff.md": {
641
- hash: "clean-local-baseline-hash",
652
+ hash: localHash,
642
653
  size: st.size,
643
654
  mtimeMs: st.mtimeMs,
644
655
  remoteEtag: "old-etag",
@@ -682,6 +693,8 @@ describe("sync", () => {
682
693
  Array.from({ length: 20 }, (_, i) => `line ${i + 1}`).join("\n") + "\n";
683
694
  fs.writeFileSync(localPath, localContent);
684
695
  const st = fs.statSync(localPath);
696
+ const crypto = await import("node:crypto");
697
+ const localHash = crypto.createHash("sha256").update(localContent).digest("hex");
685
698
 
686
699
  // Remote is SMALLER with a changed etag — a legitimate shorter rewrite.
687
700
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
@@ -695,7 +708,7 @@ describe("sync", () => {
695
708
  lastSync: new Date().toISOString(),
696
709
  files: {
697
710
  "docs/handoff.md": {
698
- hash: "clean-local-baseline-hash",
711
+ hash: localHash,
699
712
  size: st.size,
700
713
  mtimeMs: st.mtimeMs,
701
714
  remoteEtag: "old-etag",
@@ -733,6 +746,8 @@ describe("sync", () => {
733
746
  const localContent = "short local\n";
734
747
  fs.writeFileSync(localPath, localContent);
735
748
  const st = fs.statSync(localPath);
749
+ const crypto = await import("node:crypto");
750
+ const localHash = crypto.createHash("sha256").update(localContent).digest("hex");
736
751
 
737
752
  // Remote is LARGER with a changed etag → legitimate advance.
738
753
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
@@ -746,7 +761,7 @@ describe("sync", () => {
746
761
  lastSync: new Date().toISOString(),
747
762
  files: {
748
763
  "docs/handoff.md": {
749
- hash: "clean-local-baseline-hash",
764
+ hash: localHash,
750
765
  size: st.size,
751
766
  mtimeMs: st.mtimeMs,
752
767
  remoteEtag: "old-etag",
@@ -960,12 +975,9 @@ describe("sync", () => {
960
975
  expect(journal.files[trackedKey]).toBeUndefined();
961
976
  });
962
977
 
963
- it("local journal tombstone suppresses drift-restore: a deleted-but-still-remote key is NOT re-downloaded (no respawn)", async () => {
964
- // Repro of the intermittent delete-respawn: the user deleted a clean file,
965
- // its delete has not (yet) cleared the S3 object, and the SERVER
966
- // FILE_TOMBSTONE fetch is degraded (empty map — the logged failure mode).
967
- // The only intent record is the LOCAL journal tombstone (`removedAt`).
968
- // The drift-restore path must honor it and NOT re-download the key.
978
+ it("fails closed for a legacy local-delete tombstone and restores the remote key", async () => {
979
+ // A legacy `removedReason:local-delete` was inferred from an ENOENT. It
980
+ // has no version-bound explicit intent, so it must never suppress a pull.
969
981
  const key = "docs/handoff.md";
970
982
  const localPath = path.join(tmpDir, "companies", "acme", key);
971
983
 
@@ -1012,20 +1024,16 @@ describe("sync", () => {
1012
1024
  onEvent: () => {},
1013
1025
  });
1014
1026
 
1015
- // The intentionally-deleted key must NOT be re-materialised.
1016
- expect(s3Module.downloadFile).not.toHaveBeenCalledWith(
1027
+ expect(s3Module.downloadFile).toHaveBeenCalledWith(
1017
1028
  expect.anything(),
1018
1029
  key,
1019
1030
  expect.anything(),
1031
+ expect.anything(),
1020
1032
  );
1021
- expect(fs.existsSync(localPath)).toBe(false);
1033
+ expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1022
1034
  });
1023
1035
 
1024
- it("pull producer: a single clean/current/non-divergent missing file is tombstoned (intentional delete), not drift-restored", async () => {
1025
- // The user deleted a genuinely-synced file. Its entry is clean (etag matches
1026
- // the current remote — currency holds), not divergent. The pull leg must
1027
- // treat the absence as an intentional delete: stamp a persisting
1028
- // `local-delete` tombstone and SKIP the re-download — not resurrect it.
1036
+ it("does not infer local-delete intent from a single missing file", async () => {
1029
1037
  const key = "docs/gone.md";
1030
1038
  const localPath = path.join(tmpDir, "companies", "acme", key);
1031
1039
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
@@ -1052,15 +1060,15 @@ describe("sync", () => {
1052
1060
 
1053
1061
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1054
1062
 
1055
- expect(s3Module.downloadFile).not.toHaveBeenCalledWith(
1063
+ expect(s3Module.downloadFile).toHaveBeenCalledWith(
1056
1064
  expect.anything(),
1057
1065
  key,
1058
1066
  expect.anything(),
1067
+ expect.anything(),
1059
1068
  );
1060
- expect(fs.existsSync(localPath)).toBe(false);
1069
+ expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1061
1070
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1062
- expect(journal.files[key]?.removedAt).toBeTruthy();
1063
- expect(journal.files[key]?.removedReason).toBe("local-delete");
1071
+ expect(journal.files[key]?.removedAt).toBeUndefined();
1064
1072
  });
1065
1073
 
1066
1074
  it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
@@ -1079,7 +1087,14 @@ describe("sync", () => {
1079
1087
  size: 10,
1080
1088
  syncedAt: new Date().toISOString(),
1081
1089
  direction: "down",
1090
+ kind: "file",
1082
1091
  remoteEtag: `e-${k}`,
1092
+ localDeleteIntent: {
1093
+ version: 1,
1094
+ remoteEtag: `e-${k}`,
1095
+ localHash: "h",
1096
+ localKind: "file",
1097
+ },
1083
1098
  };
1084
1099
  }
1085
1100
  fs.writeFileSync(
@@ -1123,9 +1138,16 @@ describe("sync", () => {
1123
1138
  size: 42,
1124
1139
  syncedAt: new Date().toISOString(),
1125
1140
  direction: "down",
1141
+ kind: "file",
1126
1142
  remoteEtag: "cur123",
1127
1143
  removedAt: new Date().toISOString(),
1128
1144
  removedReason: "local-delete",
1145
+ localDeleteIntent: {
1146
+ version: 1,
1147
+ remoteEtag: "cur123",
1148
+ localHash: "h",
1149
+ localKind: "file",
1150
+ },
1129
1151
  },
1130
1152
  },
1131
1153
  pulls: [],
@@ -1237,6 +1259,43 @@ describe("sync", () => {
1237
1259
  expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "docs", "readme.md"))).toBe(false);
1238
1260
  });
1239
1261
 
1262
+ it("personalMode skips generated namespaced skill-wrapper objects and still pulls local skills", async () => {
1263
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1264
+ {
1265
+ key: ".claude/skills/acme:demo/scripts",
1266
+ size: 0,
1267
+ lastModified: new Date(),
1268
+ etag: '"generated"',
1269
+ },
1270
+ {
1271
+ key: ".claude/skills/local-skill/SKILL.md",
1272
+ size: 30,
1273
+ lastModified: new Date(),
1274
+ etag: '"local"',
1275
+ },
1276
+ ]);
1277
+
1278
+ const result = await sync({
1279
+ company: "acme",
1280
+ vaultConfig: mockConfig,
1281
+ hqRoot: tmpDir,
1282
+ personalMode: true,
1283
+ });
1284
+
1285
+ expect(result.filesDownloaded).toBe(1);
1286
+ expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
1287
+ expect(
1288
+ fs.existsSync(path.join(tmpDir, ".claude", "skills", "acme:demo", "scripts")),
1289
+ ).toBe(false);
1290
+ expect(
1291
+ fs.existsSync(path.join(tmpDir, ".claude", "skills", "local-skill", "SKILL.md")),
1292
+ ).toBe(true);
1293
+ const downloadedKeys = vi
1294
+ .mocked(s3Module.downloadFile)
1295
+ .mock.calls.map((call) => call[1]);
1296
+ expect(downloadedKeys).not.toContain(".claude/skills/acme:demo/scripts");
1297
+ });
1298
+
1240
1299
  it("personalMode: downloads + journals companies/manifest.yaml (carve-out round-trips) while still skipping team-synced orphan keys", async () => {
1241
1300
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1242
1301
  { key: "companies/foo/bar.md", size: 50, lastModified: new Date(), etag: '"xyz789"' },
@@ -1771,6 +1830,252 @@ describe("sync", () => {
1771
1830
  expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
1772
1831
  });
1773
1832
 
1833
+ it("CAS: preserves a file created after a remote-delete plan", async () => {
1834
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1835
+ const localPath = path.join(companyRoot, "docs", "created-after-plan.md");
1836
+ const baseline = "synced baseline";
1837
+ const crypto = await import("node:crypto");
1838
+ const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
1839
+
1840
+ fs.writeFileSync(
1841
+ journalPath,
1842
+ JSON.stringify({
1843
+ version: "2",
1844
+ lastSync: new Date().toISOString(),
1845
+ files: {
1846
+ "docs/created-after-plan.md": {
1847
+ hash: baselineHash,
1848
+ size: baseline.length,
1849
+ syncedAt: new Date().toISOString(),
1850
+ direction: "down",
1851
+ kind: "file",
1852
+ remoteEtag: "before-delete",
1853
+ },
1854
+ },
1855
+ pulls: [],
1856
+ }),
1857
+ );
1858
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([]);
1859
+ vi.mocked(s3Module.headRemoteFile).mockImplementationOnce(async () => {
1860
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
1861
+ // Same bytes as the journal baseline: hash-only revalidation would unlink
1862
+ // this newly-created file, but the planned snapshot was "absent".
1863
+ fs.writeFileSync(localPath, baseline);
1864
+ return null;
1865
+ });
1866
+
1867
+ const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1868
+
1869
+ expect(result.filesTombstoned).toBe(0);
1870
+ expect(result.conflicts).toBe(1);
1871
+ expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
1872
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1873
+ expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
1874
+ });
1875
+
1876
+ it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
1877
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1878
+ const localPath = path.join(companyRoot, "docs", "staged-race.md");
1879
+ const baseline = "synced baseline";
1880
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
1881
+ fs.writeFileSync(localPath, baseline);
1882
+ const crypto = await import("node:crypto");
1883
+ const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
1884
+ fs.writeFileSync(
1885
+ journalPath,
1886
+ JSON.stringify({
1887
+ version: "2",
1888
+ lastSync: new Date().toISOString(),
1889
+ files: {
1890
+ "docs/staged-race.md": {
1891
+ hash: baselineHash,
1892
+ size: baseline.length,
1893
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1894
+ direction: "down",
1895
+ kind: "file",
1896
+ remoteEtag: "old-etag",
1897
+ },
1898
+ },
1899
+ pulls: [],
1900
+ }),
1901
+ );
1902
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1903
+ { key: "docs/staged-race.md", size: baseline.length + 10, lastModified: new Date(), etag: '"new-etag"' },
1904
+ ]);
1905
+ vi.mocked(s3Module.downloadFile).mockImplementationOnce(async (
1906
+ _ctx: unknown,
1907
+ _key: string,
1908
+ destination: string,
1909
+ options?: { beforeReplace?: () => void },
1910
+ ) => {
1911
+ fs.writeFileSync(destination, "concurrent local edit");
1912
+ options?.beforeReplace?.();
1913
+ fs.writeFileSync(destination, "remote replacement");
1914
+ return { metadata: {} };
1915
+ });
1916
+
1917
+ const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1918
+
1919
+ expect(result.filesDownloaded).toBe(0);
1920
+ expect(result.conflicts).toBe(1);
1921
+ expect(fs.readFileSync(localPath, "utf-8")).toBe("concurrent local edit");
1922
+ });
1923
+
1924
+ it("personal vault delete planning excludes release core and generated skill mirrors", async () => {
1925
+ const personalJournalPath = path.join(stateDir, "sync-journal.personal-delete.json");
1926
+ const intent = (remoteEtag: string) => ({
1927
+ version: 1 as const,
1928
+ remoteEtag,
1929
+ localHash: "synced-hash",
1930
+ localKind: "file" as const,
1931
+ });
1932
+ fs.writeFileSync(
1933
+ personalJournalPath,
1934
+ JSON.stringify({
1935
+ version: "2",
1936
+ lastSync: new Date().toISOString(),
1937
+ files: {
1938
+ "core/core.yaml": {
1939
+ hash: "synced-hash",
1940
+ size: 1,
1941
+ syncedAt: new Date().toISOString(),
1942
+ direction: "up",
1943
+ kind: "file",
1944
+ remoteEtag: "core-etag",
1945
+ localDeleteIntent: intent("core-etag"),
1946
+ },
1947
+ ".claude/skills/core:demo/SKILL.md": {
1948
+ hash: "synced-hash",
1949
+ size: 1,
1950
+ syncedAt: new Date().toISOString(),
1951
+ direction: "up",
1952
+ kind: "file",
1953
+ remoteEtag: "mirror-etag",
1954
+ localDeleteIntent: intent("mirror-etag"),
1955
+ },
1956
+ },
1957
+ pulls: [],
1958
+ }),
1959
+ );
1960
+
1961
+ await share({
1962
+ company: "acme",
1963
+ vaultConfig: mockConfig,
1964
+ hqRoot: tmpDir,
1965
+ paths: [tmpDir],
1966
+ personalMode: true,
1967
+ journalSlug: "personal-delete",
1968
+ propagateDeletes: true,
1969
+ propagateDeletePolicy: "all",
1970
+ });
1971
+
1972
+ expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
1973
+ const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
1974
+ expect(journal.files["core/core.yaml"]).toBeDefined();
1975
+ expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
1976
+ });
1977
+
1978
+ it("does not issue DeleteObject for two ordinary missing journal files without explicit intent", async () => {
1979
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1980
+ fs.mkdirSync(companyRoot, { recursive: true });
1981
+ const entry = (remoteEtag: string) => ({
1982
+ hash: "synced-hash",
1983
+ size: 1,
1984
+ syncedAt: new Date().toISOString(),
1985
+ direction: "up",
1986
+ kind: "file",
1987
+ remoteEtag,
1988
+ });
1989
+ fs.writeFileSync(
1990
+ journalPath,
1991
+ JSON.stringify({
1992
+ version: "2",
1993
+ lastSync: new Date().toISOString(),
1994
+ files: {
1995
+ "docs/a.md": entry("a-etag"),
1996
+ "docs/b.md": entry("b-etag"),
1997
+ },
1998
+ pulls: [],
1999
+ }),
2000
+ );
2001
+
2002
+ const result = await share({
2003
+ company: "acme",
2004
+ vaultConfig: mockConfig,
2005
+ hqRoot: tmpDir,
2006
+ paths: [companyRoot],
2007
+ propagateDeletes: true,
2008
+ propagateDeletePolicy: "all",
2009
+ });
2010
+
2011
+ expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2012
+ expect(result.filesRefusedStale).toBe(2);
2013
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2014
+ expect(journal.files["docs/a.md"]).toBeDefined();
2015
+ expect(journal.files["docs/b.md"]).toBeDefined();
2016
+ });
2017
+
2018
+ it("uses a conditional PUT conflict for concurrent board.json appends", async () => {
2019
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2020
+ const boardPath = path.join(companyRoot, "board.json");
2021
+ const baseline = '{"items":[]}';
2022
+ const localAppend = '{"items":["local append"]}';
2023
+ fs.mkdirSync(companyRoot, { recursive: true });
2024
+ fs.writeFileSync(boardPath, localAppend);
2025
+ const crypto = await import("node:crypto");
2026
+ const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
2027
+ fs.writeFileSync(
2028
+ journalPath,
2029
+ JSON.stringify({
2030
+ version: "2",
2031
+ lastSync: new Date().toISOString(),
2032
+ files: {
2033
+ "board.json": {
2034
+ hash: baselineHash,
2035
+ size: baseline.length,
2036
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
2037
+ direction: "up",
2038
+ kind: "file",
2039
+ remoteEtag: "old-etag",
2040
+ },
2041
+ },
2042
+ pulls: [],
2043
+ }),
2044
+ );
2045
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
2046
+ etag: "old-etag",
2047
+ size: baseline.length,
2048
+ lastModified: new Date(),
2049
+ });
2050
+ const preconditionError = Object.assign(new Error("remote changed"), {
2051
+ name: "PreconditionFailed",
2052
+ });
2053
+ vi.mocked(s3Module.uploadFile).mockRejectedValueOnce(preconditionError);
2054
+
2055
+ const { isCloudAuthoritative } = await import(
2056
+ "../lib/cloud-authoritative.js",
2057
+ );
2058
+ expect(isCloudAuthoritative("companies/acme/board.json")).toBe(true);
2059
+
2060
+ const result = await share({
2061
+ company: "acme",
2062
+ vaultConfig: mockConfig,
2063
+ hqRoot: tmpDir,
2064
+ paths: [companyRoot],
2065
+ onConflict: "keep",
2066
+ });
2067
+
2068
+ expect(result.conflictPaths).toContain("board.json");
2069
+ expect(fs.readFileSync(boardPath, "utf-8")).toBe(localAppend);
2070
+ expect(s3Module.uploadFile).toHaveBeenCalledWith(
2071
+ expect.anything(),
2072
+ boardPath,
2073
+ "board.json",
2074
+ undefined,
2075
+ { ifMatch: "old-etag" },
2076
+ );
2077
+ });
2078
+
1774
2079
  it("does NOT tombstone symlinks whose readlink target has diverged from the journal (Codex P1 round 4)", async () => {
1775
2080
  // Codex review on PR #24 round 4 caught: the round-3 local-edit
1776
2081
  // divergence guard only covered regular files (`isFile()` is false
@@ -2586,7 +2891,7 @@ describe("sync", () => {
2586
2891
  try {
2587
2892
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(remoteFiles);
2588
2893
  vi.mocked(s3Module.downloadFile).mockImplementation(
2589
- async (ctx, key, localPath) => {
2894
+ async (ctx, key, localPath, options) => {
2590
2895
  if (key === "bulk/file-8.md") {
2591
2896
  // This models the next transfer crashing the process. The first
2592
2897
  // eight entries must already be durable before the final writer.
@@ -2594,7 +2899,7 @@ describe("sync", () => {
2594
2899
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
2595
2900
  throw new Error("simulated crash after checkpoint");
2596
2901
  }
2597
- return defaultDownload(ctx, key, localPath);
2902
+ return defaultDownload(ctx, key, localPath, options);
2598
2903
  },
2599
2904
  );
2600
2905
 
@@ -2627,6 +2932,7 @@ describe("sync", () => {
2627
2932
  expect.anything(),
2628
2933
  "bulk/file-8.md",
2629
2934
  expect.any(String),
2935
+ expect.objectContaining({ beforeReplace: expect.any(Function) }),
2630
2936
  );
2631
2937
  } finally {
2632
2938
  vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);