@indigoai-us/hq-cloud 6.13.5 → 6.14.0
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/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
|
@@ -36,6 +36,7 @@ import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
|
|
|
36
36
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
37
37
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
38
38
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
39
|
+
import { isCoveredByAny } from "../prefix-coalesce.js";
|
|
39
40
|
import type { SyncResult, SyncOptions } from "../cli/sync.js";
|
|
40
41
|
import type { ShareResult, ShareOptions } from "../cli/share.js";
|
|
41
42
|
import type {
|
|
@@ -1980,6 +1981,101 @@ describe("--direction", () => {
|
|
|
1980
1981
|
expect(opts.prefixSet).toBeUndefined();
|
|
1981
1982
|
});
|
|
1982
1983
|
|
|
1984
|
+
it("direction=push with --scope-path: forwards exact and descendant scoped prefixes to share()", async () => {
|
|
1985
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
1986
|
+
const deps = makeDeps({
|
|
1987
|
+
createVaultClient: () =>
|
|
1988
|
+
makeVaultStub({
|
|
1989
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1990
|
+
entityGet: (uid: string) =>
|
|
1991
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
1992
|
+
}),
|
|
1993
|
+
sync: vi.fn(),
|
|
1994
|
+
share: shareSpy,
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
await runRunner(
|
|
1998
|
+
[
|
|
1999
|
+
"--companies",
|
|
2000
|
+
"--direction",
|
|
2001
|
+
"push",
|
|
2002
|
+
"--hq-root",
|
|
2003
|
+
"/tmp/fake-hq",
|
|
2004
|
+
"--scope-path",
|
|
2005
|
+
"knowledge/a.md",
|
|
2006
|
+
],
|
|
2007
|
+
deps,
|
|
2008
|
+
);
|
|
2009
|
+
|
|
2010
|
+
const opts = (shareSpy.mock.calls[0] as [ShareOptions])[0];
|
|
2011
|
+
expect(opts.prefixSet).toEqual(["knowledge/a.md", "knowledge/a.md/"]);
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it("direction=push with directory --scope-path covers descendant keys for tombstones", async () => {
|
|
2015
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
2016
|
+
const deps = makeDeps({
|
|
2017
|
+
createVaultClient: () =>
|
|
2018
|
+
makeVaultStub({
|
|
2019
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
2020
|
+
entityGet: (uid: string) =>
|
|
2021
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
2022
|
+
}),
|
|
2023
|
+
sync: vi.fn(),
|
|
2024
|
+
share: shareSpy,
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
await runRunner(
|
|
2028
|
+
[
|
|
2029
|
+
"--companies",
|
|
2030
|
+
"--direction",
|
|
2031
|
+
"push",
|
|
2032
|
+
"--hq-root",
|
|
2033
|
+
"/tmp/fake-hq",
|
|
2034
|
+
"--scope-path",
|
|
2035
|
+
"knowledge/archive",
|
|
2036
|
+
],
|
|
2037
|
+
deps,
|
|
2038
|
+
);
|
|
2039
|
+
|
|
2040
|
+
const opts = (shareSpy.mock.calls[0] as [ShareOptions])[0];
|
|
2041
|
+
expect(opts.prefixSet).toBeDefined();
|
|
2042
|
+
expect(isCoveredByAny("knowledge/archive/a.md", opts.prefixSet ?? [])).toBe(
|
|
2043
|
+
true,
|
|
2044
|
+
);
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
it("direction=push with file --scope-path does not cover siblings", async () => {
|
|
2048
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
2049
|
+
const deps = makeDeps({
|
|
2050
|
+
createVaultClient: () =>
|
|
2051
|
+
makeVaultStub({
|
|
2052
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
2053
|
+
entityGet: (uid: string) =>
|
|
2054
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
2055
|
+
}),
|
|
2056
|
+
sync: vi.fn(),
|
|
2057
|
+
share: shareSpy,
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
await runRunner(
|
|
2061
|
+
[
|
|
2062
|
+
"--companies",
|
|
2063
|
+
"--direction",
|
|
2064
|
+
"push",
|
|
2065
|
+
"--hq-root",
|
|
2066
|
+
"/tmp/fake-hq",
|
|
2067
|
+
"--scope-path",
|
|
2068
|
+
"board.json",
|
|
2069
|
+
],
|
|
2070
|
+
deps,
|
|
2071
|
+
);
|
|
2072
|
+
|
|
2073
|
+
const opts = (shareSpy.mock.calls[0] as [ShareOptions])[0];
|
|
2074
|
+
expect(opts.prefixSet).toBeDefined();
|
|
2075
|
+
expect(isCoveredByAny("board.json", opts.prefixSet ?? [])).toBe(true);
|
|
2076
|
+
expect(isCoveredByAny("other.json", opts.prefixSet ?? [])).toBe(false);
|
|
2077
|
+
});
|
|
2078
|
+
|
|
1983
2079
|
it("direction=push (shared membership): forwards the resolved ACL prefixSet to share() (feedback_ded09d56)", async () => {
|
|
1984
2080
|
// Plumbing regression for the fresh fix: a member/guest's push must be
|
|
1985
2081
|
// scoped to their granted prefixes so out-of-scope keys are filtered
|
|
@@ -2015,6 +2111,92 @@ describe("--direction", () => {
|
|
|
2015
2111
|
expect(opts.prefixSet).toEqual(["knowledge/", "policies/"]);
|
|
2016
2112
|
});
|
|
2017
2113
|
|
|
2114
|
+
it("direction=push with --scope-path never widens a shared ACL prefixSet", async () => {
|
|
2115
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
2116
|
+
const client = {
|
|
2117
|
+
...makeVaultStub({
|
|
2118
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
2119
|
+
entityGet: (uid: string) =>
|
|
2120
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
2121
|
+
}),
|
|
2122
|
+
getMembershipSyncConfig: async () => ({
|
|
2123
|
+
membershipId: "mk_a",
|
|
2124
|
+
syncMode: "shared",
|
|
2125
|
+
isDefault: false,
|
|
2126
|
+
}),
|
|
2127
|
+
listMyExplicitGrants: async () => [
|
|
2128
|
+
{ companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
|
|
2129
|
+
],
|
|
2130
|
+
} as unknown as ReturnType<typeof makeVaultStub>;
|
|
2131
|
+
const deps = makeDeps({
|
|
2132
|
+
createVaultClient: () => client,
|
|
2133
|
+
sync: vi.fn(),
|
|
2134
|
+
share: shareSpy,
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
await runRunner(
|
|
2138
|
+
[
|
|
2139
|
+
"--companies",
|
|
2140
|
+
"--direction",
|
|
2141
|
+
"push",
|
|
2142
|
+
"--hq-root",
|
|
2143
|
+
"/tmp/fake-hq",
|
|
2144
|
+
"--scope-path",
|
|
2145
|
+
"knowledge/a.md",
|
|
2146
|
+
"--scope-path",
|
|
2147
|
+
"secrets/b.md",
|
|
2148
|
+
],
|
|
2149
|
+
deps,
|
|
2150
|
+
);
|
|
2151
|
+
|
|
2152
|
+
const opts = (shareSpy.mock.calls[0] as [ShareOptions])[0];
|
|
2153
|
+
expect(opts.prefixSet).toEqual(["knowledge/a.md", "knowledge/a.md/"]);
|
|
2154
|
+
expect(isCoveredByAny("secrets/b.md", opts.prefixSet ?? [])).toBe(false);
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
it("direction=push with directory --scope-path never widens outside a shared ACL prefixSet", async () => {
|
|
2158
|
+
const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
|
|
2159
|
+
const client = {
|
|
2160
|
+
...makeVaultStub({
|
|
2161
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
2162
|
+
entityGet: (uid: string) =>
|
|
2163
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
2164
|
+
}),
|
|
2165
|
+
getMembershipSyncConfig: async () => ({
|
|
2166
|
+
membershipId: "mk_a",
|
|
2167
|
+
syncMode: "shared",
|
|
2168
|
+
isDefault: false,
|
|
2169
|
+
}),
|
|
2170
|
+
listMyExplicitGrants: async () => [
|
|
2171
|
+
{ companyUid: "cmp_a", path: "policies/", permission: "read", source: "person" },
|
|
2172
|
+
],
|
|
2173
|
+
} as unknown as ReturnType<typeof makeVaultStub>;
|
|
2174
|
+
const deps = makeDeps({
|
|
2175
|
+
createVaultClient: () => client,
|
|
2176
|
+
sync: vi.fn(),
|
|
2177
|
+
share: shareSpy,
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
await runRunner(
|
|
2181
|
+
[
|
|
2182
|
+
"--companies",
|
|
2183
|
+
"--direction",
|
|
2184
|
+
"push",
|
|
2185
|
+
"--hq-root",
|
|
2186
|
+
"/tmp/fake-hq",
|
|
2187
|
+
"--scope-path",
|
|
2188
|
+
"knowledge/archive",
|
|
2189
|
+
],
|
|
2190
|
+
deps,
|
|
2191
|
+
);
|
|
2192
|
+
|
|
2193
|
+
const opts = (shareSpy.mock.calls[0] as [ShareOptions])[0];
|
|
2194
|
+
expect(isCoveredByAny("knowledge/archive/a.md", opts.prefixSet ?? [])).toBe(
|
|
2195
|
+
false,
|
|
2196
|
+
);
|
|
2197
|
+
expect(isCoveredByAny("policies/a.md", opts.prefixSet ?? [])).toBe(false);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2018
2200
|
it("direction=both: all-complete sums uploaded and downloaded across companies", async () => {
|
|
2019
2201
|
const slugs: Record<string, string> = { cmp_a: "acme", cmp_b: "beta" };
|
|
2020
2202
|
const deps = makeDeps({
|
|
@@ -2992,6 +3174,25 @@ function makeWatcherStub(): WatcherStub {
|
|
|
2992
3174
|
return stub;
|
|
2993
3175
|
}
|
|
2994
3176
|
|
|
3177
|
+
async function flushLoopMicrotasks(n = 8) {
|
|
3178
|
+
for (let i = 0; i < n; i++) await Promise.resolve();
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function makeSteppableSleep() {
|
|
3182
|
+
const pending: Array<() => void> = [];
|
|
3183
|
+
return {
|
|
3184
|
+
sleep: () =>
|
|
3185
|
+
new Promise<void>((resolve) => {
|
|
3186
|
+
pending.push(resolve);
|
|
3187
|
+
}),
|
|
3188
|
+
tick() {
|
|
3189
|
+
const resolve = pending.shift();
|
|
3190
|
+
if (!resolve) throw new Error("poll sleep was not pending");
|
|
3191
|
+
resolve();
|
|
3192
|
+
},
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
|
|
2995
3196
|
describe("runRunnerWithLoop — event-push wiring", () => {
|
|
2996
3197
|
it("starts the watcher alongside the poll loop when --event-push is on", async () => {
|
|
2997
3198
|
const watcher = makeWatcherStub();
|
|
@@ -3023,7 +3224,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3023
3224
|
expect(watcher.disposed).toBe(true);
|
|
3024
3225
|
});
|
|
3025
3226
|
|
|
3026
|
-
it("trigger-on-change: a
|
|
3227
|
+
it("trigger-on-change: a changed signal accumulates without an immediate push", async () => {
|
|
3027
3228
|
const watcher = makeWatcherStub();
|
|
3028
3229
|
const clock = new FakeClock();
|
|
3029
3230
|
let triggerShutdown = () => {};
|
|
@@ -3054,24 +3255,13 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3054
3255
|
await Promise.resolve();
|
|
3055
3256
|
await Promise.resolve();
|
|
3056
3257
|
|
|
3057
|
-
|
|
3058
|
-
(c[0] as string[]).includes("--company"),
|
|
3059
|
-
);
|
|
3060
|
-
expect(targetedCall).toBeDefined();
|
|
3061
|
-
expect(targetedCall![0]).toEqual([
|
|
3062
|
-
"--company",
|
|
3063
|
-
"indigo",
|
|
3064
|
-
"--direction",
|
|
3065
|
-
"push",
|
|
3066
|
-
"--hq-root",
|
|
3067
|
-
"/tmp/hq",
|
|
3068
|
-
]);
|
|
3258
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
3069
3259
|
|
|
3070
3260
|
triggerShutdown();
|
|
3071
3261
|
await loop;
|
|
3072
3262
|
});
|
|
3073
3263
|
|
|
3074
|
-
it("bare signal (no path)
|
|
3264
|
+
it("bare signal (no path) accumulates without an immediate push", async () => {
|
|
3075
3265
|
const watcher = makeWatcherStub();
|
|
3076
3266
|
const clock = new FakeClock();
|
|
3077
3267
|
let triggerShutdown = () => {};
|
|
@@ -3100,19 +3290,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3100
3290
|
await Promise.resolve();
|
|
3101
3291
|
await Promise.resolve();
|
|
3102
3292
|
|
|
3103
|
-
|
|
3104
|
-
(c) =>
|
|
3105
|
-
(c[0] as string[]).includes("--companies") &&
|
|
3106
|
-
(c[0] as string[]).includes("--direction"),
|
|
3107
|
-
);
|
|
3108
|
-
expect(personalCall).toBeDefined();
|
|
3109
|
-
expect(personalCall![0]).toEqual([
|
|
3110
|
-
"--companies",
|
|
3111
|
-
"--direction",
|
|
3112
|
-
"push",
|
|
3113
|
-
"--hq-root",
|
|
3114
|
-
"/tmp/hq",
|
|
3115
|
-
]);
|
|
3293
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
3116
3294
|
|
|
3117
3295
|
triggerShutdown();
|
|
3118
3296
|
await loop;
|
|
@@ -3182,7 +3360,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3182
3360
|
await loop;
|
|
3183
3361
|
});
|
|
3184
3362
|
|
|
3185
|
-
it("F05: guard-held watcher
|
|
3363
|
+
it("F05: guard-held watcher changes stay accumulated until a poll drain", async () => {
|
|
3186
3364
|
const watcher = makeWatcherStub();
|
|
3187
3365
|
const clock = new FakeClock();
|
|
3188
3366
|
let triggerShutdown = () => {};
|
|
@@ -3239,14 +3417,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3239
3417
|
const targetedCall = runPass.mock.calls.find((c) =>
|
|
3240
3418
|
(c[0] as string[]).includes("--company"),
|
|
3241
3419
|
);
|
|
3242
|
-
expect(targetedCall
|
|
3243
|
-
"--company",
|
|
3244
|
-
"indigo",
|
|
3245
|
-
"--direction",
|
|
3246
|
-
"push",
|
|
3247
|
-
"--hq-root",
|
|
3248
|
-
"/tmp/hq",
|
|
3249
|
-
]);
|
|
3420
|
+
expect(targetedCall).toBeUndefined();
|
|
3250
3421
|
});
|
|
3251
3422
|
|
|
3252
3423
|
it("poll-still-runs: the poll loop fires passes independent of the watcher", async () => {
|
|
@@ -3408,6 +3579,419 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3408
3579
|
});
|
|
3409
3580
|
});
|
|
3410
3581
|
|
|
3582
|
+
describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
3583
|
+
const watchArgv = [
|
|
3584
|
+
"--companies",
|
|
3585
|
+
"--watch",
|
|
3586
|
+
"--event-push",
|
|
3587
|
+
"--direction",
|
|
3588
|
+
"both",
|
|
3589
|
+
"--hq-root",
|
|
3590
|
+
"/tmp/hq",
|
|
3591
|
+
"--poll-remote-ms",
|
|
3592
|
+
"60000",
|
|
3593
|
+
];
|
|
3594
|
+
const fullArgv = ["--companies", "--direction", "both", "--hq-root", "/tmp/hq"];
|
|
3595
|
+
const fullPullArgv = [
|
|
3596
|
+
"--companies",
|
|
3597
|
+
"--direction",
|
|
3598
|
+
"pull",
|
|
3599
|
+
"--hq-root",
|
|
3600
|
+
"/tmp/hq",
|
|
3601
|
+
];
|
|
3602
|
+
|
|
3603
|
+
function startDrainLoop(runPass: ReturnType<typeof vi.fn>) {
|
|
3604
|
+
return startDrainLoopWithArgv(runPass, watchArgv);
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
function startDrainLoopWithArgv(
|
|
3608
|
+
runPass: ReturnType<typeof vi.fn>,
|
|
3609
|
+
argv: string[],
|
|
3610
|
+
) {
|
|
3611
|
+
const watcher = makeWatcherStub();
|
|
3612
|
+
const sleep = makeSteppableSleep();
|
|
3613
|
+
let triggerShutdown = () => {};
|
|
3614
|
+
const loop = runRunnerWithLoop(argv, {
|
|
3615
|
+
runPass,
|
|
3616
|
+
clock: new FakeClock(),
|
|
3617
|
+
createWatcher: () => watcher,
|
|
3618
|
+
sleep: sleep.sleep,
|
|
3619
|
+
onShutdownSignal: (handler) => {
|
|
3620
|
+
triggerShutdown = handler;
|
|
3621
|
+
return () => {};
|
|
3622
|
+
},
|
|
3623
|
+
});
|
|
3624
|
+
return {
|
|
3625
|
+
loop,
|
|
3626
|
+
watcher,
|
|
3627
|
+
tick: sleep.tick,
|
|
3628
|
+
shutdown: () => triggerShutdown(),
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
it("startup runs one full reconcile, then watcher edits wait for the poll drain", async () => {
|
|
3633
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3634
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
3635
|
+
await flushLoopMicrotasks();
|
|
3636
|
+
|
|
3637
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
3638
|
+
runPass.mockClear();
|
|
3639
|
+
|
|
3640
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3641
|
+
await flushLoopMicrotasks();
|
|
3642
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
3643
|
+
|
|
3644
|
+
tick();
|
|
3645
|
+
await flushLoopMicrotasks();
|
|
3646
|
+
|
|
3647
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3648
|
+
[
|
|
3649
|
+
"--company",
|
|
3650
|
+
"indigo",
|
|
3651
|
+
"--direction",
|
|
3652
|
+
"push",
|
|
3653
|
+
"--scope-path",
|
|
3654
|
+
"knowledge/a.md",
|
|
3655
|
+
"--hq-root",
|
|
3656
|
+
"/tmp/hq",
|
|
3657
|
+
],
|
|
3658
|
+
fullPullArgv,
|
|
3659
|
+
]);
|
|
3660
|
+
|
|
3661
|
+
shutdown();
|
|
3662
|
+
tick();
|
|
3663
|
+
await loop;
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
it("coalesces repeated edits to the same path into one scoped push", async () => {
|
|
3667
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3668
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
3669
|
+
await flushLoopMicrotasks();
|
|
3670
|
+
runPass.mockClear();
|
|
3671
|
+
|
|
3672
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3673
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3674
|
+
tick();
|
|
3675
|
+
await flushLoopMicrotasks();
|
|
3676
|
+
|
|
3677
|
+
const pushCalls = runPass.mock.calls
|
|
3678
|
+
.map((call) => call[0] as string[])
|
|
3679
|
+
.filter((argv) => argv.includes("--company"));
|
|
3680
|
+
expect(pushCalls).toEqual([
|
|
3681
|
+
[
|
|
3682
|
+
"--company",
|
|
3683
|
+
"indigo",
|
|
3684
|
+
"--direction",
|
|
3685
|
+
"push",
|
|
3686
|
+
"--scope-path",
|
|
3687
|
+
"knowledge/a.md",
|
|
3688
|
+
"--hq-root",
|
|
3689
|
+
"/tmp/hq",
|
|
3690
|
+
],
|
|
3691
|
+
]);
|
|
3692
|
+
|
|
3693
|
+
shutdown();
|
|
3694
|
+
tick();
|
|
3695
|
+
await loop;
|
|
3696
|
+
});
|
|
3697
|
+
|
|
3698
|
+
it("drains an unlink path through the same scoped tombstone push path", async () => {
|
|
3699
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3700
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
3701
|
+
await flushLoopMicrotasks();
|
|
3702
|
+
runPass.mockClear();
|
|
3703
|
+
|
|
3704
|
+
watcher.emit("companies/indigo/knowledge/deleted.md");
|
|
3705
|
+
tick();
|
|
3706
|
+
await flushLoopMicrotasks();
|
|
3707
|
+
|
|
3708
|
+
expect(runPass.mock.calls[0][0]).toEqual([
|
|
3709
|
+
"--company",
|
|
3710
|
+
"indigo",
|
|
3711
|
+
"--direction",
|
|
3712
|
+
"push",
|
|
3713
|
+
"--scope-path",
|
|
3714
|
+
"knowledge/deleted.md",
|
|
3715
|
+
"--hq-root",
|
|
3716
|
+
"/tmp/hq",
|
|
3717
|
+
]);
|
|
3718
|
+
|
|
3719
|
+
shutdown();
|
|
3720
|
+
tick();
|
|
3721
|
+
await loop;
|
|
3722
|
+
});
|
|
3723
|
+
|
|
3724
|
+
it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
|
|
3725
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3726
|
+
// No --direction flag → parseArgs defaults the run to pull-only. The drain
|
|
3727
|
+
// must honor that and NOT run a scoped push. Regression: the loop used to
|
|
3728
|
+
// re-parse argv and wrongly default an omitted direction to "both", so a
|
|
3729
|
+
// pull-only watcher would push local edits it should never send.
|
|
3730
|
+
const pullOnlyArgv = [
|
|
3731
|
+
"--companies",
|
|
3732
|
+
"--watch",
|
|
3733
|
+
"--event-push",
|
|
3734
|
+
"--hq-root",
|
|
3735
|
+
"/tmp/hq",
|
|
3736
|
+
"--poll-remote-ms",
|
|
3737
|
+
"60000",
|
|
3738
|
+
];
|
|
3739
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(
|
|
3740
|
+
runPass,
|
|
3741
|
+
pullOnlyArgv,
|
|
3742
|
+
);
|
|
3743
|
+
await flushLoopMicrotasks();
|
|
3744
|
+
runPass.mockClear();
|
|
3745
|
+
|
|
3746
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3747
|
+
tick();
|
|
3748
|
+
await flushLoopMicrotasks();
|
|
3749
|
+
|
|
3750
|
+
const calls = runPass.mock.calls.map((call) => call[0] as string[]);
|
|
3751
|
+
// No scoped push of any kind ran...
|
|
3752
|
+
expect(calls.some((argv) => argv.includes("--company"))).toBe(false);
|
|
3753
|
+
expect(
|
|
3754
|
+
calls.some(
|
|
3755
|
+
(argv) =>
|
|
3756
|
+
argv.includes("--direction") &&
|
|
3757
|
+
argv[argv.indexOf("--direction") + 1] === "push",
|
|
3758
|
+
),
|
|
3759
|
+
).toBe(false);
|
|
3760
|
+
// ...only the pull leg ran.
|
|
3761
|
+
expect(calls).toEqual([fullPullArgv]);
|
|
3762
|
+
|
|
3763
|
+
shutdown();
|
|
3764
|
+
tick();
|
|
3765
|
+
await loop;
|
|
3766
|
+
});
|
|
3767
|
+
|
|
3768
|
+
it("restores failed scoped push paths so the next tick retries them", async () => {
|
|
3769
|
+
const runPass = vi
|
|
3770
|
+
.fn()
|
|
3771
|
+
.mockResolvedValueOnce(0)
|
|
3772
|
+
.mockResolvedValueOnce(1)
|
|
3773
|
+
.mockResolvedValueOnce(0)
|
|
3774
|
+
.mockResolvedValueOnce(0)
|
|
3775
|
+
.mockResolvedValueOnce(0);
|
|
3776
|
+
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
3777
|
+
await flushLoopMicrotasks();
|
|
3778
|
+
runPass.mockClear();
|
|
3779
|
+
|
|
3780
|
+
watcher.emit("companies/indigo/knowledge/retry.md");
|
|
3781
|
+
tick();
|
|
3782
|
+
await flushLoopMicrotasks();
|
|
3783
|
+
tick();
|
|
3784
|
+
await flushLoopMicrotasks();
|
|
3785
|
+
|
|
3786
|
+
const pushCalls = runPass.mock.calls
|
|
3787
|
+
.map((call) => call[0] as string[])
|
|
3788
|
+
.filter((argv) => argv.includes("--company"));
|
|
3789
|
+
expect(pushCalls).toEqual([
|
|
3790
|
+
[
|
|
3791
|
+
"--company",
|
|
3792
|
+
"indigo",
|
|
3793
|
+
"--direction",
|
|
3794
|
+
"push",
|
|
3795
|
+
"--scope-path",
|
|
3796
|
+
"knowledge/retry.md",
|
|
3797
|
+
"--hq-root",
|
|
3798
|
+
"/tmp/hq",
|
|
3799
|
+
],
|
|
3800
|
+
[
|
|
3801
|
+
"--company",
|
|
3802
|
+
"indigo",
|
|
3803
|
+
"--direction",
|
|
3804
|
+
"push",
|
|
3805
|
+
"--scope-path",
|
|
3806
|
+
"knowledge/retry.md",
|
|
3807
|
+
"--hq-root",
|
|
3808
|
+
"/tmp/hq",
|
|
3809
|
+
],
|
|
3810
|
+
]);
|
|
3811
|
+
|
|
3812
|
+
shutdown();
|
|
3813
|
+
tick();
|
|
3814
|
+
await loop;
|
|
3815
|
+
});
|
|
3816
|
+
|
|
3817
|
+
it("empty scoped ticks run only the pull leg", async () => {
|
|
3818
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3819
|
+
const { loop, tick, shutdown } = startDrainLoop(runPass);
|
|
3820
|
+
await flushLoopMicrotasks();
|
|
3821
|
+
runPass.mockClear();
|
|
3822
|
+
|
|
3823
|
+
tick();
|
|
3824
|
+
await flushLoopMicrotasks();
|
|
3825
|
+
|
|
3826
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullPullArgv]);
|
|
3827
|
+
|
|
3828
|
+
shutdown();
|
|
3829
|
+
tick();
|
|
3830
|
+
await loop;
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
it("personal scoped ticks preserve personal mode for the pull leg", async () => {
|
|
3834
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3835
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3836
|
+
"--personal",
|
|
3837
|
+
"--watch",
|
|
3838
|
+
"--event-push",
|
|
3839
|
+
"--direction",
|
|
3840
|
+
"both",
|
|
3841
|
+
"--hq-root",
|
|
3842
|
+
"/tmp/hq",
|
|
3843
|
+
"--poll-remote-ms",
|
|
3844
|
+
"60000",
|
|
3845
|
+
]);
|
|
3846
|
+
await flushLoopMicrotasks();
|
|
3847
|
+
runPass.mockClear();
|
|
3848
|
+
|
|
3849
|
+
watcher.emit("knowledge/a.md");
|
|
3850
|
+
tick();
|
|
3851
|
+
await flushLoopMicrotasks();
|
|
3852
|
+
|
|
3853
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3854
|
+
[
|
|
3855
|
+
"--personal",
|
|
3856
|
+
"--direction",
|
|
3857
|
+
"push",
|
|
3858
|
+
"--scope-path",
|
|
3859
|
+
"knowledge/a.md",
|
|
3860
|
+
"--hq-root",
|
|
3861
|
+
"/tmp/hq",
|
|
3862
|
+
],
|
|
3863
|
+
["--personal", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
3864
|
+
]);
|
|
3865
|
+
|
|
3866
|
+
shutdown();
|
|
3867
|
+
tick();
|
|
3868
|
+
await loop;
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
it("single-company scoped ticks preserve company mode for the pull leg", async () => {
|
|
3872
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3873
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3874
|
+
"--company",
|
|
3875
|
+
"acme",
|
|
3876
|
+
"--watch",
|
|
3877
|
+
"--event-push",
|
|
3878
|
+
"--direction",
|
|
3879
|
+
"both",
|
|
3880
|
+
"--hq-root",
|
|
3881
|
+
"/tmp/hq",
|
|
3882
|
+
"--poll-remote-ms",
|
|
3883
|
+
"60000",
|
|
3884
|
+
]);
|
|
3885
|
+
await flushLoopMicrotasks();
|
|
3886
|
+
runPass.mockClear();
|
|
3887
|
+
|
|
3888
|
+
watcher.emit("companies/acme/knowledge/a.md");
|
|
3889
|
+
tick();
|
|
3890
|
+
await flushLoopMicrotasks();
|
|
3891
|
+
|
|
3892
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3893
|
+
[
|
|
3894
|
+
"--company",
|
|
3895
|
+
"acme",
|
|
3896
|
+
"--direction",
|
|
3897
|
+
"push",
|
|
3898
|
+
"--scope-path",
|
|
3899
|
+
"knowledge/a.md",
|
|
3900
|
+
"--hq-root",
|
|
3901
|
+
"/tmp/hq",
|
|
3902
|
+
],
|
|
3903
|
+
["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
3904
|
+
]);
|
|
3905
|
+
|
|
3906
|
+
shutdown();
|
|
3907
|
+
tick();
|
|
3908
|
+
await loop;
|
|
3909
|
+
});
|
|
3910
|
+
|
|
3911
|
+
it("push-only scoped ticks run the scoped push and no pull leg", async () => {
|
|
3912
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3913
|
+
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
3914
|
+
"--companies",
|
|
3915
|
+
"--watch",
|
|
3916
|
+
"--event-push",
|
|
3917
|
+
"--direction",
|
|
3918
|
+
"push",
|
|
3919
|
+
"--hq-root",
|
|
3920
|
+
"/tmp/hq",
|
|
3921
|
+
"--poll-remote-ms",
|
|
3922
|
+
"60000",
|
|
3923
|
+
]);
|
|
3924
|
+
await flushLoopMicrotasks();
|
|
3925
|
+
runPass.mockClear();
|
|
3926
|
+
|
|
3927
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
3928
|
+
tick();
|
|
3929
|
+
await flushLoopMicrotasks();
|
|
3930
|
+
|
|
3931
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([
|
|
3932
|
+
[
|
|
3933
|
+
"--company",
|
|
3934
|
+
"indigo",
|
|
3935
|
+
"--direction",
|
|
3936
|
+
"push",
|
|
3937
|
+
"--scope-path",
|
|
3938
|
+
"knowledge/a.md",
|
|
3939
|
+
"--hq-root",
|
|
3940
|
+
"/tmp/hq",
|
|
3941
|
+
],
|
|
3942
|
+
]);
|
|
3943
|
+
|
|
3944
|
+
shutdown();
|
|
3945
|
+
tick();
|
|
3946
|
+
await loop;
|
|
3947
|
+
});
|
|
3948
|
+
|
|
3949
|
+
it("overflow and every tenth tick run a full unscoped reconcile", async () => {
|
|
3950
|
+
const watcher = makeBatchWatcherStub();
|
|
3951
|
+
const sleep = makeSteppableSleep();
|
|
3952
|
+
let triggerShutdown = () => {};
|
|
3953
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
3954
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
3955
|
+
runPass,
|
|
3956
|
+
clock: new FakeClock(),
|
|
3957
|
+
createWatcher: () => watcher,
|
|
3958
|
+
sleep: sleep.sleep,
|
|
3959
|
+
onShutdownSignal: (handler) => {
|
|
3960
|
+
triggerShutdown = handler;
|
|
3961
|
+
return () => {};
|
|
3962
|
+
},
|
|
3963
|
+
});
|
|
3964
|
+
await flushLoopMicrotasks();
|
|
3965
|
+
runPass.mockClear();
|
|
3966
|
+
|
|
3967
|
+
watcher.emit("companies/indigo/a.md", {
|
|
3968
|
+
paths: new Map([["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"]]),
|
|
3969
|
+
overflowed: true,
|
|
3970
|
+
} as never);
|
|
3971
|
+
sleep.tick();
|
|
3972
|
+
await flushLoopMicrotasks();
|
|
3973
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
3974
|
+
|
|
3975
|
+
runPass.mockClear();
|
|
3976
|
+
for (let i = 0; i < 7; i++) {
|
|
3977
|
+
sleep.tick();
|
|
3978
|
+
await flushLoopMicrotasks();
|
|
3979
|
+
}
|
|
3980
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual(
|
|
3981
|
+
Array.from({ length: 7 }, () => fullPullArgv),
|
|
3982
|
+
);
|
|
3983
|
+
|
|
3984
|
+
runPass.mockClear();
|
|
3985
|
+
sleep.tick();
|
|
3986
|
+
await flushLoopMicrotasks();
|
|
3987
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
3988
|
+
|
|
3989
|
+
triggerShutdown();
|
|
3990
|
+
sleep.tick();
|
|
3991
|
+
await loop;
|
|
3992
|
+
});
|
|
3993
|
+
});
|
|
3994
|
+
|
|
3411
3995
|
// ---------------------------------------------------------------------------
|
|
3412
3996
|
// Re-initialize for each test (mock state hygiene)
|
|
3413
3997
|
// ---------------------------------------------------------------------------
|
|
@@ -4303,14 +4887,26 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4303
4887
|
const watcher = opts.watcher ?? makeBatchWatcherStub();
|
|
4304
4888
|
const runPass = opts.runPass ?? vi.fn().mockResolvedValue(0);
|
|
4305
4889
|
const clock = new FakeClock();
|
|
4890
|
+
const sleep = makeSteppableSleep();
|
|
4306
4891
|
let triggerShutdown = () => {};
|
|
4307
4892
|
const loop = runRunnerWithLoop(
|
|
4308
|
-
|
|
4893
|
+
// `--direction both` so the push leg (and therefore the publish-after-push
|
|
4894
|
+
// wiring these tests exercise) actually runs. Omitting --direction is a
|
|
4895
|
+
// pull-only run by the parser default, which correctly never pushes.
|
|
4896
|
+
[
|
|
4897
|
+
"--companies",
|
|
4898
|
+
"--watch",
|
|
4899
|
+
"--event-push",
|
|
4900
|
+
"--direction",
|
|
4901
|
+
"both",
|
|
4902
|
+
"--hq-root",
|
|
4903
|
+
"/tmp/hq",
|
|
4904
|
+
],
|
|
4309
4905
|
{
|
|
4310
4906
|
runPass,
|
|
4311
4907
|
clock,
|
|
4312
4908
|
createWatcher: () => watcher,
|
|
4313
|
-
sleep:
|
|
4909
|
+
sleep: sleep.sleep,
|
|
4314
4910
|
onShutdownSignal: (handler) => {
|
|
4315
4911
|
triggerShutdown = handler;
|
|
4316
4912
|
return () => {};
|
|
@@ -4321,7 +4917,14 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4321
4917
|
vi.fn().mockResolvedValue(null)) as never,
|
|
4322
4918
|
},
|
|
4323
4919
|
);
|
|
4324
|
-
return {
|
|
4920
|
+
return {
|
|
4921
|
+
loop,
|
|
4922
|
+
watcher,
|
|
4923
|
+
runPass,
|
|
4924
|
+
clock,
|
|
4925
|
+
tick: sleep.tick,
|
|
4926
|
+
shutdown: () => triggerShutdown(),
|
|
4927
|
+
};
|
|
4325
4928
|
}
|
|
4326
4929
|
|
|
4327
4930
|
async function microtasks(n = 6) {
|
|
@@ -4377,7 +4980,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4377
4980
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
4378
4981
|
});
|
|
4379
4982
|
const runPass = vi.fn().mockResolvedValue(0);
|
|
4380
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
4983
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
4381
4984
|
claims: ENROLLED_CLAIMS,
|
|
4382
4985
|
startEventSync,
|
|
4383
4986
|
runPass,
|
|
@@ -4391,11 +4994,18 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4391
4994
|
watcher.emit("companies/indigo/a.md", batch);
|
|
4392
4995
|
clock.advance(0);
|
|
4393
4996
|
await microtasks();
|
|
4997
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
4998
|
+
expect(publishBatch).not.toHaveBeenCalled();
|
|
4999
|
+
|
|
5000
|
+
tick();
|
|
5001
|
+
await microtasks();
|
|
4394
5002
|
|
|
4395
|
-
//
|
|
5003
|
+
// Scoped push drained and succeeded → batch published.
|
|
4396
5004
|
expect(runPass).toHaveBeenCalled();
|
|
4397
5005
|
expect(publishBatch).toHaveBeenCalledTimes(1);
|
|
4398
|
-
expect(publishBatch.mock.calls[0][0]).
|
|
5006
|
+
expect([...publishBatch.mock.calls[0][0].paths.values()]).toEqual([
|
|
5007
|
+
"companies/indigo/a.md",
|
|
5008
|
+
]);
|
|
4399
5009
|
|
|
4400
5010
|
shutdown();
|
|
4401
5011
|
await loop;
|
|
@@ -4414,7 +5024,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4414
5024
|
.fn()
|
|
4415
5025
|
.mockResolvedValueOnce(0)
|
|
4416
5026
|
.mockResolvedValue(1);
|
|
4417
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
5027
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
4418
5028
|
claims: ENROLLED_CLAIMS,
|
|
4419
5029
|
startEventSync,
|
|
4420
5030
|
runPass,
|
|
@@ -4427,6 +5037,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4427
5037
|
);
|
|
4428
5038
|
clock.advance(0);
|
|
4429
5039
|
await microtasks();
|
|
5040
|
+
tick();
|
|
5041
|
+
await microtasks();
|
|
4430
5042
|
|
|
4431
5043
|
expect(publishBatch).not.toHaveBeenCalled();
|
|
4432
5044
|
shutdown();
|
|
@@ -4442,7 +5054,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4442
5054
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
4443
5055
|
});
|
|
4444
5056
|
const runPass = vi.fn().mockResolvedValue(0);
|
|
4445
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
5057
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
4446
5058
|
claims: ENROLLED_CLAIMS,
|
|
4447
5059
|
startEventSync,
|
|
4448
5060
|
runPass,
|
|
@@ -4459,6 +5071,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4459
5071
|
});
|
|
4460
5072
|
clock.advance(0);
|
|
4461
5073
|
await microtasks();
|
|
5074
|
+
tick();
|
|
5075
|
+
await microtasks();
|
|
4462
5076
|
|
|
4463
5077
|
const pushedRoutes = new Set<string>();
|
|
4464
5078
|
let pushedEverything = false;
|
|
@@ -4469,6 +5083,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4469
5083
|
const companyIdx = passArgv.indexOf("--company");
|
|
4470
5084
|
if (companyIdx >= 0) {
|
|
4471
5085
|
pushedRoutes.add(`company:${passArgv[companyIdx + 1]}`);
|
|
5086
|
+
} else if (passArgv.includes("--personal")) {
|
|
5087
|
+
pushedRoutes.add("personal");
|
|
4472
5088
|
} else if (passArgv.includes("--companies")) {
|
|
4473
5089
|
pushedEverything = true;
|
|
4474
5090
|
}
|
|
@@ -4510,6 +5126,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4510
5126
|
| null = null;
|
|
4511
5127
|
const watcher = makeBatchWatcherStub();
|
|
4512
5128
|
const clock = new FakeClock();
|
|
5129
|
+
const sleep = makeSteppableSleep();
|
|
4513
5130
|
let triggerShutdown = () => {};
|
|
4514
5131
|
let releasePoll: () => void = () => {};
|
|
4515
5132
|
const pollGate = new Promise<void>((resolve) => {
|
|
@@ -4523,7 +5140,17 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4523
5140
|
});
|
|
4524
5141
|
|
|
4525
5142
|
const loop = runRunnerWithLoop(
|
|
4526
|
-
|
|
5143
|
+
// `--direction both` so the queued watcher pushes actually run (an omitted
|
|
5144
|
+
// direction is pull-only by the parser default).
|
|
5145
|
+
[
|
|
5146
|
+
"--companies",
|
|
5147
|
+
"--watch",
|
|
5148
|
+
"--event-push",
|
|
5149
|
+
"--direction",
|
|
5150
|
+
"both",
|
|
5151
|
+
"--hq-root",
|
|
5152
|
+
"/tmp/hq",
|
|
5153
|
+
],
|
|
4527
5154
|
{
|
|
4528
5155
|
runPass,
|
|
4529
5156
|
clock,
|
|
@@ -4536,7 +5163,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4536
5163
|
dispose: async () => {},
|
|
4537
5164
|
};
|
|
4538
5165
|
},
|
|
4539
|
-
sleep:
|
|
5166
|
+
sleep: sleep.sleep,
|
|
4540
5167
|
onShutdownSignal: (handler) => {
|
|
4541
5168
|
triggerShutdown = handler;
|
|
4542
5169
|
return () => {};
|
|
@@ -4585,14 +5212,32 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4585
5212
|
releasePoll();
|
|
4586
5213
|
await remotePull;
|
|
4587
5214
|
await microtasks();
|
|
4588
|
-
|
|
5215
|
+
sleep.tick();
|
|
4589
5216
|
await microtasks();
|
|
4590
5217
|
|
|
4591
5218
|
const passArgvs = runPass.mock.calls.map((call) => call[0] as string[]);
|
|
4592
5219
|
expect(passArgvs).toEqual(
|
|
4593
5220
|
expect.arrayContaining([
|
|
4594
|
-
[
|
|
4595
|
-
|
|
5221
|
+
[
|
|
5222
|
+
"--company",
|
|
5223
|
+
"indigo",
|
|
5224
|
+
"--direction",
|
|
5225
|
+
"push",
|
|
5226
|
+
"--scope-path",
|
|
5227
|
+
"a.md",
|
|
5228
|
+
"--hq-root",
|
|
5229
|
+
"/tmp/hq",
|
|
5230
|
+
],
|
|
5231
|
+
[
|
|
5232
|
+
"--company",
|
|
5233
|
+
"beta",
|
|
5234
|
+
"--direction",
|
|
5235
|
+
"push",
|
|
5236
|
+
"--scope-path",
|
|
5237
|
+
"b.md",
|
|
5238
|
+
"--hq-root",
|
|
5239
|
+
"/tmp/hq",
|
|
5240
|
+
],
|
|
4596
5241
|
["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
4597
5242
|
]),
|
|
4598
5243
|
);
|
|
@@ -4633,7 +5278,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4633
5278
|
ownDeviceId: "dev-test",
|
|
4634
5279
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
4635
5280
|
});
|
|
4636
|
-
const { loop, watcher, clock, shutdown } = runLoop({
|
|
5281
|
+
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
4637
5282
|
claims: ENROLLED_CLAIMS,
|
|
4638
5283
|
startEventSync,
|
|
4639
5284
|
});
|
|
@@ -4642,6 +5287,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
4642
5287
|
watcher.emit("companies/indigo/b.md"); // no batch
|
|
4643
5288
|
clock.advance(0);
|
|
4644
5289
|
await microtasks();
|
|
5290
|
+
tick();
|
|
5291
|
+
await microtasks();
|
|
4645
5292
|
|
|
4646
5293
|
expect(publishBatch).toHaveBeenCalledTimes(1);
|
|
4647
5294
|
const synthesized = publishBatch.mock.calls[0][0] as {
|