@indigoai-us/hq-cloud 6.12.5 → 6.13.1

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 (48) hide show
  1. package/dist/bin/sync-runner.d.ts +2 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +35 -2
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +49 -0
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/reindex.d.ts.map +1 -1
  8. package/dist/cli/reindex.js +39 -4
  9. package/dist/cli/reindex.js.map +1 -1
  10. package/dist/cli/reindex.test.js +55 -0
  11. package/dist/cli/reindex.test.js.map +1 -1
  12. package/dist/cli/share.js +15 -0
  13. package/dist/cli/share.js.map +1 -1
  14. package/dist/cli/share.test.js +100 -0
  15. package/dist/cli/share.test.js.map +1 -1
  16. package/dist/cli/sync.d.ts +1 -1
  17. package/dist/cli/sync.d.ts.map +1 -1
  18. package/dist/cli/sync.js +87 -3
  19. package/dist/cli/sync.js.map +1 -1
  20. package/dist/cli/sync.test.js +204 -0
  21. package/dist/cli/sync.test.js.map +1 -1
  22. package/dist/journal.d.ts +8 -2
  23. package/dist/journal.d.ts.map +1 -1
  24. package/dist/journal.js +32 -1
  25. package/dist/journal.js.map +1 -1
  26. package/dist/journal.test.js +34 -1
  27. package/dist/journal.test.js.map +1 -1
  28. package/dist/skill-telemetry.d.ts.map +1 -1
  29. package/dist/skill-telemetry.js +7 -3
  30. package/dist/skill-telemetry.js.map +1 -1
  31. package/dist/skill-telemetry.test.js +12 -0
  32. package/dist/skill-telemetry.test.js.map +1 -1
  33. package/dist/types.d.ts +11 -1
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/bin/sync-runner.test.ts +61 -0
  37. package/src/bin/sync-runner.ts +57 -14
  38. package/src/cli/reindex.test.ts +60 -0
  39. package/src/cli/reindex.ts +47 -8
  40. package/src/cli/share.test.ts +119 -0
  41. package/src/cli/share.ts +21 -1
  42. package/src/cli/sync.test.ts +256 -0
  43. package/src/cli/sync.ts +97 -3
  44. package/src/journal.test.ts +44 -0
  45. package/src/journal.ts +37 -2
  46. package/src/skill-telemetry.test.ts +24 -0
  47. package/src/skill-telemetry.ts +6 -3
  48. package/src/types.ts +11 -1
@@ -2034,6 +2034,51 @@ describe("share", () => {
2034
2034
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2035
2035
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
2036
2036
  });
2037
+ it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
2038
+ // The pull-leg producer stamped a persisting `local-delete` tombstone for a
2039
+ // file the user deleted (no respawn). The cross-leg hand-off: on the next
2040
+ // push the S3 object must actually be DELETED and the entry dropped, so the
2041
+ // deletion propagates to the cloud + every other surface.
2042
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2043
+ fs.mkdirSync(companyRoot, { recursive: true });
2044
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2045
+ fs.writeFileSync(journalPath, JSON.stringify({
2046
+ version: "2",
2047
+ lastSync: new Date().toISOString(),
2048
+ files: {
2049
+ "docs/deleted.md": {
2050
+ hash: "h",
2051
+ size: 100,
2052
+ syncedAt: new Date().toISOString(),
2053
+ direction: "down",
2054
+ remoteEtag: "abc123",
2055
+ removedAt: new Date().toISOString(),
2056
+ removedReason: "local-delete",
2057
+ },
2058
+ },
2059
+ pulls: [],
2060
+ }));
2061
+ // HEAD returns the same etag — this device had the current version, so the
2062
+ // currency-gated delete is safe to propagate.
2063
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2064
+ lastModified: new Date(),
2065
+ etag: '"abc123"',
2066
+ size: 100,
2067
+ });
2068
+ const result = await share({
2069
+ paths: [companyRoot],
2070
+ company: "acme",
2071
+ vaultConfig: mockConfig,
2072
+ hqRoot: tmpDir,
2073
+ skipUnchanged: true,
2074
+ propagateDeletes: true,
2075
+ propagateDeletePolicy: "currency-gated",
2076
+ });
2077
+ expect(result.filesDeleted).toBe(1);
2078
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
2079
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2080
+ expect(journal.files["docs/deleted.md"]).toBeUndefined();
2081
+ });
2037
2082
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
2038
2083
  const companyRoot = path.join(tmpDir, "companies", "acme");
