@indigoai-us/hq-cloud 6.15.42 → 6.15.44
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 +3 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +50 -14
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +135 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +33 -1
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -5
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +3 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +96 -2
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +5 -17
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +16 -77
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +48 -99
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +16 -18
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +16 -72
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +40 -59
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cli/tombstones.d.ts +23 -8
- package/dist/cli/tombstones.d.ts.map +1 -1
- package/dist/cli/tombstones.js +63 -33
- package/dist/cli/tombstones.js.map +1 -1
- package/dist/cli/tombstones.test.d.ts +2 -0
- package/dist/cli/tombstones.test.d.ts.map +1 -0
- package/dist/cli/tombstones.test.js +27 -0
- package/dist/cli/tombstones.test.js.map +1 -0
- package/dist/cli/watch-event-push-conflict.test.js +7 -0
- package/dist/cli/watch-event-push-conflict.test.js.map +1 -1
- package/dist/local-path-codec.test.js +20 -0
- package/dist/local-path-codec.test.js.map +1 -1
- package/dist/s3.d.ts +1 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +7 -23
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +13 -20
- package/dist/s3.test.js.map +1 -1
- package/dist/sync-core.d.ts +16 -0
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +38 -0
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-progress.d.ts +18 -1
- package/dist/sync-progress.d.ts.map +1 -1
- package/dist/sync-progress.js +113 -7
- package/dist/sync-progress.js.map +1 -1
- package/dist/sync-progress.test.js +32 -2
- package/dist/sync-progress.test.js.map +1 -1
- package/dist/watcher.d.ts +37 -8
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +137 -23
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +116 -9
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -1809,13 +1809,13 @@ describe("sync", () => {
|
|
|
1809
1809
|
const sweptJournal = journalAt(journalPath);
|
|
1810
1810
|
expect(sweptJournal.files[ignoredKey]).toBeDefined();
|
|
1811
1811
|
});
|
|
1812
|
-
it("tombstone lookup failure
|
|
1812
|
+
it("tombstone lookup failure fails closed before a current intent-backed delete can resurrect", async () => {
|
|
1813
1813
|
const key = "docs/watcher-deleted.md";
|
|
1814
1814
|
const localPath = path.join(tmpDir, "companies", "acme", key);
|
|
1815
1815
|
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
1816
1816
|
{ key, size: 42, lastModified: new Date(), etag: '"cur123"' },
|
|
1817
1817
|
]);
|
|
1818
|
-
setupFetchMock({ tombstoneFailure: true });
|
|
1818
|
+
const fetchMock = setupFetchMock({ tombstoneFailure: true });
|
|
1819
1819
|
fs.writeFileSync(journalPath, JSON.stringify({
|
|
1820
1820
|
version: "2",
|
|
1821
1821
|
lastSync: new Date().toISOString(),
|
|
@@ -1837,11 +1837,18 @@ describe("sync", () => {
|
|
|
1837
1837
|
},
|
|
1838
1838
|
pulls: [],
|
|
1839
1839
|
}));
|
|
1840
|
-
|
|
1840
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
1841
|
+
try {
|
|
1842
|
+
await expect(sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir })).rejects.toThrow("refusing to sync without delete suppression");
|
|
1843
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("WARN tombstone fetch attempt 3/3 failed"));
|
|
1844
|
+
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes("/v1/files/tombstones"))).toHaveLength(3);
|
|
1845
|
+
}
|
|
1846
|
+
finally {
|
|
1847
|
+
log.mockRestore();
|
|
1848
|
+
}
|
|
1841
1849
|
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
|
|
1842
1850
|
expect(fs.existsSync(localPath)).toBe(false);
|
|
1843
|
-
expect(journalAt(journalPath).files[key]
|
|
1844
|
-
?.removedReason).toBe("local-delete");
|
|
1851
|
+
expect(journalAt(journalPath).files[key]?.localDeleteIntent).toBeDefined();
|
|
1845
1852
|
});
|
|
1846
1853
|
it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
|
|
1847
1854
|
// 12 clean files vanish at once (corrupt mirror / bulk rm / checkout). The
|
|
@@ -3249,18 +3256,11 @@ describe("sync", () => {
|
|
|
3249
3256
|
expect(result.filesDownloaded).toBe(1);
|
|
3250
3257
|
expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
|
|
3251
3258
|
});
|
|
3252
|
-
it("
|
|
3253
|
-
|
|
3254
|
-
// bucket-relative. A remote key literally beginning with `companies/...` is
|
|
3255
|
-
// a doubly-scoped corrupt object: the vault-service refuses to presign it
|
|
3256
|
-
// (INVALID_KEY_COMPANIES_SCOPED), so the per-file GET fails and the whole
|
|
3257
|
-
// company wedges at "errored" (runner exit 2) on every run. Verified live
|
|
3258
|
-
// 2026-06-16 against frogbear. The pull planner must refuse it at planning
|
|
3259
|
-
// time — same policy bucket as the malformed-key / ephemeral filters.
|
|
3260
|
-
const corruptKey = "companies/acme/drafts/reports/signals-2026-06-15.html";
|
|
3259
|
+
it("round-trips a companies/ key into the identical nested company-vault path", async () => {
|
|
3260
|
+
const nestedKey = "companies/acme/audit/chat-skills/x.json";
|
|
3261
3261
|
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
3262
|
-
//
|
|
3263
|
-
{ key:
|
|
3262
|
+
// `companies/` is an ordinary directory beneath this company's root.
|
|
3263
|
+
{ key: nestedKey, size: 2152, lastModified: new Date(), etag: '"nested"' },
|
|
3264
3264
|
// A legitimate bucket-relative key — must still download.
|
|
3265
3265
|
{ key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"ok"' },
|
|
3266
3266
|
]);
|
|
@@ -3270,17 +3270,12 @@ describe("sync", () => {
|
|
|
3270
3270
|
hqRoot: tmpDir,
|
|
3271
3271
|
});
|
|
3272
3272
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
3273
|
-
// The
|
|
3274
|
-
expect(fs.existsSync(path.join(companyRoot, "companies", "acme", "
|
|
3275
|
-
|
|
3276
|
-
// failing presign is the exact symptom this guard removes.
|
|
3277
|
-
for (const call of vi.mocked(s3Module.downloadFile).mock.calls) {
|
|
3278
|
-
expect(call[1]).not.toBe(corruptKey);
|
|
3279
|
-
}
|
|
3273
|
+
// The remote key materializes exactly beneath the selected company root.
|
|
3274
|
+
expect(fs.existsSync(path.join(companyRoot, "companies", "acme", "audit", "chat-skills", "x.json"))).toBe(true);
|
|
3275
|
+
expect(vi.mocked(s3Module.downloadFile).mock.calls.map((call) => call[1])).toContain(nestedKey);
|
|
3280
3276
|
// The legitimate bucket-relative key MUST still download.
|
|
3281
3277
|
expect(fs.existsSync(path.join(companyRoot, "docs", "handoff.md"))).toBe(true);
|
|
3282
|
-
expect(result.filesDownloaded).toBe(
|
|
3283
|
-
expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
|
|
3278
|
+
expect(result.filesDownloaded).toBe(2);
|
|
3284
3279
|
expect(result.aborted).toBe(false);
|
|
3285
3280
|
});
|
|
3286
3281
|
it("F02: rejects traversal remote keys before they can escape the company root", async () => {
|
|
@@ -4753,7 +4748,7 @@ describe("reportNewFilesToNotify chunking (server cap = 1000 files/report)", ()
|
|
|
4753
4748
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
4754
4749
|
});
|
|
4755
4750
|
});
|
|
4756
|
-
describe("
|
|
4751
|
+
describe("nested companies directories in a company vault", () => {
|
|
4757
4752
|
let tmpDir;
|
|
4758
4753
|
let stateDir;
|
|
4759
4754
|
let journalPath;
|
|
@@ -4781,15 +4776,15 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4781
4776
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
4782
4777
|
delete process.env.HQ_STATE_DIR;
|
|
4783
4778
|
});
|
|
4784
|
-
it("
|
|
4785
|
-
// The incident shape: 25 poisoned `companies/indigo/…` objects in the
|
|
4786
|
-
// company bucket; the presign download API rejects them
|
|
4787
|
-
// (INVALID_KEY_COMPANIES_SCOPED), so attempting to download errored the
|
|
4788
|
-
// whole company sync and gated heartbeat toolset refreshes fleet-wide.
|
|
4789
|
-
// One bad object must never take down a company sync.
|
|
4779
|
+
it("downloads the live 25-file nested companies/ shape without a skip event", async () => {
|
|
4790
4780
|
vi.mocked(s3Module.listRemoteFiles).mockReset();
|
|
4791
4781
|
vi.mocked(s3Module.listRemoteFiles).mockResolvedValue([
|
|
4792
|
-
{
|
|
4782
|
+
...Array.from({ length: 25 }, (_, index) => ({
|
|
4783
|
+
key: `companies/acme/audit/chat-skills/file-${index.toString().padStart(2, "0")}.json`,
|
|
4784
|
+
size: 10,
|
|
4785
|
+
lastModified: new Date(),
|
|
4786
|
+
etag: `"nested-${index}"`,
|
|
4787
|
+
})),
|
|
4793
4788
|
{ key: "docs/good.md", size: 5, lastModified: new Date(), etag: '"g1"' },
|
|
4794
4789
|
]);
|
|
4795
4790
|
const events = [];
|
|
@@ -4799,26 +4794,16 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4799
4794
|
hqRoot: tmpDir,
|
|
4800
4795
|
onEvent: (e) => events.push(e),
|
|
4801
4796
|
});
|
|
4802
|
-
// The good file syncs; the poisoned key is never downloaded.
|
|
4803
4797
|
expect(result.aborted).toBe(false);
|
|
4804
|
-
expect(result.filesDownloaded).toBe(
|
|
4798
|
+
expect(result.filesDownloaded).toBe(26);
|
|
4805
4799
|
expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "docs", "good.md"))).toBe(true);
|
|
4806
|
-
expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "companies", "acme", "
|
|
4800
|
+
expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "companies", "acme", "audit", "chat-skills", "file-24.json"))).toBe(true);
|
|
4807
4801
|
const downloadedKeys = vi
|
|
4808
4802
|
.mocked(s3Module.downloadFile)
|
|
4809
4803
|
.mock.calls.map((c) => c[1]);
|
|
4810
|
-
expect(downloadedKeys).
|
|
4811
|
-
// Counted as policy-excluded, surfaced as a benign per-key warning event —
|
|
4812
|
-
// deliberately NOT type:"error" (an error event flips the runner to exit 2).
|
|
4813
|
-
expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
|
|
4804
|
+
expect(downloadedKeys).toContain("companies/acme/audit/chat-skills/file-00.json");
|
|
4814
4805
|
const skipEvents = events.filter((e) => e.type === "skip-invalid-scoped-key");
|
|
4815
|
-
expect(skipEvents).toEqual([
|
|
4816
|
-
{
|
|
4817
|
-
type: "skip-invalid-scoped-key",
|
|
4818
|
-
path: "companies/acme/knowledge/poison.md",
|
|
4819
|
-
errorCode: "INVALID_KEY_COMPANIES_SCOPED",
|
|
4820
|
-
},
|
|
4821
|
-
]);
|
|
4806
|
+
expect(skipEvents).toEqual([]);
|
|
4822
4807
|
expect(events.filter((e) => e.type === "error")).toEqual([]);
|
|
4823
4808
|
});
|
|
4824
4809
|
it("skips a control-character remote key before presigning while transferring valid keys", async () => {
|
|
@@ -4861,13 +4846,9 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4861
4846
|
]);
|
|
4862
4847
|
expect(events.filter((event) => event.type === "error")).toEqual([]);
|
|
4863
4848
|
});
|
|
4864
|
-
it("
|
|
4865
|
-
// A
|
|
4866
|
-
//
|
|
4867
|
-
// The journal entry at the invalid key must drain as a tombstone —
|
|
4868
|
-
// local copy removed, journal entry dropped — and must NOT be
|
|
4869
|
-
// HEAD-verified (the presign transport rejects the key, which would
|
|
4870
|
-
// defer the tombstone forever) and must NOT be re-uploaded.
|
|
4849
|
+
it("tombstones a remote-missing nested companies/ journal entry after HEAD verification", async () => {
|
|
4850
|
+
// A nested `companies/` key follows the same protected journal-tombstone
|
|
4851
|
+
// path as every other vault-relative file.
|
|
4871
4852
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
4872
4853
|
const doubled = path.join(companyRoot, "companies", "acme", "knowledge");
|
|
4873
4854
|
fs.mkdirSync(doubled, { recursive: true });
|
|
@@ -4890,25 +4871,25 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
|
|
|
4890
4871
|
},
|
|
4891
4872
|
},
|
|
4892
4873
|
}));
|
|
4893
|
-
// Remote LIST no longer contains the
|
|
4874
|
+
// Remote LIST no longer contains the nested key.
|
|
4894
4875
|
vi.mocked(s3Module.listRemoteFiles).mockReset();
|
|
4895
4876
|
vi.mocked(s3Module.listRemoteFiles).mockResolvedValue([]);
|
|
4877
|
+
vi.mocked(s3Module.headRemoteFile).mockResolvedValue(null);
|
|
4896
4878
|
const result = await sync({
|
|
4897
4879
|
company: "acme",
|
|
4898
4880
|
vaultConfig: mockConfig,
|
|
4899
4881
|
hqRoot: tmpDir,
|
|
4900
4882
|
});
|
|
4901
|
-
// Cleaned, not re-uploaded: local
|
|
4883
|
+
// Cleaned, not re-uploaded: local nested copy gone, journal dropped.
|
|
4902
4884
|
expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
|
|
4903
4885
|
expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
|
|
4904
4886
|
const journal = journalAt(journalPath);
|
|
4905
4887
|
expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
|
|
4906
|
-
//
|
|
4907
|
-
// reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
|
|
4888
|
+
// The normal HEAD verification confirms it is safe to tombstone.
|
|
4908
4889
|
const headKeys = vi
|
|
4909
4890
|
.mocked(s3Module.headRemoteFile)
|
|
4910
4891
|
.mock.calls.map((c) => c[1]);
|
|
4911
|
-
expect(headKeys).
|
|
4892
|
+
expect(headKeys).toContain("companies/acme/knowledge/poison.md");
|
|
4912
4893
|
// Never re-uploaded.
|
|
4913
4894
|
expect(vi.mocked(s3Module.uploadFile)).not.toHaveBeenCalled();
|
|
4914
4895
|
});
|