@indigoai-us/hq-cloud 6.15.6 → 6.15.8
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/journal-repair.d.ts +12 -0
- package/dist/bin/journal-repair.d.ts.map +1 -0
- package/dist/bin/journal-repair.js +46 -0
- package/dist/bin/journal-repair.js.map +1 -0
- package/dist/bin/journal-repair.test.d.ts +2 -0
- package/dist/bin/journal-repair.test.d.ts.map +1 -0
- package/dist/bin/journal-repair.test.js +53 -0
- package/dist/bin/journal-repair.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +2 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +2 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +37 -4
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +12 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +108 -32
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +416 -66
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +7 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +132 -11
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +11 -2
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +5 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +11 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +34 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +82 -13
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +117 -1
- package/dist/journal.test.js.map +1 -1
- package/dist/s3.d.ts +9 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +19 -0
- package/dist/s3.js.map +1 -1
- package/dist/s3.symlink-materialize.test.js +26 -1
- package/dist/s3.symlink-materialize.test.js.map +1 -1
- package/dist/sync/state-store.d.ts +52 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +411 -99
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +188 -1
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -1152,6 +1152,180 @@ describe("fanout-plan", () => {
|
|
|
1152
1152
|
expect(plan.companies).toEqual([{ uid: "cmp_empty", slug: "cmp_empty" }]);
|
|
1153
1153
|
});
|
|
1154
1154
|
});
|
|
1155
|
+
describe("automatic journal maintenance", () => {
|
|
1156
|
+
it("accepts direct liveness pulses from synchronous engine planning", async () => {
|
|
1157
|
+
vi.useFakeTimers();
|
|
1158
|
+
vi.setSystemTime(0);
|
|
1159
|
+
try {
|
|
1160
|
+
const sync = vi.fn().mockImplementation(async (options) => {
|
|
1161
|
+
// Moving wall time does not run a fake interval. The maintenance event
|
|
1162
|
+
// can only come from the direct planner callback wired into SyncOptions.
|
|
1163
|
+
vi.setSystemTime(30_000);
|
|
1164
|
+
options.onHeartbeat?.();
|
|
1165
|
+
return defaultSyncResult();
|
|
1166
|
+
});
|
|
1167
|
+
const deps = makeDeps({
|
|
1168
|
+
createVaultClient: () => makeVaultStub({
|
|
1169
|
+
memberships: [{ companyUid: "cmp_indigo" }],
|
|
1170
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
1171
|
+
}),
|
|
1172
|
+
repairJournalStateIfNeeded: vi
|
|
1173
|
+
.fn()
|
|
1174
|
+
.mockReturnValue({ status: "not-needed", slug: "indigo" }),
|
|
1175
|
+
fanoutHeartbeatIntervalMs: 30_000,
|
|
1176
|
+
sync,
|
|
1177
|
+
});
|
|
1178
|
+
expect(await runRunner(["--companies", "--skip-personal"], deps)).toBe(0);
|
|
1179
|
+
expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toEqual([
|
|
1180
|
+
{
|
|
1181
|
+
type: "maintenance-progress",
|
|
1182
|
+
company: "indigo",
|
|
1183
|
+
bytesProcessed: 0,
|
|
1184
|
+
totalBytes: 0,
|
|
1185
|
+
},
|
|
1186
|
+
]);
|
|
1187
|
+
}
|
|
1188
|
+
finally {
|
|
1189
|
+
vi.useRealTimers();
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
it("keeps the desktop protocol alive while a long fanout is still reconciling", async () => {
|
|
1193
|
+
vi.useFakeTimers();
|
|
1194
|
+
try {
|
|
1195
|
+
let finishSync;
|
|
1196
|
+
const sync = vi.fn().mockImplementation(() => new Promise((resolve) => {
|
|
1197
|
+
finishSync = resolve;
|
|
1198
|
+
}));
|
|
1199
|
+
const deps = makeDeps({
|
|
1200
|
+
createVaultClient: () => makeVaultStub({
|
|
1201
|
+
memberships: [{ companyUid: "cmp_indigo" }],
|
|
1202
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
1203
|
+
}),
|
|
1204
|
+
repairJournalStateIfNeeded: vi
|
|
1205
|
+
.fn()
|
|
1206
|
+
.mockReturnValue({ status: "not-needed", slug: "indigo" }),
|
|
1207
|
+
fanoutHeartbeatIntervalMs: 30_000,
|
|
1208
|
+
sync,
|
|
1209
|
+
});
|
|
1210
|
+
const running = runRunner(["--companies", "--skip-personal"], deps);
|
|
1211
|
+
await vi.waitFor(() => expect(sync).toHaveBeenCalledTimes(1));
|
|
1212
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1213
|
+
expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toContainEqual({
|
|
1214
|
+
type: "maintenance-progress",
|
|
1215
|
+
company: "indigo",
|
|
1216
|
+
bytesProcessed: 0,
|
|
1217
|
+
totalBytes: 0,
|
|
1218
|
+
});
|
|
1219
|
+
finishSync(defaultSyncResult());
|
|
1220
|
+
await expect(running).resolves.toBe(0);
|
|
1221
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1222
|
+
expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toHaveLength(1);
|
|
1223
|
+
}
|
|
1224
|
+
finally {
|
|
1225
|
+
vi.useRealTimers();
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
it("repairs every resolved journal shard before fanout and streams known heartbeat events", async () => {
|
|
1229
|
+
const order = [];
|
|
1230
|
+
const repairJournalStateIfNeeded = vi.fn().mockImplementation((slug, options) => {
|
|
1231
|
+
order.push(`repair:${slug}`);
|
|
1232
|
+
options.onProgress?.({ bytesRead: 13, totalBytes: 26 });
|
|
1233
|
+
options.onProgress?.({ bytesRead: 26, totalBytes: 26 });
|
|
1234
|
+
return { status: "repaired", slug };
|
|
1235
|
+
});
|
|
1236
|
+
const deps = makeDeps({
|
|
1237
|
+
createVaultClient: () => makeVaultStub({
|
|
1238
|
+
memberships: [{ companyUid: "cmp_indigo" }],
|
|
1239
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
1240
|
+
listPersons: () => Promise.resolve([
|
|
1241
|
+
{
|
|
1242
|
+
uid: "prs_me",
|
|
1243
|
+
slug: "me",
|
|
1244
|
+
type: "person",
|
|
1245
|
+
status: "active",
|
|
1246
|
+
bucketName: "hq-vault-prs-me",
|
|
1247
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
1248
|
+
},
|
|
1249
|
+
]),
|
|
1250
|
+
}),
|
|
1251
|
+
repairJournalStateIfNeeded,
|
|
1252
|
+
sync: vi.fn().mockImplementation(async (options) => {
|
|
1253
|
+
order.push(`sync:${options.journalSlug ?? "indigo"}`);
|
|
1254
|
+
return defaultSyncResult();
|
|
1255
|
+
}),
|
|
1256
|
+
});
|
|
1257
|
+
expect(await runRunner(["--companies"], deps)).toBe(0);
|
|
1258
|
+
expect(repairJournalStateIfNeeded.mock.calls.map(([slug]) => slug)).toEqual([
|
|
1259
|
+
"indigo",
|
|
1260
|
+
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
1261
|
+
"personal",
|
|
1262
|
+
]);
|
|
1263
|
+
expect(order.slice(0, 3)).toEqual([
|
|
1264
|
+
"repair:indigo",
|
|
1265
|
+
`repair:${PERSONAL_VAULT_JOURNAL_SLUG}`,
|
|
1266
|
+
"repair:personal",
|
|
1267
|
+
]);
|
|
1268
|
+
expect(order[3]).toBe("sync:indigo");
|
|
1269
|
+
expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toEqual([
|
|
1270
|
+
{
|
|
1271
|
+
type: "maintenance-progress",
|
|
1272
|
+
company: "indigo",
|
|
1273
|
+
bytesProcessed: 13,
|
|
1274
|
+
totalBytes: 26,
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
type: "maintenance-progress",
|
|
1278
|
+
company: "indigo",
|
|
1279
|
+
bytesProcessed: 26,
|
|
1280
|
+
totalBytes: 26,
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
type: "maintenance-progress",
|
|
1284
|
+
company: "personal",
|
|
1285
|
+
bytesProcessed: 13,
|
|
1286
|
+
totalBytes: 26,
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
type: "maintenance-progress",
|
|
1290
|
+
company: "personal",
|
|
1291
|
+
bytesProcessed: 26,
|
|
1292
|
+
totalBytes: 26,
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
type: "maintenance-progress",
|
|
1296
|
+
company: "personal",
|
|
1297
|
+
bytesProcessed: 13,
|
|
1298
|
+
totalBytes: 26,
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
type: "maintenance-progress",
|
|
1302
|
+
company: "personal",
|
|
1303
|
+
bytesProcessed: 26,
|
|
1304
|
+
totalBytes: 26,
|
|
1305
|
+
},
|
|
1306
|
+
]);
|
|
1307
|
+
});
|
|
1308
|
+
it("fails before network work when authoritative local journal recovery fails", async () => {
|
|
1309
|
+
const sync = vi.fn().mockResolvedValue(defaultSyncResult());
|
|
1310
|
+
const deps = makeDeps({
|
|
1311
|
+
createVaultClient: () => makeVaultStub({
|
|
1312
|
+
memberships: [{ companyUid: "cmp_indigo" }],
|
|
1313
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
1314
|
+
}),
|
|
1315
|
+
repairJournalStateIfNeeded: () => {
|
|
1316
|
+
throw new Error("newest journal generation is unreadable");
|
|
1317
|
+
},
|
|
1318
|
+
sync,
|
|
1319
|
+
});
|
|
1320
|
+
expect(await runRunner(["--companies"], deps)).toBe(1);
|
|
1321
|
+
expect(sync).not.toHaveBeenCalled();
|
|
1322
|
+
expect(deps.stderr.events()).toContainEqual({
|
|
1323
|
+
type: "error",
|
|
1324
|
+
path: "(local-state)",
|
|
1325
|
+
message: "newest journal generation is unreadable",
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1155
1329
|
// ---------------------------------------------------------------------------
|
|
1156
1330
|
// per-company event tagging
|
|
1157
1331
|
// ---------------------------------------------------------------------------
|
|
@@ -3334,6 +3508,9 @@ function makeWatcherStub() {
|
|
|
3334
3508
|
};
|
|
3335
3509
|
return stub;
|
|
3336
3510
|
}
|
|
3511
|
+
function completedPassOutcome(exitCode = 0) {
|
|
3512
|
+
return { exitCode, result: { pushPathResults: [] } };
|
|
3513
|
+
}
|
|
3337
3514
|
async function flushLoopMicrotasks(n = 8) {
|
|
3338
3515
|
for (let i = 0; i < n; i++)
|
|
3339
3516
|
await Promise.resolve();
|
|
@@ -3490,10 +3667,164 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
|
|
|
3490
3667
|
});
|
|
3491
3668
|
});
|
|
3492
3669
|
describe("runRunnerWithLoop — event-push wiring", () => {
|
|
3670
|
+
it("does not start journal-reading watcher callbacks before the initial pass finishes", async () => {
|
|
3671
|
+
const watcher = makeWatcherStub();
|
|
3672
|
+
let maintenanceComplete = false;
|
|
3673
|
+
let callbackObservedMaintenance = false;
|
|
3674
|
+
let triggerShutdown = () => { };
|
|
3675
|
+
let releaseInitialPass = () => { };
|
|
3676
|
+
const initialPass = new Promise((resolve) => {
|
|
3677
|
+
releaseInitialPass = resolve;
|
|
3678
|
+
});
|
|
3679
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
3680
|
+
await initialPass;
|
|
3681
|
+
maintenanceComplete = true;
|
|
3682
|
+
return completedPassOutcome();
|
|
3683
|
+
});
|
|
3684
|
+
const createWatcher = vi.fn((opts) => {
|
|
3685
|
+
watcher.start = () => {
|
|
3686
|
+
watcher.started = true;
|
|
3687
|
+
callbackObservedMaintenance = maintenanceComplete;
|
|
3688
|
+
opts.captureLocalDeleteSnapshots("companies/indigo/knowledge/deleted.md", "unlink");
|
|
3689
|
+
};
|
|
3690
|
+
return watcher;
|
|
3691
|
+
});
|
|
3692
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3693
|
+
runPass,
|
|
3694
|
+
createWatcher: createWatcher,
|
|
3695
|
+
sleep: () => new Promise(() => { }),
|
|
3696
|
+
onShutdownSignal: (handler) => {
|
|
3697
|
+
triggerShutdown = handler;
|
|
3698
|
+
return () => { };
|
|
3699
|
+
},
|
|
3700
|
+
});
|
|
3701
|
+
await flushLoopMicrotasks();
|
|
3702
|
+
expect(runPass).toHaveBeenCalledTimes(1);
|
|
3703
|
+
expect(createWatcher).not.toHaveBeenCalled();
|
|
3704
|
+
expect(watcher.started).toBe(false);
|
|
3705
|
+
releaseInitialPass();
|
|
3706
|
+
await flushLoopMicrotasks();
|
|
3707
|
+
expect(createWatcher).toHaveBeenCalledTimes(1);
|
|
3708
|
+
expect(watcher.started).toBe(true);
|
|
3709
|
+
expect(callbackObservedMaintenance).toBe(true);
|
|
3710
|
+
triggerShutdown();
|
|
3711
|
+
await loop;
|
|
3712
|
+
});
|
|
3713
|
+
it("keeps every event surface off until a result proves maintenance completed", async () => {
|
|
3714
|
+
const watcher = makeWatcherStub();
|
|
3715
|
+
const sleep = makeSteppableSleep();
|
|
3716
|
+
let triggerShutdown = () => { };
|
|
3717
|
+
const runPass = vi.fn()
|
|
3718
|
+
.mockResolvedValueOnce({ exitCode: TRANSIENT_NETWORK_EXIT })
|
|
3719
|
+
.mockResolvedValueOnce(completedPassOutcome());
|
|
3720
|
+
const createWatcher = vi.fn(() => watcher);
|
|
3721
|
+
const receiver = {
|
|
3722
|
+
connected: false,
|
|
3723
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
3724
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
3725
|
+
};
|
|
3726
|
+
const createReceiver = vi.fn(() => receiver);
|
|
3727
|
+
const scheduler = {
|
|
3728
|
+
signal: vi.fn(),
|
|
3729
|
+
checkHighWater: vi.fn().mockResolvedValue(undefined),
|
|
3730
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
3731
|
+
};
|
|
3732
|
+
const startRealtimeScheduler = vi.fn().mockResolvedValue(scheduler);
|
|
3733
|
+
const startEventSync = vi.fn().mockResolvedValue(null);
|
|
3734
|
+
const priorEventSyncOverride = process.env.HQ_SYNC_EVENT_SYNC;
|
|
3735
|
+
process.env.HQ_SYNC_EVENT_SYNC = "1";
|
|
3736
|
+
try {
|
|
3737
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3738
|
+
runPass,
|
|
3739
|
+
createWatcher,
|
|
3740
|
+
createReceiver,
|
|
3741
|
+
startRealtimeScheduler,
|
|
3742
|
+
startEventSync,
|
|
3743
|
+
getIdTokenClaims: () => null,
|
|
3744
|
+
getAccessToken: async () => "jwt-test",
|
|
3745
|
+
sleep: sleep.sleep,
|
|
3746
|
+
onShutdownSignal: (handler) => {
|
|
3747
|
+
triggerShutdown = handler;
|
|
3748
|
+
return () => { };
|
|
3749
|
+
},
|
|
3750
|
+
});
|
|
3751
|
+
await flushLoopMicrotasks();
|
|
3752
|
+
expect(runPass).toHaveBeenCalledTimes(1);
|
|
3753
|
+
expect(createWatcher).not.toHaveBeenCalled();
|
|
3754
|
+
expect(createReceiver).not.toHaveBeenCalled();
|
|
3755
|
+
expect(startRealtimeScheduler).not.toHaveBeenCalled();
|
|
3756
|
+
expect(startEventSync).not.toHaveBeenCalled();
|
|
3757
|
+
sleep.tick();
|
|
3758
|
+
await flushLoopMicrotasks();
|
|
3759
|
+
expect(runPass).toHaveBeenCalledTimes(2);
|
|
3760
|
+
expect(createWatcher).toHaveBeenCalledTimes(1);
|
|
3761
|
+
expect(createReceiver).toHaveBeenCalledTimes(1);
|
|
3762
|
+
expect(receiver.start).toHaveBeenCalledTimes(1);
|
|
3763
|
+
expect(startRealtimeScheduler).toHaveBeenCalledTimes(1);
|
|
3764
|
+
expect(startEventSync).toHaveBeenCalledTimes(1);
|
|
3765
|
+
expect(watcher.started).toBe(true);
|
|
3766
|
+
triggerShutdown();
|
|
3767
|
+
await loop;
|
|
3768
|
+
}
|
|
3769
|
+
finally {
|
|
3770
|
+
if (priorEventSyncOverride === undefined) {
|
|
3771
|
+
delete process.env.HQ_SYNC_EVENT_SYNC;
|
|
3772
|
+
}
|
|
3773
|
+
else {
|
|
3774
|
+
process.env.HQ_SYNC_EVENT_SYNC = priorEventSyncOverride;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
it("rejects paused-company delete snapshots before opening their journal", async () => {
|
|
3779
|
+
const watcher = makeWatcherStub();
|
|
3780
|
+
let capture;
|
|
3781
|
+
let triggerShutdown = () => { };
|
|
3782
|
+
const priorSkipCompanies = process.env.HQ_SYNC_SKIP_COMPANIES;
|
|
3783
|
+
process.env.HQ_SYNC_SKIP_COMPANIES = "indigo";
|
|
3784
|
+
const stateDir = process.env.HQ_STATE_DIR;
|
|
3785
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3786
|
+
runPass: vi.fn().mockResolvedValue(completedPassOutcome()),
|
|
3787
|
+
createWatcher: (options) => {
|
|
3788
|
+
capture = options.captureLocalDeleteSnapshots;
|
|
3789
|
+
return watcher;
|
|
3790
|
+
},
|
|
3791
|
+
startEventSync: vi.fn().mockResolvedValue(null),
|
|
3792
|
+
getIdTokenClaims: () => null,
|
|
3793
|
+
getAccessToken: async () => "jwt-test",
|
|
3794
|
+
sleep: () => new Promise(() => { }),
|
|
3795
|
+
onShutdownSignal: (handler) => {
|
|
3796
|
+
triggerShutdown = handler;
|
|
3797
|
+
return () => { };
|
|
3798
|
+
},
|
|
3799
|
+
});
|
|
3800
|
+
try {
|
|
3801
|
+
await flushLoopMicrotasks();
|
|
3802
|
+
expect(watcher.started).toBe(true);
|
|
3803
|
+
expect(capture).toBeTypeOf("function");
|
|
3804
|
+
// Turn the configured state directory into a regular file. Any attempt
|
|
3805
|
+
// to open a journal beneath it now fails with ENOTDIR, so returning an
|
|
3806
|
+
// empty snapshot proves the pause check happens before journal I/O.
|
|
3807
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
3808
|
+
fs.writeFileSync(stateDir, "not-a-directory");
|
|
3809
|
+
expect(capture("companies/indigo/knowledge/deleted.md", "unlink")).toEqual([]);
|
|
3810
|
+
}
|
|
3811
|
+
finally {
|
|
3812
|
+
triggerShutdown();
|
|
3813
|
+
await loop;
|
|
3814
|
+
fs.rmSync(stateDir, { force: true });
|
|
3815
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
3816
|
+
if (priorSkipCompanies === undefined) {
|
|
3817
|
+
delete process.env.HQ_SYNC_SKIP_COMPANIES;
|
|
3818
|
+
}
|
|
3819
|
+
else {
|
|
3820
|
+
process.env.HQ_SYNC_SKIP_COMPANIES = priorSkipCompanies;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3493
3824
|
it("starts the watcher alongside the poll loop when --event-push is on", async () => {
|
|
3494
3825
|
const watcher = makeWatcherStub();
|
|
3495
3826
|
let triggerShutdown = () => { };
|
|
3496
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
3827
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3497
3828
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3498
3829
|
runPass,
|
|
3499
3830
|
clock: new FakeClock(),
|
|
@@ -3518,7 +3849,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3518
3849
|
const watcher = makeWatcherStub();
|
|
3519
3850
|
const clock = new FakeClock();
|
|
3520
3851
|
let triggerShutdown = () => { };
|
|
3521
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
3852
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3522
3853
|
const sleepCalls = [];
|
|
3523
3854
|
const loop = runRunnerWithLoop([
|
|
3524
3855
|
"--companies",
|
|
@@ -3556,7 +3887,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3556
3887
|
const watcher = makeWatcherStub();
|
|
3557
3888
|
const clock = new FakeClock();
|
|
3558
3889
|
let triggerShutdown = () => { };
|
|
3559
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
3890
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3560
3891
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3561
3892
|
runPass,
|
|
3562
3893
|
clock,
|
|
@@ -3596,7 +3927,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3596
3927
|
if (callCount === 1)
|
|
3597
3928
|
await firstGate; // hold the first (poll) pass
|
|
3598
3929
|
active--;
|
|
3599
|
-
return
|
|
3930
|
+
return completedPassOutcome();
|
|
3600
3931
|
});
|
|
3601
3932
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3602
3933
|
runPass,
|
|
@@ -3631,7 +3962,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3631
3962
|
triggerShutdown();
|
|
3632
3963
|
await loop;
|
|
3633
3964
|
});
|
|
3634
|
-
it("U07:
|
|
3965
|
+
it("U07: drains the first watcher event after activation without guarded overlap", async () => {
|
|
3635
3966
|
const watcher = makeWatcherStub();
|
|
3636
3967
|
let triggerShutdown = () => { };
|
|
3637
3968
|
let releaseFirstPass = () => { };
|
|
@@ -3647,7 +3978,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3647
3978
|
if (passes === 1)
|
|
3648
3979
|
await firstPassGate;
|
|
3649
3980
|
span.exitedAt = process.hrtime.bigint();
|
|
3650
|
-
return
|
|
3981
|
+
return completedPassOutcome();
|
|
3651
3982
|
});
|
|
3652
3983
|
const loop = runRunnerWithLoop([
|
|
3653
3984
|
"--companies",
|
|
@@ -3668,11 +3999,13 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3668
3999
|
});
|
|
3669
4000
|
await flushLoopMicrotasks();
|
|
3670
4001
|
expect(spans).toHaveLength(1);
|
|
3671
|
-
// This event arrives while the initial guarded pass owns the scheduler,
|
|
3672
|
-
// before its next sleep/wait has been registered.
|
|
3673
|
-
watcher.emit("companies/indigo/pre-registration.md");
|
|
3674
4002
|
releaseFirstPass();
|
|
3675
4003
|
await flushLoopMicrotasks();
|
|
4004
|
+
expect(watcher.started).toBe(true);
|
|
4005
|
+
// Activation happens synchronously after the maintenance-bearing pass,
|
|
4006
|
+
// before the next wait is registered. Its first event must still drain.
|
|
4007
|
+
watcher.emit("companies/indigo/pre-registration.md");
|
|
4008
|
+
await flushLoopMicrotasks();
|
|
3676
4009
|
expect(spans).toHaveLength(2);
|
|
3677
4010
|
expect(spans[1].enteredAt >= spans[0].exitedAt).toBe(true);
|
|
3678
4011
|
triggerShutdown();
|
|
@@ -3696,7 +4029,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3696
4029
|
if (calls === 1)
|
|
3697
4030
|
await pollGate;
|
|
3698
4031
|
active--;
|
|
3699
|
-
return
|
|
4032
|
+
return completedPassOutcome();
|
|
3700
4033
|
});
|
|
3701
4034
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3702
4035
|
runPass,
|
|
@@ -3737,7 +4070,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3737
4070
|
const poll = { resolve: null };
|
|
3738
4071
|
const runPass = vi.fn().mockImplementation(async () => {
|
|
3739
4072
|
passes++;
|
|
3740
|
-
return
|
|
4073
|
+
return completedPassOutcome();
|
|
3741
4074
|
});
|
|
3742
4075
|
// A sleep we can step: resolve once to allow a second poll pass.
|
|
3743
4076
|
const sleep = () => new Promise((resolve) => {
|
|
@@ -3771,7 +4104,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3771
4104
|
const clock = new FakeClock();
|
|
3772
4105
|
let triggerShutdown = () => { };
|
|
3773
4106
|
let detached = false;
|
|
3774
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4107
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3775
4108
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3776
4109
|
runPass,
|
|
3777
4110
|
clock,
|
|
@@ -3806,7 +4139,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3806
4139
|
onShutdownSignal: () => () => { },
|
|
3807
4140
|
});
|
|
3808
4141
|
expect(code).toBe(1);
|
|
3809
|
-
expect(watcher.disposed).toBe(
|
|
4142
|
+
expect(watcher.disposed).toBe(false);
|
|
3810
4143
|
});
|
|
3811
4144
|
it("an exit-0 auth-required pass stops the unattended loop without reporting a crash", async () => {
|
|
3812
4145
|
const watcher = makeWatcherStub();
|
|
@@ -3822,7 +4155,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3822
4155
|
expect(code).toBe(0);
|
|
3823
4156
|
expect(runPass).toHaveBeenCalledTimes(1);
|
|
3824
4157
|
expect(sleep).not.toHaveBeenCalled();
|
|
3825
|
-
expect(watcher.disposed).toBe(
|
|
4158
|
+
expect(watcher.disposed).toBe(false);
|
|
3826
4159
|
});
|
|
3827
4160
|
it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
|
|
3828
4161
|
// A transient offline blip must keep the watcher alive: the loop logs and
|
|
@@ -3849,7 +4182,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3849
4182
|
// …and the transient code was never surfaced as the watcher's exit code.
|
|
3850
4183
|
expect(code).toBe(1);
|
|
3851
4184
|
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
3852
|
-
expect(watcher.disposed).toBe(
|
|
4185
|
+
expect(watcher.disposed).toBe(false);
|
|
3853
4186
|
});
|
|
3854
4187
|
it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
|
|
3855
4188
|
const watcher = makeWatcherStub();
|
|
@@ -3883,7 +4216,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3883
4216
|
expect(transientPass.stderr
|
|
3884
4217
|
.events()
|
|
3885
4218
|
.some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
|
|
3886
|
-
expect(watcher.disposed).toBe(
|
|
4219
|
+
expect(watcher.disposed).toBe(false);
|
|
3887
4220
|
});
|
|
3888
4221
|
it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
|
|
3889
4222
|
// A partial pass (some per-file transfers threw: oversized object on an
|
|
@@ -3913,7 +4246,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3913
4246
|
// …and the partial code was never surfaced as the watcher's exit code.
|
|
3914
4247
|
expect(code).toBe(1);
|
|
3915
4248
|
expect(code).not.toBe(PARTIAL_SYNC_EXIT);
|
|
3916
|
-
expect(watcher.disposed).toBe(
|
|
4249
|
+
expect(watcher.disposed).toBe(false);
|
|
3917
4250
|
});
|
|
3918
4251
|
it("alerts when a partial pass failed before a company's download leg", async () => {
|
|
3919
4252
|
const watcher = makeWatcherStub();
|
|
@@ -3961,7 +4294,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3961
4294
|
it("without --event-push, no watcher is created (poll-only safety net)", async () => {
|
|
3962
4295
|
let triggerShutdown = () => { };
|
|
3963
4296
|
const createWatcher = vi.fn(() => makeWatcherStub());
|
|
3964
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4297
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3965
4298
|
const loop = runRunnerWithLoop(["--companies", "--watch"], {
|
|
3966
4299
|
runPass,
|
|
3967
4300
|
createWatcher,
|
|
@@ -3992,17 +4325,17 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
|
3992
4325
|
active += 1;
|
|
3993
4326
|
maximumActive = Math.max(maximumActive, active);
|
|
3994
4327
|
passes += 1;
|
|
3995
|
-
if (passes ===
|
|
4328
|
+
if (passes === 2)
|
|
3996
4329
|
await pollGate;
|
|
3997
4330
|
active -= 1;
|
|
3998
|
-
return
|
|
4331
|
+
return completedPassOutcome();
|
|
3999
4332
|
});
|
|
4000
4333
|
let signal = null;
|
|
4001
4334
|
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
4002
4335
|
runPass,
|
|
4003
4336
|
clock: new FakeClock(),
|
|
4004
4337
|
createWatcher: () => watcher,
|
|
4005
|
-
sleep: ()
|
|
4338
|
+
sleep: makeSteppableSleep().sleep,
|
|
4006
4339
|
onShutdownSignal: (handler) => {
|
|
4007
4340
|
triggerShutdown = handler;
|
|
4008
4341
|
return () => { };
|
|
@@ -4022,7 +4355,12 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
|
4022
4355
|
dispose: async () => undefined,
|
|
4023
4356
|
}),
|
|
4024
4357
|
});
|
|
4025
|
-
for (let i = 0; i <
|
|
4358
|
+
for (let i = 0; i < 12; i++)
|
|
4359
|
+
await Promise.resolve();
|
|
4360
|
+
expect(watcher.started).toBe(true);
|
|
4361
|
+
// A second legacy pass owns the guard after activation.
|
|
4362
|
+
watcher.emit("companies/indigo/legacy.md");
|
|
4363
|
+
for (let i = 0; i < 8; i++)
|
|
4026
4364
|
await Promise.resolve();
|
|
4027
4365
|
expect(active).toBe(1);
|
|
4028
4366
|
watcher.emit("companies/indigo/v2.md");
|
|
@@ -4054,7 +4392,7 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
|
4054
4392
|
passes += 1;
|
|
4055
4393
|
if (passes === 1)
|
|
4056
4394
|
await initial;
|
|
4057
|
-
return
|
|
4395
|
+
return completedPassOutcome();
|
|
4058
4396
|
},
|
|
4059
4397
|
clock: new FakeClock(),
|
|
4060
4398
|
createWatcher: () => watcher,
|
|
@@ -4073,11 +4411,10 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
|
4073
4411
|
dispose: async () => undefined,
|
|
4074
4412
|
}),
|
|
4075
4413
|
});
|
|
4076
|
-
for (let i = 0; i <
|
|
4414
|
+
for (let i = 0; i < 12; i++)
|
|
4077
4415
|
await Promise.resolve();
|
|
4078
4416
|
releaseInitial();
|
|
4079
|
-
|
|
4080
|
-
await Promise.resolve();
|
|
4417
|
+
await flushLoopMicrotasks(20);
|
|
4081
4418
|
expect(sleepState.delays[0]).toBe(300_000);
|
|
4082
4419
|
clockState.now = 300_000;
|
|
4083
4420
|
sleepState.resolve?.();
|
|
@@ -4133,7 +4470,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4133
4470
|
};
|
|
4134
4471
|
}
|
|
4135
4472
|
it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
|
|
4136
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4473
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4137
4474
|
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
4138
4475
|
await flushLoopMicrotasks();
|
|
4139
4476
|
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
@@ -4158,7 +4495,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4158
4495
|
await loop;
|
|
4159
4496
|
});
|
|
4160
4497
|
it("coalesces repeated edits to the same path into one scoped push", async () => {
|
|
4161
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4498
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4162
4499
|
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
4163
4500
|
await flushLoopMicrotasks();
|
|
4164
4501
|
runPass.mockClear();
|
|
@@ -4186,7 +4523,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4186
4523
|
await loop;
|
|
4187
4524
|
});
|
|
4188
4525
|
it("U08: --companies --skip-personal drops a personal watcher edit before it queues work", async () => {
|
|
4189
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4526
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4190
4527
|
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
4191
4528
|
...watchArgv,
|
|
4192
4529
|
"--skip-personal",
|
|
@@ -4204,7 +4541,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4204
4541
|
await loop;
|
|
4205
4542
|
});
|
|
4206
4543
|
it("drains an unlink path through the same scoped tombstone push path", async () => {
|
|
4207
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4544
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4208
4545
|
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
4209
4546
|
await flushLoopMicrotasks();
|
|
4210
4547
|
runPass.mockClear();
|
|
@@ -4226,7 +4563,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4226
4563
|
await loop;
|
|
4227
4564
|
});
|
|
4228
4565
|
it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
|
|
4229
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4566
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4230
4567
|
// No --direction flag → parseArgs defaults the run to pull-only. The drain
|
|
4231
4568
|
// must honor that and NOT run a scoped push. Regression: the loop used to
|
|
4232
4569
|
// re-parse argv and wrongly default an omitted direction to "both", so a
|
|
@@ -4260,7 +4597,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4260
4597
|
it("restores failed scoped push paths so the next tick retries them", async () => {
|
|
4261
4598
|
const runPass = vi
|
|
4262
4599
|
.fn()
|
|
4263
|
-
.mockResolvedValueOnce(
|
|
4600
|
+
.mockResolvedValueOnce(completedPassOutcome())
|
|
4264
4601
|
.mockResolvedValueOnce(1)
|
|
4265
4602
|
.mockResolvedValueOnce(0)
|
|
4266
4603
|
.mockResolvedValueOnce(0)
|
|
@@ -4303,7 +4640,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4303
4640
|
await loop;
|
|
4304
4641
|
});
|
|
4305
4642
|
it("empty scoped ticks run only the pull leg", async () => {
|
|
4306
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4643
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4307
4644
|
const { loop, tick, shutdown } = startDrainLoop(runPass);
|
|
4308
4645
|
await flushLoopMicrotasks();
|
|
4309
4646
|
runPass.mockClear();
|
|
@@ -4315,7 +4652,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4315
4652
|
await loop;
|
|
4316
4653
|
});
|
|
4317
4654
|
it("personal scoped ticks preserve personal mode for the pull leg", async () => {
|
|
4318
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4655
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4319
4656
|
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
4320
4657
|
"--personal",
|
|
4321
4658
|
"--watch",
|
|
@@ -4349,7 +4686,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4349
4686
|
await loop;
|
|
4350
4687
|
});
|
|
4351
4688
|
it("single-company scoped ticks preserve company mode for the pull leg", async () => {
|
|
4352
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4689
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4353
4690
|
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
4354
4691
|
"--company",
|
|
4355
4692
|
"acme",
|
|
@@ -4385,7 +4722,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4385
4722
|
await loop;
|
|
4386
4723
|
});
|
|
4387
4724
|
it("push-only scoped ticks run the scoped push and no pull leg", async () => {
|
|
4388
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4725
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4389
4726
|
const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
|
|
4390
4727
|
"--companies",
|
|
4391
4728
|
"--watch",
|
|
@@ -4422,7 +4759,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
4422
4759
|
const watcher = makeBatchWatcherStub();
|
|
4423
4760
|
const sleep = makeSteppableSleep();
|
|
4424
4761
|
let triggerShutdown = () => { };
|
|
4425
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
4762
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4426
4763
|
const loop = runRunnerWithLoop(watchArgv, {
|
|
4427
4764
|
runPass,
|
|
4428
4765
|
clock: new FakeClock(),
|
|
@@ -5210,7 +5547,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5210
5547
|
const UNENROLLED_CLAIMS = { email: "someone@getindigo.ai" };
|
|
5211
5548
|
function runLoop(opts) {
|
|
5212
5549
|
const watcher = opts.watcher ?? makeBatchWatcherStub();
|
|
5213
|
-
const runPass = opts.runPass ??
|
|
5550
|
+
const runPass = opts.runPass ??
|
|
5551
|
+
vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5214
5552
|
const clock = new FakeClock();
|
|
5215
5553
|
const sleep = makeSteppableSleep();
|
|
5216
5554
|
let triggerShutdown = () => { };
|
|
@@ -5304,7 +5642,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5304
5642
|
});
|
|
5305
5643
|
const runPass = vi
|
|
5306
5644
|
.fn()
|
|
5307
|
-
.mockResolvedValueOnce(
|
|
5645
|
+
.mockResolvedValueOnce(completedPassOutcome())
|
|
5308
5646
|
.mockImplementation((passArgv) => passArgv.includes("--company") ? targetedPush : Promise.resolve(0));
|
|
5309
5647
|
const { loop, watcher, clock, shutdown } = runLoop({
|
|
5310
5648
|
claims: EMAIL_CLAIMS,
|
|
@@ -5349,7 +5687,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5349
5687
|
// Initial poll pass succeeds; the targeted pass fails.
|
|
5350
5688
|
const runPass = vi
|
|
5351
5689
|
.fn()
|
|
5352
|
-
.mockResolvedValueOnce(
|
|
5690
|
+
.mockResolvedValueOnce(completedPassOutcome())
|
|
5353
5691
|
.mockResolvedValue(1);
|
|
5354
5692
|
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
5355
5693
|
claims: EMAIL_CLAIMS,
|
|
@@ -5381,7 +5719,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5381
5719
|
ownDeviceId: "dev-test",
|
|
5382
5720
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
5383
5721
|
});
|
|
5384
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
5722
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5385
5723
|
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
5386
5724
|
claims: EMAIL_CLAIMS,
|
|
5387
5725
|
startEventSync,
|
|
@@ -5421,7 +5759,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5421
5759
|
});
|
|
5422
5760
|
const runPass = vi
|
|
5423
5761
|
.fn()
|
|
5424
|
-
.mockResolvedValueOnce(
|
|
5762
|
+
.mockResolvedValueOnce(completedPassOutcome())
|
|
5425
5763
|
.mockResolvedValueOnce({
|
|
5426
5764
|
exitCode: 0,
|
|
5427
5765
|
result: {
|
|
@@ -5462,7 +5800,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5462
5800
|
});
|
|
5463
5801
|
const runPass = vi
|
|
5464
5802
|
.fn()
|
|
5465
|
-
.mockResolvedValueOnce(
|
|
5803
|
+
.mockResolvedValueOnce(completedPassOutcome())
|
|
5466
5804
|
.mockResolvedValueOnce({
|
|
5467
5805
|
exitCode: 0,
|
|
5468
5806
|
result: {
|
|
@@ -5503,7 +5841,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5503
5841
|
ownDeviceId: "dev-test",
|
|
5504
5842
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
5505
5843
|
});
|
|
5506
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
5844
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5507
5845
|
const { loop, watcher, clock, tick, shutdown } = runLoop({
|
|
5508
5846
|
claims: EMAIL_CLAIMS,
|
|
5509
5847
|
startEventSync,
|
|
@@ -5580,9 +5918,9 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5580
5918
|
let passCount = 0;
|
|
5581
5919
|
const runPass = vi.fn().mockImplementation(async () => {
|
|
5582
5920
|
passCount += 1;
|
|
5583
|
-
if (passCount ===
|
|
5921
|
+
if (passCount === 2)
|
|
5584
5922
|
await pollGate;
|
|
5585
|
-
return
|
|
5923
|
+
return completedPassOutcome();
|
|
5586
5924
|
});
|
|
5587
5925
|
const loop = runRunnerWithLoop(
|
|
5588
5926
|
// `--direction both` so the queued watcher pushes actually run (an omitted
|
|
@@ -5619,6 +5957,21 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5619
5957
|
await microtasks();
|
|
5620
5958
|
expect(passCount).toBe(1);
|
|
5621
5959
|
expect(receiverSync).toBeTruthy();
|
|
5960
|
+
const remotePull = receiverSync({
|
|
5961
|
+
event: {
|
|
5962
|
+
kind: "upsert",
|
|
5963
|
+
relativePath: "companies/acme/remote.md",
|
|
5964
|
+
contentHash: "sha256:remote",
|
|
5965
|
+
mtime: "2026-06-18T12:00:00.000Z",
|
|
5966
|
+
originDeviceId: "peer-device",
|
|
5967
|
+
originTenantId: "tenant-acme",
|
|
5968
|
+
sequenceNumber: 7,
|
|
5969
|
+
eventTimestamp: "2026-06-18T12:00:00.000Z",
|
|
5970
|
+
},
|
|
5971
|
+
signal: new AbortController().signal,
|
|
5972
|
+
});
|
|
5973
|
+
await microtasks();
|
|
5974
|
+
expect(passCount).toBe(2);
|
|
5622
5975
|
const indigoBatch = {
|
|
5623
5976
|
paths: new Map([
|
|
5624
5977
|
["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
|
|
@@ -5635,20 +5988,6 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
5635
5988
|
watcher.emit("companies/beta/b.md", betaBatch);
|
|
5636
5989
|
clock.advance(0);
|
|
5637
5990
|
await microtasks();
|
|
5638
|
-
const remotePull = receiverSync({
|
|
5639
|
-
event: {
|
|
5640
|
-
kind: "upsert",
|
|
5641
|
-
relativePath: "companies/acme/remote.md",
|
|
5642
|
-
contentHash: "sha256:remote",
|
|
5643
|
-
mtime: "2026-06-18T12:00:00.000Z",
|
|
5644
|
-
originDeviceId: "peer-device",
|
|
5645
|
-
originTenantId: "tenant-acme",
|
|
5646
|
-
sequenceNumber: 7,
|
|
5647
|
-
eventTimestamp: "2026-06-18T12:00:00.000Z",
|
|
5648
|
-
},
|
|
5649
|
-
signal: new AbortController().signal,
|
|
5650
|
-
});
|
|
5651
|
-
await microtasks();
|
|
5652
5991
|
releasePoll();
|
|
5653
5992
|
await remotePull;
|
|
5654
5993
|
await microtasks();
|
|
@@ -6622,7 +6961,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
6622
6961
|
const initialPullGate = new Promise((resolve) => {
|
|
6623
6962
|
releaseInitialPull = resolve;
|
|
6624
6963
|
});
|
|
6625
|
-
let
|
|
6964
|
+
let pullPasses = 0;
|
|
6626
6965
|
const runPass = vi.fn(async (argv) => {
|
|
6627
6966
|
// Lets a test hold a pass mid-flight and act while the loop is awaiting
|
|
6628
6967
|
// it, which is where producer-side state that outlives a pass is decided.
|
|
@@ -6673,8 +7012,8 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
6673
7012
|
return { exitCode: 0, result: { pushPathResults: pathResults } };
|
|
6674
7013
|
}
|
|
6675
7014
|
if (direction === "pull" || direction === "both") {
|
|
6676
|
-
|
|
6677
|
-
|
|
7015
|
+
pullPasses += 1;
|
|
7016
|
+
if (options.gateNextPull === true && pullPasses === 2) {
|
|
6678
7017
|
await initialPullGate;
|
|
6679
7018
|
}
|
|
6680
7019
|
const journal = readJournal("indigo");
|
|
@@ -6694,7 +7033,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
6694
7033
|
downloads++;
|
|
6695
7034
|
}
|
|
6696
7035
|
}
|
|
6697
|
-
return
|
|
7036
|
+
return completedPassOutcome();
|
|
6698
7037
|
});
|
|
6699
7038
|
const loop = runRunnerWithLoop([
|
|
6700
7039
|
"--companies", "--watch", "--event-push",
|
|
@@ -6761,7 +7100,9 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
6761
7100
|
await stopHarness(h);
|
|
6762
7101
|
});
|
|
6763
7102
|
it("persists the intent before an in-flight pull plans the missing path", async () => {
|
|
6764
|
-
const h = startDeleteHarness(["knowledge/in-flight.md"], {
|
|
7103
|
+
const h = startDeleteHarness(["knowledge/in-flight.md"], { gateNextPull: true });
|
|
7104
|
+
await settle();
|
|
7105
|
+
h.tick();
|
|
6765
7106
|
await settle();
|
|
6766
7107
|
const relativePath = "companies/indigo/knowledge/in-flight.md";
|
|
6767
7108
|
const absolutePath = path.join(h.companyRoot, "knowledge/in-flight.md");
|
|
@@ -6907,7 +7248,16 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
6907
7248
|
"--poll-remote-ms",
|
|
6908
7249
|
"60000",
|
|
6909
7250
|
], {
|
|
6910
|
-
runPass: (passArgv) =>
|
|
7251
|
+
runPass: async (passArgv) => {
|
|
7252
|
+
let result;
|
|
7253
|
+
const exitCode = await runRunner(passArgv, {
|
|
7254
|
+
...runnerDeps,
|
|
7255
|
+
onPassResult: (passResult) => {
|
|
7256
|
+
result = passResult;
|
|
7257
|
+
},
|
|
7258
|
+
});
|
|
7259
|
+
return { exitCode, ...(result ? { result } : {}) };
|
|
7260
|
+
},
|
|
6911
7261
|
createWatcher: (options) => {
|
|
6912
7262
|
capture = options.captureLocalDeleteSnapshots;
|
|
6913
7263
|
return watcher;
|