@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.
- package/dist/bin/sync-runner.d.ts +2 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +35 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +49 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +39 -4
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +55 -0
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/share.js +15 -0
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +100 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +87 -3
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +204 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +8 -2
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +32 -1
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +34 -1
- package/dist/journal.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +7 -3
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +12 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +61 -0
- package/src/bin/sync-runner.ts +57 -14
- package/src/cli/reindex.test.ts +60 -0
- package/src/cli/reindex.ts +47 -8
- package/src/cli/share.test.ts +119 -0
- package/src/cli/share.ts +21 -1
- package/src/cli/sync.test.ts +256 -0
- package/src/cli/sync.ts +97 -3
- package/src/journal.test.ts +44 -0
- package/src/journal.ts +37 -2
- package/src/skill-telemetry.test.ts +24 -0
- package/src/skill-telemetry.ts +6 -3
- package/src/types.ts +11 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -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 });
|