2039
2084
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -2087,6 +2132,61 @@ describe("share", () => {
2087
2132
  expect(result.filesRefusedStale).toBe(1);
2088
2133
  expect(result.filesTombstoned).toBe(0);
2089
2134
  });
2135
+ it("currency-gated: refuses delete for a divergent-local entry (journal-honesty — never delete a remote the local never genuinely matched)", async () => {
2136
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2137
+ fs.mkdirSync(companyRoot, { recursive: true });
2138
+ // A keep-conflict recorded the REMOTE etag (so the conflict can't re-fire —
2139
+ // the #137 invariant) but the kept LOCAL content DIVERGED from that remote
2140
+ // (`localDiverges:true`). The local copy was then deleted. The currency HEAD
2141
+ // would MATCH the recorded etag — the trap — yet the etag never reflected a
2142
+ // genuine local==remote sync, so propagating the delete would destroy the
2143
+ // (different) remote version. Must be REFUSED, not propagated.
2144
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2145
+ fs.writeFileSync(journalPath, JSON.stringify({
2146
+ version: "2",
2147
+ lastSync: new Date().toISOString(),
2148
+ files: {
2149
+ "core/policies/kept.md": {
2150
+ hash: "local-divergent-hash",
2151
+ size: 100,
2152
+ syncedAt: new Date().toISOString(),
2153
+ direction: "down",
2154
+ remoteEtag: "abc123",
2155
+ localDiverges: true,
2156
+ },
2157
+ },
2158
+ pulls: [],
2159
+ }));
2160
+ // Deliberately NO HEAD mock: the divergence guard short-circuits BEFORE the
2161
+ // currency HEAD, so `headRemoteFile` is never called for this entry — the
2162
+ // refusal costs no network round-trip (asserted below).
2163
+ const events = [];
2164
+ const result = await share({
2165
+ paths: [companyRoot],
2166
+ company: "acme",
2167
+ vaultConfig: mockConfig,
2168
+ hqRoot: tmpDir,
2169
+ skipUnchanged: true,
2170
+ propagateDeletes: true,
2171
+ propagateDeletePolicy: "currency-gated",
2172
+ onEvent: (e) => events.push(e),
2173
+ });
2174
+ // No remote DELETE — the recorded etag never reflected a genuine sync.
2175
+ expect(result.filesDeleted).toBe(0);
2176
+ expect(deleteRemoteFile).not.toHaveBeenCalledWith(expect.anything(), "core/policies/kept.md");
2177
+ // Refusal short-circuits before the currency HEAD — no network needed.
2178
+ expect(headRemoteFile).not.toHaveBeenCalled();
2179
+ // Journal entry preserved, not silently dropped.
2180
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2181
+ expect(journal.files["core/policies/kept.md"]).toBeDefined();
2182
+ // Surfaced as a refusal with the divergent-local reason.
2183
+ const refused = events.find((e) => e.type === "delete-refused-stale-etag");
2184
+ expect(refused).toMatchObject({
2185
+ type: "delete-refused-stale-etag",
2186
+ path: "core/policies/kept.md",
2187
+ reason: "divergent-local",
2188
+ });
2189
+ });
2090
2190
  it("currency-gated: tombstones journal entry when remote returns 404 (out-of-band cleanup)", async () => {
2091
2191
  const companyRoot = path.join(tmpDir, "companies", "acme");
2092
2192
  fs.mkdirSync(companyRoot, { recursive: true });