@indigoai-us/hq-cloud 6.15.44 → 6.15.46

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.
@@ -127,7 +127,7 @@ import * as conflictModule from "./conflict.js";
127
127
  import { reindex } from "./reindex.js";
128
128
  import { readSyncProgress } from "../sync-progress.js";
129
129
  import { VaultAuthError } from "../vault-client.js";
130
- import { readJournal, getEntry, listJournals } from "../journal.js";
130
+ import { readJournal, writeJournal, getEntry, listJournals } from "../journal.js";
131
131
  /**
132
132
  * Read a journal the way production does.
133
133
  *
@@ -246,6 +246,53 @@ describe("sync", () => {
246
246
  it("keeps classifyVaultKey available from the partial S3 mock", () => {
247
247
  expect(s3Module.classifyVaultKey("docs/readme.md", "company")).toBeNull();
248
248
  });
249
+ it("uses a journal update committed during LIST to classify a local delete as intentional", async () => {
250
+ const companyRoot = path.join(tmpDir, "companies", "acme");
251
+ fs.mkdirSync(companyRoot, { recursive: true });
252
+ writeJournal("acme", {
253
+ version: "2",
254
+ lastSync: "",
255
+ pulls: [],
256
+ files: {
257
+ "deleted-during-list.md": {
258
+ hash: "a".repeat(64),
259
+ size: 1,
260
+ syncedAt: "2026-08-24T21:48:05.000Z",
261
+ direction: "up",
262
+ remoteEtag: "current-etag",
263
+ kind: "file",
264
+ },
265
+ },
266
+ });
267
+ // This is the production timing: sync has loaded its journal and is blocked
268
+ // in LIST when the watcher records the local deletion intent.
269
+ vi.mocked(s3Module.listRemoteFiles).mockImplementationOnce(async () => {
270
+ const watcherJournal = readJournal("acme");
271
+ watcherJournal.files["deleted-during-list.md"].localDeleteIntent = {
272
+ version: 1,
273
+ remoteEtag: "current-etag",
274
+ localHash: "a".repeat(64),
275
+ localKind: "file",
276
+ };
277
+ writeJournal("acme", watcherJournal);
278
+ return [{
279
+ key: "deleted-during-list.md",
280
+ size: 1,
281
+ lastModified: new Date(),
282
+ etag: '"current-etag"',
283
+ }];
284
+ });
285
+ await sync({
286
+ company: "acme",
287
+ vaultConfig: mockConfig,
288
+ hqRoot: tmpDir,
289
+ onConflict: "keep",
290
+ skipReindex: true,
291
+ });
292
+ expect(s3Module.downloadFile).not.toHaveBeenCalled();
293
+ expect(readJournal("acme").files["deleted-during-list.md"]?.removedReason)
294
+ .toBe("local-delete");
295
+ });
249
296
  it("pulses liveness from the synchronous pull planner", async () => {
250
297
  let heartbeatCalls = 0;
251
298
  await sync({
@@ -1734,7 +1781,7 @@ describe("sync", () => {
1734
1781
  expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1735
1782
  expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1736
1783
  });
1737
- it("does not infer local-delete intent from a single missing file", async () => {
1784
+ it("currency-gated pull treats a watcherless, ETag-current missing file as an intentional delete", async () => {
1738
1785
  const key = "docs/gone.md";
1739
1786
  const localPath = path.join(tmpDir, "companies", "acme", key);
1740
1787
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
@@ -1755,11 +1802,88 @@ describe("sync", () => {
1755
1802
  },
1756
1803
  pulls: [],
1757
1804
  }));
1758
- await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1805
+ await sync({
1806
+ company: "acme",
1807
+ vaultConfig: mockConfig,
1808
+ hqRoot: tmpDir,
1809
+ propagateDeletePolicy: "currency-gated",
1810
+ });
1811
+ expect(s3Module.downloadFile).not.toHaveBeenCalled();
1812
+ expect(fs.existsSync(localPath)).toBe(false);
1813
+ const journal = journalAt(journalPath);
1814
+ expect(journal.files[key]?.removedReason).toBe("local-delete");
1815
+ });
1816
+ it("owned-only pull refuses the same watcherless missing file for want of an intent", async () => {
1817
+ const key = "docs/gone.md";
1818
+ const localPath = path.join(tmpDir, "companies", "acme", key);
1819
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1820
+ { key, size: 42, lastModified: new Date(), etag: '"cur123"' },
1821
+ ]);
1822
+ setupFetchMock({ tombstones: [] });
1823
+ fs.writeFileSync(journalPath, JSON.stringify({
1824
+ version: "2",
1825
+ lastSync: new Date().toISOString(),
1826
+ files: {
1827
+ [key]: {
1828
+ hash: "h",
1829
+ size: 42,
1830
+ syncedAt: new Date().toISOString(),
1831
+ direction: "down",
1832
+ remoteEtag: "cur123",
1833
+ },
1834
+ },
1835
+ pulls: [],
1836
+ }));
1837
+ await sync({
1838
+ company: "acme",
1839
+ vaultConfig: mockConfig,
1840
+ hqRoot: tmpDir,
1841
+ propagateDeletePolicy: "owned-only",
1842
+ });
1759
1843
  expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1760
1844
  expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1845
+ expect(journalAt(journalPath).files[key]?.removedAt).toBeUndefined();
1846
+ });
1847
+ it("currency-gated pull still downloads stale-ETag and divergent watcherless absences", async () => {
1848
+ const staleKey = "docs/stale.md";
1849
+ const divergentKey = "docs/divergent.md";
1850
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1851
+ { key: staleKey, size: 42, lastModified: new Date(), etag: '"remote-etag"' },
1852
+ { key: divergentKey, size: 42, lastModified: new Date(), etag: '"cur123"' },
1853
+ ]);
1854
+ setupFetchMock({ tombstones: [] });
1855
+ fs.writeFileSync(journalPath, JSON.stringify({
1856
+ version: "2",
1857
+ lastSync: new Date().toISOString(),
1858
+ files: {
1859
+ [staleKey]: {
1860
+ hash: "h",
1861
+ size: 42,
1862
+ syncedAt: new Date().toISOString(),
1863
+ direction: "down",
1864
+ remoteEtag: "old-etag",
1865
+ },
1866
+ [divergentKey]: {
1867
+ hash: "h",
1868
+ size: 42,
1869
+ syncedAt: new Date().toISOString(),
1870
+ direction: "down",
1871
+ remoteEtag: "cur123",
1872
+ localDiverges: true,
1873
+ },
1874
+ },
1875
+ pulls: [],
1876
+ }));
1877
+ await sync({
1878
+ company: "acme",
1879
+ vaultConfig: mockConfig,
1880
+ hqRoot: tmpDir,
1881
+ propagateDeletePolicy: "currency-gated",
1882
+ });
1883
+ expect(s3Module.downloadFile).toHaveBeenCalledTimes(2);
1761
1884
  const journal = journalAt(journalPath);
1762
- expect(journal.files[key]?.removedAt).toBeUndefined();
1885
+ expect(journal.files[staleKey]?.removedAt).toBeUndefined();
1886
+ expect(journal.files[divergentKey]?.removedAt).toBeUndefined();
1763
1887
  });
1764
1888
  it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
1765
1889
  // Regression (2026-08-19 sync-runner CPU storm): a stale journal carried
@@ -1850,7 +1974,7 @@ describe("sync", () => {
1850
1974
  expect(fs.existsSync(localPath)).toBe(false);
1851
1975
  expect(journalAt(journalPath).files[key]?.localDeleteIntent).toBeDefined();
1852
1976
  });
1853
- it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
1977
+ it("pull producer: a watcherless BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
1854
1978
  // 12 clean files vanish at once (corrupt mirror / bulk rm / checkout). The
1855
1979
  // bulk-asymmetry guard refuses to treat this as intent — every file is
1856
1980
  // healed (re-downloaded), none tombstoned.
@@ -1866,12 +1990,6 @@ describe("sync", () => {
1866
1990
  direction: "down",
1867
1991
  kind: "file",
1868
1992
  remoteEtag: `e-${k}`,
1869
- localDeleteIntent: {
1870
- version: 1,
1871
- remoteEtag: `e-${k}`,
1872
- localHash: "h",
1873
- localKind: "file",
1874
- },
1875
1993
  };
1876
1994
  }
1877
1995
  fs.writeFileSync(journalPath, JSON.stringify({
@@ -1880,7 +1998,12 @@ describe("sync", () => {
1880
1998
  files,
1881
1999
  pulls: [],
1882
2000
  }));
1883
- await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
2001
+ await sync({
2002
+ company: "acme",
2003
+ vaultConfig: mockConfig,
2004
+ hqRoot: tmpDir,
2005
+ propagateDeletePolicy: "currency-gated",
2006
+ });
1884
2007
  // All restored, none tombstoned.
1885
2008
  expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
1886
2009
  const journal = journalAt(journalPath);
@@ -1921,7 +2044,12 @@ describe("sync", () => {
1921
2044
  },
1922
2045
  pulls: [],
1923
2046
  }));
1924
- await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
2047
+ await sync({
2048
+ company: "acme",
2049
+ vaultConfig: mockConfig,
2050
+ hqRoot: tmpDir,
2051
+ propagateDeletePolicy: "currency-gated",
2052
+ });
1925
2053
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
1926
2054
  expect(fs.existsSync(localPath)).toBe(false);
1927
2055
  const journal = journalAt(journalPath);