@indigoai-us/hq-cloud 6.13.5 → 6.14.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-company.d.ts +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +41 -2
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +9 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +75 -50
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +3 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +52 -1
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +10 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +597 -45
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +18 -0
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +27 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +61 -2
- package/src/bin/sync-runner-watch-loop.ts +105 -54
- package/src/bin/sync-runner-watch-routes.ts +57 -1
- package/src/bin/sync-runner.test.ts +697 -50
- package/src/bin/sync-runner.ts +11 -0
- package/src/watcher.test.ts +42 -0
- package/src/watcher.ts +18 -0
|
@@ -17,6 +17,7 @@ import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
|
|
|
17
17
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
18
18
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
19
19
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
20
|
+
import { isCoveredByAny } from "../prefix-coalesce.js";
|
|
20
21
|
import { VaultAuthError } from "../vault-client.js";
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Hermetic journal state — the runner now calls migratePersonalVaultJournal()
|
|
@@ -1640,6 +1641,75 @@ describe("--direction", () => {
|
|
|
1640
1641
|
// preserving the pre-fix company-target args shape (full access).
|
|
1641
1642
|
expect(opts.prefixSet).toBeUndefined();
|
|
1642
1643
|
});
|
|
1644
|
+
it("direction=push with --scope-path: forwards exact and descendant scoped prefixes to share()", async () => {
|
|
1645
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1646
|
+
const deps = makeDeps({
|
|
1647
|
+
createVaultClient: () => makeVaultStub({
|
|
1648
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1649
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1650
|
+
}),
|
|
1651
|
+
sync: vi.fn(),
|
|
1652
|
+
share: shareSpy,
|
|
1653
|
+
});
|
|
1654
|
+
await runRunner([
|
|
1655
|
+
"--companies",
|
|
1656
|
+
"--direction",
|
|
1657
|
+
"push",
|
|
1658
|
+
"--hq-root",
|
|
1659
|
+
"/tmp/fake-hq",
|
|
1660
|
+
"--scope-path",
|
|
1661
|
+
"knowledge/a.md",
|
|
1662
|
+
], deps);
|
|
1663
|
+
const opts = shareSpy.mock.calls[0][0];
|
|
1664
|
+
expect(opts.prefixSet).toEqual(["knowledge/a.md", "knowledge/a.md/"]);
|
|
1665
|
+
});
|
|
1666
|
+
it("direction=push with directory --scope-path covers descendant keys for tombstones", async () => {
|
|
1667
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1668
|
+
const deps = makeDeps({
|
|
1669
|
+
createVaultClient: () => makeVaultStub({
|
|
1670
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1671
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1672
|
+
}),
|
|
1673
|
+
sync: vi.fn(),
|
|
1674
|
+
share: shareSpy,
|
|
1675
|
+
});
|
|
1676
|
+
await runRunner([
|
|
1677
|
+
"--companies",
|
|
1678
|
+
"--direction",
|
|
1679
|
+
"push",
|
|
1680
|
+
"--hq-root",
|
|
1681
|
+
"/tmp/fake-hq",
|
|
1682
|
+
"--scope-path",
|
|
1683
|
+
"knowledge/archive",
|
|
1684
|
+
], deps);
|
|
1685
|
+
const opts = shareSpy.mock.calls[0][0];
|
|
1686
|
+
expect(opts.prefixSet).toBeDefined();
|
|
1687
|
+
expect(isCoveredByAny("knowledge/archive/a.md", opts.prefixSet ?? [])).toBe(true);
|
|
1688
|
+
});
|
|
1689
|
+
it("direction=push with file --scope-path does not cover siblings", async () => {
|
|
1690
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1691
|
+
const deps = makeDeps({
|
|
1692
|
+
createVaultClient: () => makeVaultStub({
|
|
1693
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1694
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1695
|
+
}),
|
|
1696
|
+
sync: vi.fn(),
|
|
1697
|
+
share: shareSpy,
|
|
1698
|
+
});
|
|
1699
|
+
await runRunner([
|
|
1700
|
+
"--companies",
|
|
1701
|
+
"--direction",
|
|
1702
|
+
"push",
|
|
1703
|
+
"--hq-root",
|
|
1704
|
+
"/tmp/fake-hq",
|
|
1705
|
+
"--scope-path",
|
|
1706
|
+
"board.json",
|
|
1707
|
+
], deps);
|
|
1708
|
+
const opts = shareSpy.mock.calls[0][0];
|
|
1709
|
+
expect(opts.prefixSet).toBeDefined();
|
|
1710
|
+
expect(isCoveredByAny("board.json", opts.prefixSet ?? [])).toBe(true);
|
|
1711
|
+
expect(isCoveredByAny("other.json", opts.prefixSet ?? [])).toBe(false);
|
|
1712
|
+
});
|
|
1643
1713
|
it("direction=push (shared membership): forwards the resolved ACL prefixSet to share() (feedback_ded09d56)", async () => {
|
|
1644
1714
|
// Plumbing regression for the fresh fix: a member/guest's push must be
|
|
1645
1715
|
// scoped to their granted prefixes so out-of-scope keys are filtered
|
|
@@ -1669,6 +1739,76 @@ describe("--direction", () => {
|
|
|
1669
1739
|
const opts = shareSpy.mock.calls[0][0];
|
|
1670
1740
|
expect(opts.prefixSet).toEqual(["knowledge/", "policies/"]);
|
|
1671
1741
|
});
|
|
1742
|
+
it("direction=push with --scope-path never widens a shared ACL prefixSet", async () => {
|
|
1743
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1744
|
+
const client = {
|
|
1745
|
+
...makeVaultStub({
|
|
1746
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1747
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1748
|
+
}),
|
|
1749
|
+
getMembershipSyncConfig: async () => ({
|
|
1750
|
+
membershipId: "mk_a",
|
|
1751
|
+
syncMode: "shared",
|
|
1752
|
+
isDefault: false,
|
|
1753
|
+
}),
|
|
1754
|
+
listMyExplicitGrants: async () => [
|
|
1755
|
+
{ companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
|
|
1756
|
+
],
|
|
1757
|
+
};
|
|
1758
|
+
const deps = makeDeps({
|
|
1759
|
+
createVaultClient: () => client,
|
|
1760
|
+
sync: vi.fn(),
|
|
1761
|
+
share: shareSpy,
|
|
1762
|
+
});
|
|
1763
|
+
await runRunner([
|
|
1764
|
+
"--companies",
|
|
1765
|
+
"--direction",
|
|
1766
|
+
"push",
|
|
1767
|
+
"--hq-root",
|
|
1768
|
+
"/tmp/fake-hq",
|
|
1769
|
+
"--scope-path",
|
|
1770
|
+
"knowledge/a.md",
|
|
1771
|
+
"--scope-path",
|
|
1772
|
+
"secrets/b.md",
|
|
1773
|
+
], deps);
|
|
1774
|
+
const opts = shareSpy.mock.calls[0][0];
|
|
1775
|
+
expect(opts.prefixSet).toEqual(["knowledge/a.md", "knowledge/a.md/"]);
|
|
1776
|
+
expect(isCoveredByAny("secrets/b.md", opts.prefixSet ?? [])).toBe(false);
|
|
1777
|
+
});
|
|
1778
|
+
it("direction=push with directory --scope-path never widens outside a shared ACL prefixSet", async () => {
|
|
1779
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1780
|
+
const client = {
|
|
1781
|
+
...makeVaultStub({
|
|
1782
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1783
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1784
|
+
}),
|
|
1785
|
+
getMembershipSyncConfig: async () => ({
|
|
1786
|
+
membershipId: "mk_a",
|
|
1787
|
+
syncMode: "shared",
|
|
1788
|
+
isDefault: false,
|
|
1789
|
+
}),
|
|
1790
|
+
listMyExplicitGrants: async () => [
|
|
1791
|
+
{ companyUid: "cmp_a", path: "policies/", permission: "read", source: "person" },
|
|
1792
|
+
],
|
|
1793
|
+
};
|
|
1794
|
+
const deps = makeDeps({
|
|
1795
|
+
createVaultClient: () => client,
|
|
1796
|
+
sync: vi.fn(),
|
|
1797
|
+
share: shareSpy,
|
|
1798
|
+
});
|
|
1799
|
+
await runRunner([
|
|
1800
|
+
"--companies",
|
|
1801
|
+
"--direction",
|
|
1802
|
+
"push",
|
|
1803
|
+
"--hq-root",
|
|
1804
|
+
"/tmp/fake-hq",
|
|
1805
|
+
"--scope-path",
|
|
1806
|
+
"knowledge/archive",
|
|
1807
|
+
], deps);
|
|
1808
|
+
const opts = shareSpy.mock.calls[0][0];
|
|
1809
|
+
expect(isCoveredByAny("knowledge/archive/a.md", opts.prefixSet ?? [])).toBe(false);
|
|
1810
|
+
expect(isCoveredByAny("policies/a.md", opts.prefixSet ?? [])).toBe(false);
|
|
1811
|
+
});
|
|
1672
1812
|
it("direction=both: all-complete sums uploaded and downloaded across companies", async () => {
|
|
1673
1813
|
const slugs = { cmp_a: "acme", cmp_b: "beta" };
|
|
1674
1814
|
const deps = makeDeps({
|
|
@@ -2413,6 +2553,24 @@ function makeWatcherStub() {
|
|
|
2413
2553
|
};
|
|
2414
2554
|
return stub;
|
|
2415
2555
|
}
|
|
2556
|
+
async function flushLoopMicrotasks(n = 8) {
|
|
2557
|
+
for (let i = 0; i < n; i++)
|
|
2558
|
+
await Promise.resolve();
|
|
2559
|
+
}
|
|
2560
|
+
function makeSteppableSleep() {
|
|
2561
|
+
const pending = [];
|
|
2562
|
+
return {
|
|
2563
|
+
sleep: () => new Promise((resolve) => {
|
|
2564
|
+
pending.push(resolve);
|
|
2565
|
+
}),
|
|
2566
|
+
tick() {
|
|
2567
|
+
const resolve = pending.shift();
|
|
2568
|
+
if (!resolve)
|
|
2569
|
+
throw new Error("poll sleep was not pending");
|
|
2570
|
+
resolve();
|
|
2571
|
+
},
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2416
2574
|
describe("runRunnerWithLoop — event-push wiring", () => {
|
|
2417
2575
|
it("starts the watcher alongside the poll loop when --event-push is on", async () => {
|
|
2418
2576
|
const watcher = makeWatcherStub();
|
|
@@ -2438,7 +2596,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2438
2596
|
expect(runPass).toHaveBeenCalled();
|
|
2439
2597
|
expect(watcher.disposed).toBe(true);
|
|
2440
2598
|
});
|
|
2441
|
-
it("trigger-on-change: a
|
|
2599
|
+
it("trigger-on-change: a changed signal accumulates without an immediate push", async () => {
|
|
2442
2600
|
const watcher = makeWatcherStub();
|
|
2443
2601
|
const clock = new FakeClock();
|
|
2444
2602
|
let triggerShutdown = () => { };
|
|
@@ -2462,20 +2620,11 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2462
2620
|
clock.advance(0);
|
|
2463
2621
|
await Promise.resolve();
|
|
2464
2622
|
await Promise.resolve();
|
|
2465
|
-
|
|
2466
|
-
expect(targetedCall).toBeDefined();
|
|
2467
|
-
expect(targetedCall[0]).toEqual([
|
|
2468
|
-
"--company",
|
|
2469
|
-
"indigo",
|
|
2470
|
-
"--direction",
|
|
2471
|
-
"push",
|
|
2472
|
-
"--hq-root",
|
|
2473
|
-
"/tmp/hq",
|
|
2474
|
-
]);
|
|
2623
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
2475
2624
|
triggerShutdown();
|
|
2476
2625
|
await loop;
|
|
2477
2626
|
});
|
|
2478
|
-
it("bare signal (no path)
|
|
2627
|
+
it("bare signal (no path) accumulates without an immediate push", async () => {
|
|
2479
2628
|
const watcher = makeWatcherStub();
|
|
2480
2629
|
const clock = new FakeClock();
|
|
2481
2630
|
let triggerShutdown = () => { };
|
|
@@ -2497,16 +2646,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2497
2646
|
clock.advance(0);
|
|
2498
2647
|
await Promise.resolve();
|
|
2499
2648
|
await Promise.resolve();
|
|
2500
|
-
|
|
2501
|
-
c[0].includes("--direction"));
|
|
2502
|
-
expect(personalCall).toBeDefined();
|
|
2503
|
-
expect(personalCall[0]).toEqual([
|
|
2504
|
-
"--companies",
|
|
2505
|
-
"--direction",
|
|
2506
|
-
"push",
|
|
2507
|
-
"--hq-root",
|
|
2508
|
-
"/tmp/hq",
|
|
2509
|
-
]);
|
|
2649
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
2510
2650
|
triggerShutdown();
|
|
2511
2651
|
await loop;
|
|
2512
2652
|
});
|
|
@@ -2564,7 +2704,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2564
2704
|
triggerShutdown();
|
|
2565
2705
|
await loop;
|
|
2566
2706
|
});
|
|
2567
|
-
it("F05: guard-held watcher
|
|
2707
|
+
it("F05: guard-held watcher changes stay accumulated until a poll drain", async () => {
|
|
2568
2708
|
const watcher = makeWatcherStub();
|
|
2569
2709
|
const clock = new FakeClock();
|
|
2570
2710
|
let triggerShutdown = () => { };
|
|
@@ -2612,14 +2752,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2612
2752
|
await loop;
|
|
2613
2753
|
expect(maxConcurrent).toBe(1);
|
|
2614
2754
|
const targetedCall = runPass.mock.calls.find((c) => c[0].includes("--company"));
|
|
2615
|
-
expect(targetedCall
|
|
2616
|
-
"--company",
|
|
2617
|
-
"indigo",
|
|
2618
|
-
"--direction",
|
|
2619
|
-
"push",
|
|
2620
|
-
"--hq-root",
|
|
2621
|
-
"/tmp/hq",
|
|
2622
|
-
]);
|
|
2755
|
+
expect(targetedCall).toBeUndefined();
|
|
2623
2756
|
});
|
|
2624
2757
|
it("poll-still-runs: the poll loop fires passes independent of the watcher", async () => {
|
|
2625
2758
|
const watcher = makeWatcherStub();
|
|
@@ -2750,6 +2883,360 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2750
2883
|
await loop;
|
|
2751
2884
|
});
|
|
2752
2885
|
});
|
|
2886
|
+
describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
2887
|
+
const watchArgv = [
|
|
2888
|
+
"--companies",
|
|
2889
|
+
"--watch",
|
|
2890
|
+
"--event-push",
|
|
2891
|
+
"--direction",
|
|
2892
|
+
"both",
|
|
2893
|
+
"--hq-root",
|
|
2894
|
+
"/tmp/hq",
|
|
2895
|
+
"--poll-remote-ms",
|
|
2896
|
+
"60000",
|
|
2897
|
+
];
|
|
2898
|
+
const fullArgv = ["--companies", "--direction", "both", "--hq-root", "/tmp/hq"];
|
|
2899
|
+
const fullPullArgv = [
|
|
2900
|
+
"--companies",
|
|
2901
|
+
"--direction",
|
|
2902
|
+
"pull",
|
|
2903
|
+
"--hq-root",
|
|
2904
|
+
"/tmp/hq",
|
|
2905
|
+
];
|
|
2906
|
+
function startDrainLoop(runPass) {
|
|
2907
|
+
return startDrainLoopWithArgv(runPass, watchArgv);
|
|
2908
|
+
}
|
|
2909
|
+
function startDrainLoopWithArgv(runPass, argv) {
|
|
2910
|
+
const watcher = makeWatcherStub();
|
|
2911
|
+
const sleep = makeSteppableSleep();
|
|
2912
|
+
let triggerShutdown = () => { };
|
|
2913
|
+
const loop = runRunnerWithLoop(argv, {
|
|
2914
|
+
runPass,
|
|
2915
|
+
clock: new FakeClock(),
|
|
2916
|
+
createWatcher: () => watcher,
|
|
2917
|
+
sleep: sleep.sleep,
|
|
2918
|
+
onShutdownSignal: (handler) => {
|
|
2919
|
+
triggerShutdown = handler;
|
|
2920
|
+
return () => { };
|
|
2921
|
+
},
|
|
2922
|
+
});
|
|
2923
|
+
return {
|
|
2924
|
+
loop,
|
|
2925
|
+
watcher,
|
|
2926
|
+
tick: sleep.tick,
|
|
2927
|
+
shutdown: () => triggerShutdown(),
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
it("startup runs one full reconcile, then watcher edits wait for the poll drain", async () => {
|
|
2931
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
2932
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
2933
|
+
await flushLoopMicrotasks();
|
|
2934
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
2935
|
+
runPass.mockClear();
|
|
2936
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
2937
|
+
await flushLoopMicrotasks();
|
|
2938
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
2939
|
+
tick();
|
|
2940
|
+
await flushLoopMicrotasks();
|
|
2941
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
2942
|
+
[
|
|
2943
|
+
"--company",
|
|
2944
|
+
"indigo",
|
|
2945
|
+
"--direction",
|
|
2946
|
+
"push",
|
|
2947
|
+
"--scope-path",
|
|
2948
|
+
"knowledge/a.md",
|
|
2949
|
+
"--hq-root",
|
|
2950
|
+
"/tmp/hq",
|
|
2951
|
+
],
|
|
2952
|
+
fullPullArgv,
|
|
2953
|
+
]);
|
|
2954
|
+
shutdown();
|
|
2955
|
+
tick();
|
|
2956
|
+
await loop;
|
|
2957
|
+
});
|
|
2958
|
+
it("coalesces repeated edits to the same path into one scoped push", async () => {
|
|
2959
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
2960
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
2961
|
+
await flushLoopMicrotasks();
|
|
2962
|
+
runPass.mockClear();
|
|
2963
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
2964
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
2965
|
+
tick();
|
|
2966
|
+
await flushLoopMicrotasks();
|
|
2967
|
+
const pushCalls = runPass.mock.calls
|
|
2968
|
+
.map((call) => call[0])
|
|
2969
|
+
.filter((argv) => argv.includes("--company"));
|
|
2970
|
+
expect(pushCalls).toEqual([
|
|
2971
|
+
[
|
|
2972
|
+
"--company",
|
|
2973
|
+
"indigo",
|
|
2974
|
+
"--direction",
|
|
2975
|
+
"push",
|
|
2976
|
+
"--scope-path",
|
|
2977
|
+
"knowledge/a.md",
|
|
2978
|
+
"--hq-root",
|
|
2979
|
+
"/tmp/hq",
|
|
2980
|
+
],
|
|
2981
|
+
]);
|
|
2982
|
+
shutdown();
|
|
2983
|
+
tick();
|
|
2984
|
+
await loop;
|
|
2985
|
+
});
|
|
2986
|
+
it("drains an unlink path through the same scoped tombstone push path", async () => {
|
|
2987
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
2988
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
2989
|
+
await flushLoopMicrotasks();
|
|
2990
|
+
runPass.mockClear();
|
|
2991
|
+
watcher.emit("companies/indigo/knowledge/deleted.md");
|
|
2992
|
+
tick();
|
|
2993
|
+
await flushLoopMicrotasks();
|
|
2994
|
+
expect(runPass.mock.calls[0][0]).toEqual([
|
|
2995
|
+
"--company",
|
|
2996
|
+
"indigo",
|
|
2997
|
+
"--direction",
|
|
2998
|
+
"push",
|
|
2999
|
+
"--scope-path",
|
|
3000
|
+
"knowledge/deleted.md",
|
|
3001
|
+
"--hq-root",
|
|
3002
|
+
"/tmp/hq",
|
|
3003
|
+
]);
|
|
3004
|
+
shutdown();
|
|
3005
|
+
tick();
|
|
3006
|
+
await loop;
|
|
3007
|
+
});
|
|
3008
|
+
it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
|
|
3009
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3010
|
+
// No --direction flag → parseArgs defaults the run to pull-only. The drain
|
|
3011
|
+
// must honor that and NOT run a scoped push. Regression: the loop used to
|
|
3012
|
+
// re-parse argv and wrongly default an omitted direction to "both", so a
|
|
3013
|
+
// pull-only watcher would push local edits it should never send.
|
|
3014
|
+
const pullOnlyArgv = [
|
|
3015
|
+
"--companies",
|
|
3016
|
+
"--watch",
|
|
3017
|
+
"--event-push",
|
|
3018
|
+
"--hq-root",
|
|
3019
|
+
"/tmp/hq",
|
|
3020
|
+
"--poll-remote-ms",
|
|
3021
|
+
"60000",
|
|
3022
|
+
];
|
|
3023
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, pullOnlyArgv);
|
|
3024
|
+
await flushLoopMicrotasks();
|
|
3025
|
+
runPass.mockClear();
|
|
3026
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3027
|
+
tick();
|
|
3028
|
+
await flushLoopMicrotasks();
|
|
3029
|
+
const calls = runPass.mock.calls.map((call) => call[0]);
|
|
3030
|
+
// No scoped push of any kind ran...
|
|
3031
|
+
expect(calls.some((argv) => argv.includes("--company"))).toBe(false);
|
|
3032
|
+
expect(calls.some((argv) => argv.includes("--direction") &&
|
|
3033
|
+
argv[argv.indexOf("--direction") + 1] === "push")).toBe(false);
|
|
3034
|
+
// ...only the pull leg ran.
|
|
3035
|
+
expect(calls).toEqual([fullPullArgv]);
|
|
3036
|
+
shutdown();
|
|
3037
|
+
tick();
|
|
3038
|
+
await loop;
|
|
3039
|
+
});
|
|
3040
|
+
it("restores failed scoped push paths so the next tick retries them", async () => {
|
|
3041
|
+
const runPass = vi
|
|
3042
|
+
.fn()
|
|
3043
|
+
.mockResolvedValueOnce(0)
|
|
3044
|
+
.mockResolvedValueOnce(1)
|
|
3045
|
+
.mockResolvedValueOnce(0)
|
|
3046
|
+
.mockResolvedValueOnce(0)
|
|
3047
|
+
.mockResolvedValueOnce(0);
|
|
3048
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
3049
|
+
await flushLoopMicrotasks();
|
|
3050
|
+
runPass.mockClear();
|
|
3051
|
+
watcher.emit("companies/indigo/knowledge/retry.md");
|
|
3052
|
+
tick();
|
|
3053
|
+
await flushLoopMicrotasks();
|
|
3054
|
+
tick();
|
|
3055
|
+
await flushLoopMicrotasks();
|
|
3056
|
+
const pushCalls = runPass.mock.calls
|
|
3057
|
+
.map((call) => call[0])
|
|
3058
|
+
.filter((argv) => argv.includes("--company"));
|
|
3059
|
+
expect(pushCalls).toEqual([
|
|
3060
|
+
[
|
|
3061
|
+
"--company",
|
|
3062
|
+
"indigo",
|
|
3063
|
+
"--direction",
|
|
3064
|
+
"push",
|
|
3065
|
+
"--scope-path",
|
|
3066
|
+
"knowledge/retry.md",
|
|
3067
|
+
"--hq-root",
|
|
3068
|
+
"/tmp/hq",
|
|
3069
|
+
],
|
|
3070
|
+
[
|
|
3071
|
+
"--company",
|
|
3072
|
+
"indigo",
|
|
3073
|
+
"--direction",
|
|
3074
|
+
"push",
|
|
3075
|
+
"--scope-path",
|
|
3076
|
+
"knowledge/retry.md",
|
|
3077
|
+
"--hq-root",
|
|
3078
|
+
"/tmp/hq",
|
|
3079
|
+
],
|
|
3080
|
+
]);
|
|
3081
|
+
shutdown();
|
|
3082
|
+
tick();
|
|
3083
|
+
await loop;
|
|
3084
|
+
});
|
|
3085
|
+
it("empty scoped ticks run only the pull leg", async () => {
|
|
3086
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3087
|
+
const { loop, tick, shutdown } = startDrainLoop(runPass);
|
|
3088
|
+
await flushLoopMicrotasks();
|
|
3089
|
+
runPass.mockClear();
|
|
3090
|
+
tick();
|
|
3091
|
+
await flushLoopMicrotasks();
|
|
3092
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullPullArgv]);
|
|
3093
|
+
shutdown();
|
|
3094
|
+
tick();
|
|
3095
|
+
await loop;
|
|
3096
|
+
});
|
|
3097
|
+
it("personal scoped ticks preserve personal mode for the pull leg", async () => {
|
|
3098
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3099
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3100
|
+
"--personal",
|
|
3101
|
+
"--watch",
|
|
3102
|
+
"--event-push",
|
|
3103
|
+
"--direction",
|
|
3104
|
+
"both",
|
|
3105
|
+
"--hq-root",
|
|
3106
|
+
"/tmp/hq",
|
|
3107
|
+
"--poll-remote-ms",
|
|
3108
|
+
"60000",
|
|
3109
|
+
]);
|
|
3110
|
+
await flushLoopMicrotasks();
|
|
3111
|
+
runPass.mockClear();
|
|
3112
|
+
watcher.emit("knowledge/a.md");
|
|
3113
|
+
tick();
|
|
3114
|
+
await flushLoopMicrotasks();
|
|
3115
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3116
|
+
[
|
|
3117
|
+
"--personal",
|
|
3118
|
+
"--direction",
|
|
3119
|
+
"push",
|
|
3120
|
+
"--scope-path",
|
|
3121
|
+
"knowledge/a.md",
|
|
3122
|
+
"--hq-root",
|
|
3123
|
+
"/tmp/hq",
|
|
3124
|
+
],
|
|
3125
|
+
["--personal", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
3126
|
+
]);
|
|
3127
|
+
shutdown();
|
|
3128
|
+
tick();
|
|
3129
|
+
await loop;
|
|
3130
|
+
});
|
|
3131
|
+
it("single-company scoped ticks preserve company mode for the pull leg", async () => {
|
|
3132
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3133
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3134
|
+
"--company",
|
|
3135
|
+
"acme",
|
|
3136
|
+
"--watch",
|
|
3137
|
+
"--event-push",
|
|
3138
|
+
"--direction",
|
|
3139
|
+
"both",
|
|
3140
|
+
"--hq-root",
|
|
3141
|
+
"/tmp/hq",
|
|
3142
|
+
"--poll-remote-ms",
|
|
3143
|
+
"60000",
|
|
3144
|
+
]);
|
|
3145
|
+
await flushLoopMicrotasks();
|
|
3146
|
+
runPass.mockClear();
|
|
3147
|
+
watcher.emit("companies/acme/knowledge/a.md");
|
|
3148
|
+
tick();
|
|
3149
|
+
await flushLoopMicrotasks();
|
|
3150
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3151
|
+
[
|
|
3152
|
+
"--company",
|
|
3153
|
+
"acme",
|
|
3154
|
+
"--direction",
|
|
3155
|
+
"push",
|
|
3156
|
+
"--scope-path",
|
|
3157
|
+
"knowledge/a.md",
|
|
3158
|
+
"--hq-root",
|
|
3159
|
+
"/tmp/hq",
|
|
3160
|
+
],
|
|
3161
|
+
["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
3162
|
+
]);
|
|
3163
|
+
shutdown();
|
|
3164
|
+
tick();
|
|
3165
|
+
await loop;
|
|
3166
|
+
});
|
|
3167
|
+
it("push-only scoped ticks run the scoped push and no pull leg", async () => {
|
|
3168
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3169
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3170
|
+
"--companies",
|
|
3171
|
+
"--watch",
|
|
3172
|
+
"--event-push",
|
|
3173
|
+
"--direction",
|
|
3174
|
+
"push",
|
|
3175
|
+
"--hq-root",
|
|
3176
|
+
"/tmp/hq",
|
|
3177
|
+
"--poll-remote-ms",
|
|
3178
|
+
"60000",
|
|
3179
|
+
]);
|
|
3180
|
+
await flushLoopMicrotasks();
|
|
3181
|
+
runPass.mockClear();
|
|
3182
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3183
|
+
tick();
|
|
3184
|
+
await flushLoopMicrotasks();
|
|
3185
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3186
|
+
[
|
|
3187
|
+
"--company",
|
|
3188
|
+
"indigo",
|
|
3189
|
+
"--direction",
|
|
3190
|
+
"push",
|
|
3191
|
+
"--scope-path",
|
|
3192
|
+
"knowledge/a.md",
|
|
3193
|
+
"--hq-root",
|
|
3194
|
+
"/tmp/hq",
|
|
3195
|
+
],
|
|
3196
|
+
]);
|
|
3197
|
+
shutdown();
|
|
3198
|
+
tick();
|
|
3199
|
+
await loop;
|
|
3200
|
+
});
|
|
3201
|
+
it("overflow and every tenth tick run a full unscoped reconcile", async () => {
|
|
3202
|
+
const watcher = makeBatchWatcherStub();
|
|
3203
|
+
const sleep = makeSteppableSleep();
|
|
3204
|
+
let triggerShutdown = () => { };
|
|
3205
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3206
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
3207
|
+
runPass,
|
|
3208
|
+
clock: new FakeClock(),
|
|
3209
|
+
createWatcher: () => watcher,
|
|
3210
|
+
sleep: sleep.sleep,
|
|
3211
|
+
onShutdownSignal: (handler) => {
|
|
3212
|
+
triggerShutdown = handler;
|
|
3213
|
+
return () => { };
|
|
3214
|
+
},
|
|
3215
|
+
});
|
|
3216
|
+
await flushLoopMicrotasks();
|
|
3217
|
+
runPass.mockClear();
|
|
3218
|
+
watcher.emit("companies/indigo/a.md", {
|
|
3219
|
+
paths: new Map([["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"]]),
|
|
3220
|
+
overflowed: true,
|
|
3221
|
+
});
|
|
3222
|
+
sleep.tick();
|
|
3223
|
+
await flushLoopMicrotasks();
|
|
3224
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
3225
|
+
runPass.mockClear();
|
|
3226
|
+
for (let i = 0; i < 7; i++) {
|
|
3227
|
+
sleep.tick();
|
|
3228
|
+
await flushLoopMicrotasks();
|
|
3229
|
+
}
|
|
3230
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual(Array.from({ length: 7 }, () => fullPullArgv));
|
|
3231
|
+
runPass.mockClear();
|
|
3232
|
+
sleep.tick();
|
|
3233
|
+
await flushLoopMicrotasks();
|
|
3234
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
3235
|
+
triggerShutdown();
|
|
3236
|
+
sleep.tick();
|
|
3237
|
+
await loop;
|
|
3238
|
+
});
|
|
3239
|
+
});
|
|
2753
3240
|
// ---------------------------------------------------------------------------
|
|
2754
3241
|
// Re-initialize for each test (mock state hygiene)
|
|
2755
3242
|
// ---------------------------------------------------------------------------
|
|
@@ -3438,12 +3925,25 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3438
3925
|
const watcher = opts.watcher ?? makeBatchWatcherStub();
|
|
3439
3926
|
const runPass = opts.runPass ?? vi.fn().mockResolvedValue(0);
|
|
3440
3927
|
const clock = new FakeClock();
|
|
3928
|
+
const sleep = makeSteppableSleep();
|
|
3441
3929
|
let triggerShutdown = () => { };
|
|
3442
|
-
const loop = runRunnerWithLoop(
|
|
3930
|
+
const loop = runRunnerWithLoop(
|
|
3931
|
+
// `--direction both` so the push leg (and therefore the publish-after-push
|
|
3932
|
+
// wiring these tests exercise) actually runs. Omitting --direction is a
|
|
3933
|
+
// pull-only run by the parser default, which correctly never pushes.
|
|
3934
|
+
[
|
|
3935
|
+
"--companies",
|
|
3936
|
+
"--watch",
|
|
3937
|
+
"--event-push",
|
|
3938
|
+
"--direction",
|
|
3939
|
+
"both",
|
|
3940
|
+
"--hq-root",
|
|
3941
|
+
"/tmp/hq",
|
|
3942
|
+
], {
|
|
3443
3943
|
runPass,
|
|
3444
3944
|
clock,
|
|
3445
3945
|
createWatcher: () => watcher,
|
|
3446
|
-
sleep:
|
|
3946
|
+
sleep: sleep.sleep,
|
|
3447
3947
|
onShutdownSignal: (handler) => {
|
|
3448
3948
|
triggerShutdown = handler;
|
|
3449
3949
|
return () => { };
|
|
@@ -3453,7 +3953,14 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3453
3953
|
startEventSync: (opts.startEventSync ??
|
|
3454
3954
|
vi.fn().mockResolvedValue(null)),
|
|
3455
3955
|
});
|
|
3456
|
-
return {
|
|
3956
|
+
return {
|
|
3957
|
+
loop,
|
|
3958
|
+
watcher,
|
|
3959
|
+
runPass,
|
|
3960
|
+
clock,
|
|
3961
|
+
tick: sleep.tick,
|
|
3962
|
+
shutdown: () => triggerShutdown(),
|
|
3963
|
+
};
|
|
3457
3964
|
}
|
|
3458
3965
|
async function microtasks(n = 6) {
|
|
3459
3966
|
for (let i = 0; i < n; i++)
|
|
@@ -3505,7 +4012,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3505
4012
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
3506
4013
|
});
|
|
3507
4014
|
const runPass = vi.fn().mockResolvedValue(0);
|
|
3508
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
4015
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
3509
4016
|
claims: ENROLLED_CLAIMS,
|
|
3510
4017
|
startEventSync,
|
|
3511
4018
|
runPass,
|
|
@@ -3518,10 +4025,16 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3518
4025
|
watcher.emit("companies/indigo/a.md", batch);
|
|
3519
4026
|
clock.advance(0);
|
|
3520
4027
|
await microtasks();
|
|
3521
|
-
|
|
4028
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
4029
|
+
expect(publishBatch).not.toHaveBeenCalled();
|
|
4030
|
+
tick();
|
|
4031
|
+
await microtasks();
|
|
4032
|
+
// Scoped push drained and succeeded → batch published.
|
|
3522
4033
|
expect(runPass).toHaveBeenCalled();
|
|
3523
4034
|
expect(publishBatch).toHaveBeenCalledTimes(1);
|
|
3524
|
-
expect(publishBatch.mock.calls[0][0]).
|
|
4035
|
+
expect([...publishBatch.mock.calls[0][0].paths.values()]).toEqual([
|
|
4036
|
+
"companies/indigo/a.md",
|
|
4037
|
+
]);
|
|
3525
4038
|
shutdown();
|
|
3526
4039
|
await loop;
|
|
3527
4040
|
});
|
|
@@ -3538,7 +4051,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3538
4051
|
.fn()
|
|
3539
4052
|
.mockResolvedValueOnce(0)
|
|
3540
4053
|
.mockResolvedValue(1);
|
|
3541
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
4054
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
3542
4055
|
claims: ENROLLED_CLAIMS,
|
|
3543
4056
|
startEventSync,
|
|
3544
4057
|
runPass,
|
|
@@ -3547,6 +4060,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3547
4060
|
watcher.emit("companies/indigo/a.md", { paths: new Map([["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"]]) });
|
|
3548
4061
|
clock.advance(0);
|
|
3549
4062
|
await microtasks();
|
|
4063
|
+
tick();
|
|
4064
|
+
await microtasks();
|
|
3550
4065
|
expect(publishBatch).not.toHaveBeenCalled();
|
|
3551
4066
|
shutdown();
|
|
3552
4067
|
await loop;
|
|
@@ -3560,7 +4075,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3560
4075
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
3561
4076
|
});
|
|
3562
4077
|
const runPass = vi.fn().mockResolvedValue(0);
|
|
3563
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
4078
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
3564
4079
|
claims: ENROLLED_CLAIMS,
|
|
3565
4080
|
startEventSync,
|
|
3566
4081
|
runPass,
|
|
@@ -3576,6 +4091,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3576
4091
|
});
|
|
3577
4092
|
clock.advance(0);
|
|
3578
4093
|
await microtasks();
|
|
4094
|
+
tick();
|
|
4095
|
+
await microtasks();
|
|
3579
4096
|
const pushedRoutes = new Set();
|
|
3580
4097
|
let pushedEverything = false;
|
|
3581
4098
|
for (const call of runPass.mock.calls) {
|
|
@@ -3587,6 +4104,9 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3587
4104
|
if (companyIdx >= 0) {
|
|
3588
4105
|
pushedRoutes.add(`company:${passArgv[companyIdx + 1]}`);
|
|
3589
4106
|
}
|
|
4107
|
+
else if (passArgv.includes("--personal")) {
|
|
4108
|
+
pushedRoutes.add("personal");
|
|
4109
|
+
}
|
|
3590
4110
|
else if (passArgv.includes("--companies")) {
|
|
3591
4111
|
pushedEverything = true;
|
|
3592
4112
|
}
|
|
@@ -3622,6 +4142,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3622
4142
|
let receiverSync = null;
|
|
3623
4143
|
const watcher = makeBatchWatcherStub();
|
|
3624
4144
|
const clock = new FakeClock();
|
|
4145
|
+
const sleep = makeSteppableSleep();
|
|
3625
4146
|
let triggerShutdown = () => { };
|
|
3626
4147
|
let releasePoll = () => { };
|
|
3627
4148
|
const pollGate = new Promise((resolve) => {
|
|
@@ -3634,7 +4155,18 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3634
4155
|
await pollGate;
|
|
3635
4156
|
return 0;
|
|
3636
4157
|
});
|
|
3637
|
-
const loop = runRunnerWithLoop(
|
|
4158
|
+
const loop = runRunnerWithLoop(
|
|
4159
|
+
// `--direction both` so the queued watcher pushes actually run (an omitted
|
|
4160
|
+
// direction is pull-only by the parser default).
|
|
4161
|
+
[
|
|
4162
|
+
"--companies",
|
|
4163
|
+
"--watch",
|
|
4164
|
+
"--event-push",
|
|
4165
|
+
"--direction",
|
|
4166
|
+
"both",
|
|
4167
|
+
"--hq-root",
|
|
4168
|
+
"/tmp/hq",
|
|
4169
|
+
], {
|
|
3638
4170
|
runPass,
|
|
3639
4171
|
clock,
|
|
3640
4172
|
createWatcher: () => watcher,
|
|
@@ -3646,7 +4178,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3646
4178
|
dispose: async () => { },
|
|
3647
4179
|
};
|
|
3648
4180
|
},
|
|
3649
|
-
sleep:
|
|
4181
|
+
sleep: sleep.sleep,
|
|
3650
4182
|
onShutdownSignal: (handler) => {
|
|
3651
4183
|
triggerShutdown = handler;
|
|
3652
4184
|
return () => { };
|
|
@@ -3691,12 +4223,30 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3691
4223
|
releasePoll();
|
|
3692
4224
|
await remotePull;
|
|
3693
4225
|
await microtasks();
|
|
3694
|
-
|
|
4226
|
+
sleep.tick();
|
|
3695
4227
|
await microtasks();
|
|
3696
4228
|
const passArgvs = runPass.mock.calls.map((call) => call[0]);
|
|
3697
4229
|
expect(passArgvs).toEqual(expect.arrayContaining([
|
|
3698
|
-
[
|
|
3699
|
-
|
|
4230
|
+
[
|
|
4231
|
+
"--company",
|
|
4232
|
+
"indigo",
|
|
4233
|
+
"--direction",
|
|
4234
|
+
"push",
|
|
4235
|
+
"--scope-path",
|
|
4236
|
+
"a.md",
|
|
4237
|
+
"--hq-root",
|
|
4238
|
+
"/tmp/hq",
|
|
4239
|
+
],
|
|
4240
|
+
[
|
|
4241
|
+
"--company",
|
|
4242
|
+
"beta",
|
|
4243
|
+
"--direction",
|
|
4244
|
+
"push",
|
|
4245
|
+
"--scope-path",
|
|
4246
|
+
"b.md",
|
|
4247
|
+
"--hq-root",
|
|
4248
|
+
"/tmp/hq",
|
|
4249
|
+
],
|
|
3700
4250
|
["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
3701
4251
|
]));
|
|
3702
4252
|
const pushedRoutes = new Set();
|
|
@@ -3732,7 +4282,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3732
4282
|
ownDeviceId: "dev-test",
|
|
3733
4283
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
3734
4284
|
});
|
|
3735
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
4285
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
3736
4286
|
claims: ENROLLED_CLAIMS,
|
|
3737
4287
|
startEventSync,
|
|
3738
4288
|
});
|
|
@@ -3740,6 +4290,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
3740
4290
|
watcher.emit("companies/indigo/b.md"); // no batch
|
|
3741
4291
|
clock.advance(0);
|
|
3742
4292
|
await microtasks();
|
|
4293
|
+
tick();
|
|
4294
|
+
await microtasks();
|
|
3743
4295
|
expect(publishBatch).toHaveBeenCalledTimes(1);
|
|
3744
4296
|
const synthesized = publishBatch.mock.calls[0][0];
|
|
3745
4297
|
expect([...synthesized.paths.values()]).toEqual(["companies/indigo/b.md"]);
|