@indigoai-us/hq-cloud 6.15.31 → 6.15.32
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/bandwidth.d.ts +128 -0
- package/dist/bandwidth.d.ts.map +1 -0
- package/dist/bandwidth.js +254 -0
- package/dist/bandwidth.js.map +1 -0
- package/dist/bandwidth.test.d.ts +2 -0
- package/dist/bandwidth.test.d.ts.map +1 -0
- package/dist/bandwidth.test.js +243 -0
- package/dist/bandwidth.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +13 -24
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +71 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +181 -16
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +16 -1
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +40 -2
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +53 -1
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +15 -3
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +66 -13
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +433 -13
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict.d.ts +25 -2
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +29 -4
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/share.d.ts +40 -4
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +478 -86
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +845 -150
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +147 -18
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +20 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +261 -29
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +519 -90
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-delta-writes.test.d.ts +2 -0
- package/dist/journal-delta-writes.test.d.ts.map +1 -0
- package/dist/journal-delta-writes.test.js +140 -0
- package/dist/journal-delta-writes.test.js.map +1 -0
- package/dist/journal.d.ts +1 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +155 -25
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +29 -11
- package/dist/journal.test.js.map +1 -1
- package/dist/object-io.d.ts +34 -13
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +154 -29
- package/dist/object-io.js.map +1 -1
- package/dist/operation-lock.d.ts +12 -0
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +103 -21
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +90 -0
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +7 -1
- package/dist/outcome-telemetry.d.ts.map +1 -1
- package/dist/outcome-telemetry.js +15 -9
- package/dist/outcome-telemetry.js.map +1 -1
- package/dist/outcome-telemetry.test.js +15 -0
- package/dist/outcome-telemetry.test.js.map +1 -1
- package/dist/outposts/client.d.ts +2 -2
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +4 -3
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +9 -6
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/command.test.js +59 -2
- package/dist/outposts/command.test.js.map +1 -1
- package/dist/outposts/remote-command.d.ts +8 -8
- package/dist/outposts/remote-command.d.ts.map +1 -1
- package/dist/outposts/remote-command.js +10 -8
- package/dist/outposts/remote-command.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +14 -0
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +29 -0
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +22 -1
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -8
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +43 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sts-credential-refresh.test.js +87 -2
- package/dist/sts-credential-refresh.test.js.map +1 -1
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +20 -1
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +64 -0
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +7 -2
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-core.test.js +36 -0
- package/dist/sync-core.test.js.map +1 -1
- package/dist/telemetry.d.ts +30 -6
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +331 -135
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +403 -0
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +26 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.d.ts +76 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +105 -3
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +165 -1
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import * as os from "os";
|
|
|
12
12
|
import * as path from "path";
|
|
13
13
|
import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
|
|
14
14
|
import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
15
|
+
import { setBandwidthGovernorForTesting } from "../bandwidth.js";
|
|
15
16
|
import { FakeClock, } from "../watcher.js";
|
|
16
17
|
import { adaptivePollMs, advanceMonotonicDeadline, journalEntriesForWatcherDelete, shouldQuarantineWatcherDeleteIntents, } from "./sync-runner-watch-loop.js";
|
|
17
18
|
import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
|
|
@@ -215,6 +216,63 @@ describe("argv parsing", () => {
|
|
|
215
216
|
});
|
|
216
217
|
});
|
|
217
218
|
// ---------------------------------------------------------------------------
|
|
219
|
+
// bandwidth governor startup line
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
describe("bandwidth governor startup line", () => {
|
|
222
|
+
const ENV_KEYS = [
|
|
223
|
+
"HQ_SYNC_MAX_BYTES_PER_SEC",
|
|
224
|
+
"HQ_SYNC_MAX_CONCURRENCY",
|
|
225
|
+
"HQ_SYNC_BANDWIDTH_PERCENT",
|
|
226
|
+
];
|
|
227
|
+
let saved;
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
230
|
+
for (const k of ENV_KEYS)
|
|
231
|
+
delete process.env[k];
|
|
232
|
+
setBandwidthGovernorForTesting(null); // re-read env per test
|
|
233
|
+
});
|
|
234
|
+
afterEach(() => {
|
|
235
|
+
for (const k of ENV_KEYS) {
|
|
236
|
+
if (saved[k] === undefined)
|
|
237
|
+
delete process.env[k];
|
|
238
|
+
else
|
|
239
|
+
process.env[k] = saved[k];
|
|
240
|
+
}
|
|
241
|
+
setBandwidthGovernorForTesting(null);
|
|
242
|
+
});
|
|
243
|
+
it("states the effective policy on stderr when the env vars are set", async () => {
|
|
244
|
+
process.env.HQ_SYNC_MAX_BYTES_PER_SEC = "500000";
|
|
245
|
+
process.env.HQ_SYNC_MAX_CONCURRENCY = "2";
|
|
246
|
+
const deps = makeDeps({
|
|
247
|
+
createVaultClient: () => makeVaultStub({ memberships: [] }),
|
|
248
|
+
});
|
|
249
|
+
const code = await runRunner(["--companies"], deps);
|
|
250
|
+
expect(code).toBe(0);
|
|
251
|
+
expect(deps.stderr.events()).toContainEqual({
|
|
252
|
+
type: "bandwidth-policy",
|
|
253
|
+
message: "bandwidth governor: 500000 B/s cap, concurrency 2",
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
it("stays silent (exact legacy stream) when no policy is configured", async () => {
|
|
257
|
+
const deps = makeDeps({
|
|
258
|
+
createVaultClient: () => makeVaultStub({ memberships: [] }),
|
|
259
|
+
});
|
|
260
|
+
const code = await runRunner(["--companies"], deps);
|
|
261
|
+
expect(code).toBe(0);
|
|
262
|
+
expect(deps.stderr.events().some((e) => e.type === "bandwidth-policy")).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
it("treats invalid env values as off (no policy line)", async () => {
|
|
265
|
+
process.env.HQ_SYNC_MAX_BYTES_PER_SEC = "0";
|
|
266
|
+
process.env.HQ_SYNC_BANDWIDTH_PERCENT = "101";
|
|
267
|
+
const deps = makeDeps({
|
|
268
|
+
createVaultClient: () => makeVaultStub({ memberships: [] }),
|
|
269
|
+
});
|
|
270
|
+
const code = await runRunner(["--companies"], deps);
|
|
271
|
+
expect(code).toBe(0);
|
|
272
|
+
expect(deps.stderr.events().some((e) => e.type === "bandwidth-policy")).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
218
276
|
// auth
|
|
219
277
|
// ---------------------------------------------------------------------------
|
|
220
278
|
describe("auth", () => {
|
|
@@ -4523,7 +4581,18 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4523
4581
|
"--hq-root",
|
|
4524
4582
|
"/tmp/hq",
|
|
4525
4583
|
],
|
|
4526
|
-
|
|
4584
|
+
// The drain pull is route-targeted AND path-scoped now: it lists only
|
|
4585
|
+
// the touched subtree instead of running a full --companies pull.
|
|
4586
|
+
[
|
|
4587
|
+
"--company",
|
|
4588
|
+
"indigo",
|
|
4589
|
+
"--direction",
|
|
4590
|
+
"pull",
|
|
4591
|
+
"--scope-path",
|
|
4592
|
+
"knowledge/a.md",
|
|
4593
|
+
"--hq-root",
|
|
4594
|
+
"/tmp/hq",
|
|
4595
|
+
],
|
|
4527
4596
|
]);
|
|
4528
4597
|
shutdown();
|
|
4529
4598
|
tick();
|
|
@@ -4540,7 +4609,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4540
4609
|
await flushLoopMicrotasks();
|
|
4541
4610
|
const pushCalls = runPass.mock.calls
|
|
4542
4611
|
.map((call) => call[0])
|
|
4543
|
-
.filter((argv) => argv.includes("--company"));
|
|
4612
|
+
.filter((argv) => argv.includes("--company") && argv.includes("push"));
|
|
4544
4613
|
expect(pushCalls).toEqual([
|
|
4545
4614
|
[
|
|
4546
4615
|
"--company",
|
|
@@ -4620,11 +4689,21 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4620
4689
|
await flushLoopMicrotasks();
|
|
4621
4690
|
const calls = runPass.mock.calls.map((call) => call[0]);
|
|
4622
4691
|
// No scoped push of any kind ran...
|
|
4623
|
-
expect(calls.some((argv) => argv.includes("--company"))).toBe(false);
|
|
4624
4692
|
expect(calls.some((argv) => argv.includes("--direction") &&
|
|
4625
4693
|
argv[argv.indexOf("--direction") + 1] === "push")).toBe(false);
|
|
4626
|
-
// ...only the pull leg ran.
|
|
4627
|
-
expect(calls).toEqual([
|
|
4694
|
+
// ...only the pull leg ran — route-targeted and path-scoped now.
|
|
4695
|
+
expect(calls).toEqual([
|
|
4696
|
+
[
|
|
4697
|
+
"--company",
|
|
4698
|
+
"indigo",
|
|
4699
|
+
"--direction",
|
|
4700
|
+
"pull",
|
|
4701
|
+
"--scope-path",
|
|
4702
|
+
"knowledge/a.md",
|
|
4703
|
+
"--hq-root",
|
|
4704
|
+
"/tmp/hq",
|
|
4705
|
+
],
|
|
4706
|
+
]);
|
|
4628
4707
|
shutdown();
|
|
4629
4708
|
tick();
|
|
4630
4709
|
await loop;
|
|
@@ -4647,7 +4726,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4647
4726
|
await flushLoopMicrotasks();
|
|
4648
4727
|
const pushCalls = runPass.mock.calls
|
|
4649
4728
|
.map((call) => call[0])
|
|
4650
|
-
.filter((argv) => argv.includes("--company"));
|
|
4729
|
+
.filter((argv) => argv.includes("--company") && argv.includes("push"));
|
|
4651
4730
|
expect(pushCalls).toEqual([
|
|
4652
4731
|
[
|
|
4653
4732
|
"--company",
|
|
@@ -4714,7 +4793,17 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4714
4793
|
"--hq-root",
|
|
4715
4794
|
"/tmp/hq",
|
|
4716
4795
|
],
|
|
4717
|
-
|
|
4796
|
+
// Path-scoped now: the personal pull leg lists only the touched
|
|
4797
|
+
// subtree instead of re-walking the whole vault on every event batch.
|
|
4798
|
+
[
|
|
4799
|
+
"--personal",
|
|
4800
|
+
"--direction",
|
|
4801
|
+
"pull",
|
|
4802
|
+
"--scope-path",
|
|
4803
|
+
"knowledge/a.md",
|
|
4804
|
+
"--hq-root",
|
|
4805
|
+
"/tmp/hq",
|
|
4806
|
+
],
|
|
4718
4807
|
]);
|
|
4719
4808
|
shutdown();
|
|
4720
4809
|
tick();
|
|
@@ -4750,7 +4839,16 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4750
4839
|
"--hq-root",
|
|
4751
4840
|
"/tmp/hq",
|
|
4752
4841
|
],
|
|
4753
|
-
[
|
|
4842
|
+
[
|
|
4843
|
+
"--company",
|
|
4844
|
+
"acme",
|
|
4845
|
+
"--direction",
|
|
4846
|
+
"pull",
|
|
4847
|
+
"--scope-path",
|
|
4848
|
+
"knowledge/a.md",
|
|
4849
|
+
"--hq-root",
|
|
4850
|
+
"/tmp/hq",
|
|
4851
|
+
],
|
|
4754
4852
|
]);
|
|
4755
4853
|
shutdown();
|
|
4756
4854
|
tick();
|
|
@@ -4828,6 +4926,245 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4828
4926
|
sleep.tick();
|
|
4829
4927
|
await loop;
|
|
4830
4928
|
});
|
|
4929
|
+
it("overflow with known dropped-route hints fans out per touched route instead of a full reconcile", async () => {
|
|
4930
|
+
const watcher = makeBatchWatcherStub();
|
|
4931
|
+
const sleep = makeSteppableSleep();
|
|
4932
|
+
let triggerShutdown = () => { };
|
|
4933
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4934
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
4935
|
+
runPass,
|
|
4936
|
+
clock: new FakeClock(),
|
|
4937
|
+
createWatcher: () => watcher,
|
|
4938
|
+
sleep: sleep.sleep,
|
|
4939
|
+
onShutdownSignal: (handler) => {
|
|
4940
|
+
triggerShutdown = handler;
|
|
4941
|
+
return () => { };
|
|
4942
|
+
},
|
|
4943
|
+
});
|
|
4944
|
+
await flushLoopMicrotasks();
|
|
4945
|
+
runPass.mockClear();
|
|
4946
|
+
// A burst overflowed, but every dropped path recorded route evidence:
|
|
4947
|
+
// retained path in indigo, dropped paths in acme. The drain must run one
|
|
4948
|
+
// whole-route pass per touched route — never the all-companies fanout.
|
|
4949
|
+
watcher.emit("companies/indigo/a.md", {
|
|
4950
|
+
paths: new Map([
|
|
4951
|
+
["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
|
|
4952
|
+
]),
|
|
4953
|
+
overflowed: true,
|
|
4954
|
+
droppedPaths: 3,
|
|
4955
|
+
droppedRouteHints: ["companies/acme/dropped.md"],
|
|
4956
|
+
});
|
|
4957
|
+
sleep.tick();
|
|
4958
|
+
// Four guarded passes run sequentially — give the loop room to finish.
|
|
4959
|
+
await flushLoopMicrotasks(40);
|
|
4960
|
+
const calls = runPass.mock.calls.map((call) => call[0]);
|
|
4961
|
+
expect(calls).toEqual([
|
|
4962
|
+
// indigo: whole-route push (no --scope-path — dropped paths unknown)…
|
|
4963
|
+
["--company", "indigo", "--direction", "push", "--hq-root", "/tmp/hq"],
|
|
4964
|
+
// …then the route's pull.
|
|
4965
|
+
["--company", "indigo", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
4966
|
+
["--company", "acme", "--direction", "push", "--hq-root", "/tmp/hq"],
|
|
4967
|
+
["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
4968
|
+
]);
|
|
4969
|
+
expect(calls).not.toContainEqual(fullArgv);
|
|
4970
|
+
triggerShutdown();
|
|
4971
|
+
await loop;
|
|
4972
|
+
});
|
|
4973
|
+
it("falls back to a full reconcile when an event batch exceeds HQ_SYNC_EVENT_BATCH_LIMIT", async () => {
|
|
4974
|
+
const saved = process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
|
|
4975
|
+
process.env.HQ_SYNC_EVENT_BATCH_LIMIT = "2";
|
|
4976
|
+
try {
|
|
4977
|
+
const watcher = makeBatchWatcherStub();
|
|
4978
|
+
const sleep = makeSteppableSleep();
|
|
4979
|
+
let triggerShutdown = () => { };
|
|
4980
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4981
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
4982
|
+
runPass,
|
|
4983
|
+
clock: new FakeClock(),
|
|
4984
|
+
createWatcher: () => watcher,
|
|
4985
|
+
sleep: sleep.sleep,
|
|
4986
|
+
onShutdownSignal: (handler) => {
|
|
4987
|
+
triggerShutdown = handler;
|
|
4988
|
+
return () => { };
|
|
4989
|
+
},
|
|
4990
|
+
});
|
|
4991
|
+
await flushLoopMicrotasks();
|
|
4992
|
+
runPass.mockClear();
|
|
4993
|
+
watcher.emit("companies/indigo/a.md", {
|
|
4994
|
+
paths: new Map([
|
|
4995
|
+
["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
|
|
4996
|
+
["/tmp/hq/companies/indigo/b.md", "companies/indigo/b.md"],
|
|
4997
|
+
["/tmp/hq/companies/indigo/c.md", "companies/indigo/c.md"],
|
|
4998
|
+
]),
|
|
4999
|
+
});
|
|
5000
|
+
sleep.tick();
|
|
5001
|
+
await flushLoopMicrotasks();
|
|
5002
|
+
// Mass change: one full reconcile, no per-path scoped passes.
|
|
5003
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5004
|
+
triggerShutdown();
|
|
5005
|
+
await loop;
|
|
5006
|
+
}
|
|
5007
|
+
finally {
|
|
5008
|
+
if (saved === undefined)
|
|
5009
|
+
delete process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
|
|
5010
|
+
else
|
|
5011
|
+
process.env.HQ_SYNC_EVENT_BATCH_LIMIT = saved;
|
|
5012
|
+
}
|
|
5013
|
+
});
|
|
5014
|
+
it("a batch under the default limit stays on the targeted path", async () => {
|
|
5015
|
+
const watcher = makeBatchWatcherStub();
|
|
5016
|
+
const sleep = makeSteppableSleep();
|
|
5017
|
+
let triggerShutdown = () => { };
|
|
5018
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5019
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5020
|
+
runPass,
|
|
5021
|
+
clock: new FakeClock(),
|
|
5022
|
+
createWatcher: () => watcher,
|
|
5023
|
+
sleep: sleep.sleep,
|
|
5024
|
+
onShutdownSignal: (handler) => {
|
|
5025
|
+
triggerShutdown = handler;
|
|
5026
|
+
return () => { };
|
|
5027
|
+
},
|
|
5028
|
+
});
|
|
5029
|
+
await flushLoopMicrotasks();
|
|
5030
|
+
runPass.mockClear();
|
|
5031
|
+
watcher.emit("companies/indigo/a.md", {
|
|
5032
|
+
paths: new Map([
|
|
5033
|
+
["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
|
|
5034
|
+
["/tmp/hq/companies/indigo/b.md", "companies/indigo/b.md"],
|
|
5035
|
+
["/tmp/hq/companies/indigo/c.md", "companies/indigo/c.md"],
|
|
5036
|
+
]),
|
|
5037
|
+
});
|
|
5038
|
+
sleep.tick();
|
|
5039
|
+
await flushLoopMicrotasks();
|
|
5040
|
+
const calls = runPass.mock.calls.map((call) => call[0]);
|
|
5041
|
+
expect(calls).not.toContainEqual(fullArgv);
|
|
5042
|
+
expect(calls[0]).toEqual([
|
|
5043
|
+
"--company",
|
|
5044
|
+
"indigo",
|
|
5045
|
+
"--direction",
|
|
5046
|
+
"push",
|
|
5047
|
+
"--scope-path",
|
|
5048
|
+
"a.md",
|
|
5049
|
+
"--scope-path",
|
|
5050
|
+
"b.md",
|
|
5051
|
+
"--scope-path",
|
|
5052
|
+
"c.md",
|
|
5053
|
+
"--hq-root",
|
|
5054
|
+
"/tmp/hq",
|
|
5055
|
+
]);
|
|
5056
|
+
triggerShutdown();
|
|
5057
|
+
await loop;
|
|
5058
|
+
});
|
|
5059
|
+
it("a throwing targeted pass falls back to one full reconcile instead of dropping the change", async () => {
|
|
5060
|
+
const runPass = vi.fn(async (argv) => {
|
|
5061
|
+
if (argv.includes("pull") && argv.includes("--scope-path")) {
|
|
5062
|
+
throw new Error("scoped pull exploded");
|
|
5063
|
+
}
|
|
5064
|
+
return completedPassOutcome();
|
|
5065
|
+
});
|
|
5066
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
5067
|
+
await flushLoopMicrotasks();
|
|
5068
|
+
runPass.mockClear();
|
|
5069
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
5070
|
+
tick();
|
|
5071
|
+
await flushLoopMicrotasks(40);
|
|
5072
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
5073
|
+
[
|
|
5074
|
+
"--company",
|
|
5075
|
+
"indigo",
|
|
5076
|
+
"--direction",
|
|
5077
|
+
"push",
|
|
5078
|
+
"--scope-path",
|
|
5079
|
+
"knowledge/a.md",
|
|
5080
|
+
"--hq-root",
|
|
5081
|
+
"/tmp/hq",
|
|
5082
|
+
],
|
|
5083
|
+
// The scoped pull throws…
|
|
5084
|
+
[
|
|
5085
|
+
"--company",
|
|
5086
|
+
"indigo",
|
|
5087
|
+
"--direction",
|
|
5088
|
+
"pull",
|
|
5089
|
+
"--scope-path",
|
|
5090
|
+
"knowledge/a.md",
|
|
5091
|
+
"--hq-root",
|
|
5092
|
+
"/tmp/hq",
|
|
5093
|
+
],
|
|
5094
|
+
// …and the loop degrades to the full reconcile, never dropping the tick.
|
|
5095
|
+
fullArgv,
|
|
5096
|
+
]);
|
|
5097
|
+
shutdown();
|
|
5098
|
+
tick();
|
|
5099
|
+
await loop;
|
|
5100
|
+
});
|
|
5101
|
+
it("wires the event debounce config (default 15s quiet / 120s max-wait) into the watcher factory", async () => {
|
|
5102
|
+
const captured = [];
|
|
5103
|
+
const watcher = makeWatcherStub();
|
|
5104
|
+
const sleep = makeSteppableSleep();
|
|
5105
|
+
let triggerShutdown = () => { };
|
|
5106
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5107
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5108
|
+
runPass,
|
|
5109
|
+
clock: new FakeClock(),
|
|
5110
|
+
createWatcher: (opts) => {
|
|
5111
|
+
captured.push({ debounceMs: opts.debounceMs, maxWaitMs: opts.maxWaitMs });
|
|
5112
|
+
return watcher;
|
|
5113
|
+
},
|
|
5114
|
+
sleep: sleep.sleep,
|
|
5115
|
+
onShutdownSignal: (handler) => {
|
|
5116
|
+
triggerShutdown = handler;
|
|
5117
|
+
return () => { };
|
|
5118
|
+
},
|
|
5119
|
+
});
|
|
5120
|
+
await flushLoopMicrotasks();
|
|
5121
|
+
expect(captured).toEqual([{ debounceMs: 15_000, maxWaitMs: 120_000 }]);
|
|
5122
|
+
triggerShutdown();
|
|
5123
|
+
await loop;
|
|
5124
|
+
});
|
|
5125
|
+
it("HQ_SYNC_EVENT_DEBOUNCE_MS / HQ_SYNC_EVENT_MAX_WAIT_MS override the watcher debounce", async () => {
|
|
5126
|
+
const savedDebounce = process.env.HQ_SYNC_EVENT_DEBOUNCE_MS;
|
|
5127
|
+
const savedMaxWait = process.env.HQ_SYNC_EVENT_MAX_WAIT_MS;
|
|
5128
|
+
process.env.HQ_SYNC_EVENT_DEBOUNCE_MS = "5000";
|
|
5129
|
+
process.env.HQ_SYNC_EVENT_MAX_WAIT_MS = "45000";
|
|
5130
|
+
try {
|
|
5131
|
+
const captured = [];
|
|
5132
|
+
const watcher = makeWatcherStub();
|
|
5133
|
+
const sleep = makeSteppableSleep();
|
|
5134
|
+
let triggerShutdown = () => { };
|
|
5135
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5136
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5137
|
+
runPass,
|
|
5138
|
+
clock: new FakeClock(),
|
|
5139
|
+
createWatcher: (opts) => {
|
|
5140
|
+
captured.push({
|
|
5141
|
+
debounceMs: opts.debounceMs,
|
|
5142
|
+
maxWaitMs: opts.maxWaitMs,
|
|
5143
|
+
});
|
|
5144
|
+
return watcher;
|
|
5145
|
+
},
|
|
5146
|
+
sleep: sleep.sleep,
|
|
5147
|
+
onShutdownSignal: (handler) => {
|
|
5148
|
+
triggerShutdown = handler;
|
|
5149
|
+
return () => { };
|
|
5150
|
+
},
|
|
5151
|
+
});
|
|
5152
|
+
await flushLoopMicrotasks();
|
|
5153
|
+
expect(captured).toEqual([{ debounceMs: 5000, maxWaitMs: 45_000 }]);
|
|
5154
|
+
triggerShutdown();
|
|
5155
|
+
await loop;
|
|
5156
|
+
}
|
|
5157
|
+
finally {
|
|
5158
|
+
if (savedDebounce === undefined)
|
|
5159
|
+
delete process.env.HQ_SYNC_EVENT_DEBOUNCE_MS;
|
|
5160
|
+
else
|
|
5161
|
+
process.env.HQ_SYNC_EVENT_DEBOUNCE_MS = savedDebounce;
|
|
5162
|
+
if (savedMaxWait === undefined)
|
|
5163
|
+
delete process.env.HQ_SYNC_EVENT_MAX_WAIT_MS;
|
|
5164
|
+
else
|
|
5165
|
+
process.env.HQ_SYNC_EVENT_MAX_WAIT_MS = savedMaxWait;
|
|
5166
|
+
}
|
|
5167
|
+
});
|
|
4831
5168
|
});
|
|
4832
5169
|
// ---------------------------------------------------------------------------
|
|
4833
5170
|
// Re-initialize for each test (mock state hygiene)
|
|
@@ -6050,7 +6387,17 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
6050
6387
|
"--hq-root",
|
|
6051
6388
|
"/tmp/hq",
|
|
6052
6389
|
],
|
|
6053
|
-
|
|
6390
|
+
// The receiver's targeted pull is path-scoped to the pushed key now.
|
|
6391
|
+
[
|
|
6392
|
+
"--company",
|
|
6393
|
+
"acme",
|
|
6394
|
+
"--direction",
|
|
6395
|
+
"pull",
|
|
6396
|
+
"--scope-path",
|
|
6397
|
+
"remote.md",
|
|
6398
|
+
"--hq-root",
|
|
6399
|
+
"/tmp/hq",
|
|
6400
|
+
],
|
|
6054
6401
|
]));
|
|
6055
6402
|
const pushedRoutes = new Set();
|
|
6056
6403
|
for (const passArgv of passArgvs) {
|
|
@@ -6151,7 +6498,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
6151
6498
|
// the differential signal for "did telemetry run".
|
|
6152
6499
|
// ---------------------------------------------------------------------------
|
|
6153
6500
|
describe("defaultCollectTelemetry person-entity gate", () => {
|
|
6154
|
-
function makeTelemetryClient(persons) {
|
|
6501
|
+
function makeTelemetryClient(persons, agents = []) {
|
|
6155
6502
|
const calls = {
|
|
6156
6503
|
listByType: 0,
|
|
6157
6504
|
getTelemetryOptIn: 0,
|
|
@@ -6161,9 +6508,9 @@ describe("defaultCollectTelemetry person-entity gate", () => {
|
|
|
6161
6508
|
const client = {
|
|
6162
6509
|
entity: {
|
|
6163
6510
|
get: async () => ({}),
|
|
6164
|
-
listByType: async (
|
|
6511
|
+
listByType: async (type) => {
|
|
6165
6512
|
calls.listByType++;
|
|
6166
|
-
return persons;
|
|
6513
|
+
return type === "agent" ? agents : persons;
|
|
6167
6514
|
},
|
|
6168
6515
|
},
|
|
6169
6516
|
// Telemetry surface (cast-to internally by defaultCollectTelemetry). With
|
|
@@ -6198,7 +6545,7 @@ describe("defaultCollectTelemetry person-entity gate", () => {
|
|
|
6198
6545
|
try {
|
|
6199
6546
|
const { client, calls } = makeTelemetryClient([]); // unprovisioned caller
|
|
6200
6547
|
await defaultCollectTelemetry(client, false, hqRoot);
|
|
6201
|
-
expect(calls.listByType).toBe(
|
|
6548
|
+
expect(calls.listByType).toBe(2); // person probe then agent probe
|
|
6202
6549
|
expect(calls.getTelemetryOptIn).toBe(0); // collectors never ran
|
|
6203
6550
|
expect(calls.postUsage).toBe(0);
|
|
6204
6551
|
expect(calls.postSkillInvocations).toBe(0);
|
|
@@ -6220,6 +6567,79 @@ describe("defaultCollectTelemetry person-entity gate", () => {
|
|
|
6220
6567
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6221
6568
|
}
|
|
6222
6569
|
});
|
|
6570
|
+
it("runs telemetry for an agent machine identity with no person entity", async () => {
|
|
6571
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-"));
|
|
6572
|
+
try {
|
|
6573
|
+
const AGENT = {
|
|
6574
|
+
uid: "agt_01KUSAGEAGENTTEST00000000",
|
|
6575
|
+
type: "agent",
|
|
6576
|
+
slug: "jeff",
|
|
6577
|
+
status: "active",
|
|
6578
|
+
bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
|
|
6579
|
+
createdAt: "2026-06-01T00:00:00Z",
|
|
6580
|
+
};
|
|
6581
|
+
const { client, calls } = makeTelemetryClient([], [AGENT]);
|
|
6582
|
+
await defaultCollectTelemetry(client, false, hqRoot, undefined, undefined, AGENT.uid);
|
|
6583
|
+
expect(calls.listByType).toBe(2);
|
|
6584
|
+
// Both collectors forceCollect — neither probes GET /v1/usage/opt-in.
|
|
6585
|
+
expect(calls.getTelemetryOptIn).toBe(0);
|
|
6586
|
+
// Passed the gate: identity resolution writes machine-id before collecting.
|
|
6587
|
+
expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(true);
|
|
6588
|
+
}
|
|
6589
|
+
finally {
|
|
6590
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6591
|
+
}
|
|
6592
|
+
});
|
|
6593
|
+
it("does not force-collect when a claimed agent uid is absent from the self-listing", async () => {
|
|
6594
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-miss-"));
|
|
6595
|
+
try {
|
|
6596
|
+
const AGENT = {
|
|
6597
|
+
uid: "agt_01KUSAGEAGENTTEST00000000",
|
|
6598
|
+
type: "agent",
|
|
6599
|
+
slug: "jeff",
|
|
6600
|
+
status: "active",
|
|
6601
|
+
bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
|
|
6602
|
+
createdAt: "2026-06-01T00:00:00Z",
|
|
6603
|
+
};
|
|
6604
|
+
const { client, calls } = makeTelemetryClient([], [AGENT]);
|
|
6605
|
+
await defaultCollectTelemetry(client, false, hqRoot, undefined, undefined, "agt_01KSOMEONEELSE00000000000");
|
|
6606
|
+
expect(calls.listByType).toBe(2);
|
|
6607
|
+
expect(calls.getTelemetryOptIn).toBe(0);
|
|
6608
|
+
expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(false);
|
|
6609
|
+
}
|
|
6610
|
+
finally {
|
|
6611
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6612
|
+
}
|
|
6613
|
+
});
|
|
6614
|
+
it("does not treat any agt_ listing as self when more than one agent is visible", async () => {
|
|
6615
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-many-"));
|
|
6616
|
+
try {
|
|
6617
|
+
const a = {
|
|
6618
|
+
uid: "agt_01KUSAGEAGENTTEST00000000",
|
|
6619
|
+
type: "agent",
|
|
6620
|
+
slug: "jeff",
|
|
6621
|
+
status: "active",
|
|
6622
|
+
bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
|
|
6623
|
+
createdAt: "2026-06-01T00:00:00Z",
|
|
6624
|
+
};
|
|
6625
|
+
const b = {
|
|
6626
|
+
uid: "agt_01KUSAGEAGENTOTHER0000000",
|
|
6627
|
+
type: "agent",
|
|
6628
|
+
slug: "cx",
|
|
6629
|
+
status: "active",
|
|
6630
|
+
bucketName: "hq-vault-agt_01KUSAGEAGENTOTHER0000000",
|
|
6631
|
+
createdAt: "2026-06-02T00:00:00Z",
|
|
6632
|
+
};
|
|
6633
|
+
const { client, calls } = makeTelemetryClient([], [a, b]);
|
|
6634
|
+
await defaultCollectTelemetry(client, false, hqRoot);
|
|
6635
|
+
expect(calls.listByType).toBe(2);
|
|
6636
|
+
expect(calls.getTelemetryOptIn).toBe(0);
|
|
6637
|
+
expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(false);
|
|
6638
|
+
}
|
|
6639
|
+
finally {
|
|
6640
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6641
|
+
}
|
|
6642
|
+
});
|
|
6223
6643
|
it("fails open: a thrown person probe still lets telemetry run (no provisioned-user regression)", async () => {
|
|
6224
6644
|
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-throw-"));
|
|
6225
6645
|
try {
|