@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +1 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +49 -4
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.test.js +51 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +32 -31
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +80 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +20 -8
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +141 -13
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-delta-writes.test.js +19 -0
- package/dist/journal-delta-writes.test.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +28 -10
- package/dist/journal.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.d.ts +4 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +39 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +28 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -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("
|
|
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({
|
|
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[
|
|
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({
|
|
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({
|
|
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);
|