@indigoai-us/hq-cloud 5.25.0 → 5.26.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.d.ts +100 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +214 -16
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +372 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/push-event.d.ts +110 -0
- package/dist/sync/push-event.d.ts.map +1 -0
- package/dist/sync/push-event.js +153 -0
- package/dist/sync/push-event.js.map +1 -0
- package/dist/sync/push-event.test.d.ts +15 -0
- package/dist/sync/push-event.test.d.ts.map +1 -0
- package/dist/sync/push-event.test.js +188 -0
- package/dist/sync/push-event.test.js.map +1 -0
- package/dist/sync/push-transport.d.ts +67 -0
- package/dist/sync/push-transport.d.ts.map +1 -0
- package/dist/sync/push-transport.js +66 -0
- package/dist/sync/push-transport.js.map +1 -0
- package/dist/watcher.d.ts +160 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +298 -0
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.d.ts +2 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +334 -0
- package/dist/watcher.test.js.map +1 -0
- package/package.json +3 -2
- package/src/bin/sync-runner.test.ts +487 -1
- package/src/bin/sync-runner.ts +305 -9
- package/src/index.ts +17 -0
- package/src/sync/index.ts +19 -0
- package/src/sync/push-event.test.ts +224 -0
- package/src/sync/push-event.ts +208 -0
- package/src/sync/push-transport.ts +84 -0
- package/src/watcher.test.ts +388 -0
- package/src/watcher.ts +386 -0
|
@@ -10,7 +10,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as path from "path";
|
|
13
|
-
import { runRunner, resolveDeletePolicy, resolveSkipPersonal } from "./sync-runner.js";
|
|
13
|
+
import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, routeChangeToTarget, buildTargetedPushArgv, } from "./sync-runner.js";
|
|
14
|
+
import { FakeClock } from "../watcher.js";
|
|
14
15
|
import { VaultAuthError } from "../vault-client.js";
|
|
15
16
|
function makeWriter() {
|
|
16
17
|
let buf = "";
|
|
@@ -1370,6 +1371,376 @@ describe("watch mode argv parsing", () => {
|
|
|
1370
1371
|
expect(code).toBe(1);
|
|
1371
1372
|
expect(deps.stderr.raw()).toContain("--poll-remote-ms requires --watch");
|
|
1372
1373
|
});
|
|
1374
|
+
it("accepts --event-push together with --watch (parser-level)", async () => {
|
|
1375
|
+
const deps = makeDeps({
|
|
1376
|
+
createVaultClient: () => makeVaultStub({ memberships: [] }),
|
|
1377
|
+
});
|
|
1378
|
+
// One-shot single pass via runRunner (no loop) — the parser must accept
|
|
1379
|
+
// --event-push as a known flag. No memberships → setup-needed → exit 0.
|
|
1380
|
+
const code = await runRunner(["--companies", "--watch", "--event-push"], deps);
|
|
1381
|
+
expect(deps.stderr.raw()).not.toContain("Unknown argument");
|
|
1382
|
+
expect(code).toBe(0);
|
|
1383
|
+
});
|
|
1384
|
+
it("rejects --event-push without --watch (the flag has no meaning otherwise)", async () => {
|
|
1385
|
+
const deps = makeDeps();
|
|
1386
|
+
const code = await runRunner(["--companies", "--event-push"], deps);
|
|
1387
|
+
expect(code).toBe(1);
|
|
1388
|
+
expect(deps.stderr.raw()).toContain("--event-push requires --watch");
|
|
1389
|
+
});
|
|
1390
|
+
it("is OFF by default — plain --watch run does not require/accept event flags implicitly", async () => {
|
|
1391
|
+
const deps = makeDeps({
|
|
1392
|
+
createVaultClient: () => makeVaultStub({ memberships: [] }),
|
|
1393
|
+
});
|
|
1394
|
+
const code = await runRunner(["--companies", "--watch"], deps);
|
|
1395
|
+
expect(deps.stderr.raw()).not.toContain("Unknown argument");
|
|
1396
|
+
expect(code).toBe(0);
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
// ---------------------------------------------------------------------------
|
|
1400
|
+
// US-003 — event-driven push routing helpers (pure)
|
|
1401
|
+
// ---------------------------------------------------------------------------
|
|
1402
|
+
describe("routeChangeToTarget", () => {
|
|
1403
|
+
it("routes companies/<slug>/... to a targeted company push", () => {
|
|
1404
|
+
expect(routeChangeToTarget("companies/indigo/board.json")).toEqual({
|
|
1405
|
+
kind: "company",
|
|
1406
|
+
slug: "indigo",
|
|
1407
|
+
});
|
|
1408
|
+
expect(routeChangeToTarget("companies/acme/projects/x/prd.json")).toEqual({ kind: "company", slug: "acme" });
|
|
1409
|
+
});
|
|
1410
|
+
it("routes non-company top-levels to the personal target", () => {
|
|
1411
|
+
expect(routeChangeToTarget("personal/notes.md")).toEqual({
|
|
1412
|
+
kind: "personal",
|
|
1413
|
+
});
|
|
1414
|
+
expect(routeChangeToTarget("core/policies/x.md")).toEqual({
|
|
1415
|
+
kind: "personal",
|
|
1416
|
+
});
|
|
1417
|
+
expect(routeChangeToTarget("README.md")).toEqual({ kind: "personal" });
|
|
1418
|
+
});
|
|
1419
|
+
it("normalizes OS path separators and ./ prefixes", () => {
|
|
1420
|
+
expect(routeChangeToTarget(["companies", "indigo", "f.json"].join(path.sep))).toEqual({ kind: "company", slug: "indigo" });
|
|
1421
|
+
expect(routeChangeToTarget("./companies/indigo/f.json")).toEqual({
|
|
1422
|
+
kind: "company",
|
|
1423
|
+
slug: "indigo",
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
it("returns null for unattributable paths (defensive)", () => {
|
|
1427
|
+
expect(routeChangeToTarget("")).toBeNull();
|
|
1428
|
+
expect(routeChangeToTarget("..")).toBeNull();
|
|
1429
|
+
expect(routeChangeToTarget("../escape")).toBeNull();
|
|
1430
|
+
// companies top-level with no slug segment is unattributable — a real
|
|
1431
|
+
// edit always lands at companies/<slug>/..., never at bare "companies".
|
|
1432
|
+
expect(routeChangeToTarget("companies")).toBeNull();
|
|
1433
|
+
expect(routeChangeToTarget("companies/")).toBeNull();
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
describe("buildTargetedPushArgv", () => {
|
|
1437
|
+
it("company route → --company <slug> --direction push", () => {
|
|
1438
|
+
expect(buildTargetedPushArgv({ kind: "company", slug: "indigo" }, [
|
|
1439
|
+
"--companies",
|
|
1440
|
+
"--hq-root",
|
|
1441
|
+
"/tmp/hq",
|
|
1442
|
+
"--on-conflict",
|
|
1443
|
+
"keep",
|
|
1444
|
+
])).toEqual([
|
|
1445
|
+
"--company",
|
|
1446
|
+
"indigo",
|
|
1447
|
+
"--direction",
|
|
1448
|
+
"push",
|
|
1449
|
+
"--hq-root",
|
|
1450
|
+
"/tmp/hq",
|
|
1451
|
+
"--on-conflict",
|
|
1452
|
+
"keep",
|
|
1453
|
+
]);
|
|
1454
|
+
});
|
|
1455
|
+
it("personal route → --companies --direction push (inherits root/conflict)", () => {
|
|
1456
|
+
expect(buildTargetedPushArgv({ kind: "personal" }, [
|
|
1457
|
+
"--companies",
|
|
1458
|
+
"--hq-root",
|
|
1459
|
+
"/tmp/hq",
|
|
1460
|
+
])).toEqual(["--companies", "--direction", "push", "--hq-root", "/tmp/hq"]);
|
|
1461
|
+
});
|
|
1462
|
+
it("carries no flags when the base argv has none", () => {
|
|
1463
|
+
expect(buildTargetedPushArgv({ kind: "personal" }, ["--companies"])).toEqual(["--companies", "--direction", "push"]);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
function makeWatcherStub() {
|
|
1467
|
+
const listeners = new Set();
|
|
1468
|
+
const stub = {
|
|
1469
|
+
started: false,
|
|
1470
|
+
disposed: false,
|
|
1471
|
+
onChange(listener) {
|
|
1472
|
+
listeners.add(listener);
|
|
1473
|
+
return () => listeners.delete(listener);
|
|
1474
|
+
},
|
|
1475
|
+
start() {
|
|
1476
|
+
this.started = true;
|
|
1477
|
+
},
|
|
1478
|
+
stop() {
|
|
1479
|
+
/* no-op for the stub */
|
|
1480
|
+
},
|
|
1481
|
+
dispose() {
|
|
1482
|
+
this.disposed = true;
|
|
1483
|
+
listeners.clear();
|
|
1484
|
+
},
|
|
1485
|
+
emit(changedRelPath) {
|
|
1486
|
+
for (const l of [...listeners])
|
|
1487
|
+
l(changedRelPath);
|
|
1488
|
+
},
|
|
1489
|
+
};
|
|
1490
|
+
return stub;
|
|
1491
|
+
}
|
|
1492
|
+
describe("runRunnerWithLoop — event-push wiring", () => {
|
|
1493
|
+
it("starts the watcher alongside the poll loop when --event-push is on", async () => {
|
|
1494
|
+
const watcher = makeWatcherStub();
|
|
1495
|
+
let triggerShutdown = () => { };
|
|
1496
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
1497
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1498
|
+
runPass,
|
|
1499
|
+
clock: new FakeClock(),
|
|
1500
|
+
createWatcher: () => watcher,
|
|
1501
|
+
sleep: () => new Promise(() => { }), // never resolves; shutdown ends it
|
|
1502
|
+
onShutdownSignal: (handler) => {
|
|
1503
|
+
triggerShutdown = handler;
|
|
1504
|
+
return () => { };
|
|
1505
|
+
},
|
|
1506
|
+
});
|
|
1507
|
+
// Let the first poll pass run, then shut down.
|
|
1508
|
+
await Promise.resolve();
|
|
1509
|
+
await Promise.resolve();
|
|
1510
|
+
expect(watcher.started).toBe(true);
|
|
1511
|
+
triggerShutdown();
|
|
1512
|
+
await loop;
|
|
1513
|
+
// The poll pass ran at least once (safety-net path still runs).
|
|
1514
|
+
expect(runPass).toHaveBeenCalled();
|
|
1515
|
+
expect(watcher.disposed).toBe(true);
|
|
1516
|
+
});
|
|
1517
|
+
it("trigger-on-change: a debounced changed signal runs a targeted company push", async () => {
|
|
1518
|
+
const watcher = makeWatcherStub();
|
|
1519
|
+
const clock = new FakeClock();
|
|
1520
|
+
let triggerShutdown = () => { };
|
|
1521
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
1522
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1523
|
+
runPass,
|
|
1524
|
+
clock,
|
|
1525
|
+
createWatcher: () => watcher,
|
|
1526
|
+
sleep: () => new Promise(() => { }),
|
|
1527
|
+
onShutdownSignal: (handler) => {
|
|
1528
|
+
triggerShutdown = handler;
|
|
1529
|
+
return () => { };
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1532
|
+
// Drain the initial poll pass.
|
|
1533
|
+
await Promise.resolve();
|
|
1534
|
+
await Promise.resolve();
|
|
1535
|
+
runPass.mockClear();
|
|
1536
|
+
// Fire a change for an indigo file; advance the (zero) driver window.
|
|
1537
|
+
watcher.emit("companies/indigo/board.json");
|
|
1538
|
+
clock.advance(0);
|
|
1539
|
+
await Promise.resolve();
|
|
1540
|
+
await Promise.resolve();
|
|
1541
|
+
const targetedCall = runPass.mock.calls.find((c) => c[0].includes("--company"));
|
|
1542
|
+
expect(targetedCall).toBeDefined();
|
|
1543
|
+
expect(targetedCall[0]).toEqual([
|
|
1544
|
+
"--company",
|
|
1545
|
+
"indigo",
|
|
1546
|
+
"--direction",
|
|
1547
|
+
"push",
|
|
1548
|
+
"--hq-root",
|
|
1549
|
+
"/tmp/hq",
|
|
1550
|
+
]);
|
|
1551
|
+
triggerShutdown();
|
|
1552
|
+
await loop;
|
|
1553
|
+
});
|
|
1554
|
+
it("bare signal (no path) routes the targeted push to the personal vault", async () => {
|
|
1555
|
+
const watcher = makeWatcherStub();
|
|
1556
|
+
const clock = new FakeClock();
|
|
1557
|
+
let triggerShutdown = () => { };
|
|
1558
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
1559
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1560
|
+
runPass,
|
|
1561
|
+
clock,
|
|
1562
|
+
createWatcher: () => watcher,
|
|
1563
|
+
sleep: () => new Promise(() => { }),
|
|
1564
|
+
onShutdownSignal: (handler) => {
|
|
1565
|
+
triggerShutdown = handler;
|
|
1566
|
+
return () => { };
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
await Promise.resolve();
|
|
1570
|
+
await Promise.resolve();
|
|
1571
|
+
runPass.mockClear();
|
|
1572
|
+
watcher.emit(); // no path — TreeWatcher's bare signal shape
|
|
1573
|
+
clock.advance(0);
|
|
1574
|
+
await Promise.resolve();
|
|
1575
|
+
await Promise.resolve();
|
|
1576
|
+
const personalCall = runPass.mock.calls.find((c) => c[0].includes("--companies") &&
|
|
1577
|
+
c[0].includes("--direction"));
|
|
1578
|
+
expect(personalCall).toBeDefined();
|
|
1579
|
+
expect(personalCall[0]).toEqual([
|
|
1580
|
+
"--companies",
|
|
1581
|
+
"--direction",
|
|
1582
|
+
"push",
|
|
1583
|
+
"--hq-root",
|
|
1584
|
+
"/tmp/hq",
|
|
1585
|
+
]);
|
|
1586
|
+
triggerShutdown();
|
|
1587
|
+
await loop;
|
|
1588
|
+
});
|
|
1589
|
+
it("overlap guard: a change during an in-flight pass does not start a concurrent pass", async () => {
|
|
1590
|
+
const watcher = makeWatcherStub();
|
|
1591
|
+
const clock = new FakeClock();
|
|
1592
|
+
let triggerShutdown = () => { };
|
|
1593
|
+
// A controllable in-flight pass: the first call hangs until we release it.
|
|
1594
|
+
let release = () => { };
|
|
1595
|
+
const firstGate = new Promise((r) => {
|
|
1596
|
+
release = r;
|
|
1597
|
+
});
|
|
1598
|
+
let callCount = 0;
|
|
1599
|
+
let maxConcurrent = 0;
|
|
1600
|
+
let active = 0;
|
|
1601
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
1602
|
+
active++;
|
|
1603
|
+
maxConcurrent = Math.max(maxConcurrent, active);
|
|
1604
|
+
callCount++;
|
|
1605
|
+
if (callCount === 1)
|
|
1606
|
+
await firstGate; // hold the first (poll) pass
|
|
1607
|
+
active--;
|
|
1608
|
+
return 0;
|
|
1609
|
+
});
|
|
1610
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1611
|
+
runPass,
|
|
1612
|
+
clock,
|
|
1613
|
+
createWatcher: () => watcher,
|
|
1614
|
+
sleep: () => new Promise(() => { }),
|
|
1615
|
+
onShutdownSignal: (handler) => {
|
|
1616
|
+
triggerShutdown = handler;
|
|
1617
|
+
return () => { };
|
|
1618
|
+
},
|
|
1619
|
+
});
|
|
1620
|
+
// The first poll pass is now in flight and gated.
|
|
1621
|
+
await Promise.resolve();
|
|
1622
|
+
await Promise.resolve();
|
|
1623
|
+
expect(active).toBe(1);
|
|
1624
|
+
// Fire a change while the pass is in flight → must be collapsed, not run.
|
|
1625
|
+
watcher.emit("companies/indigo/x.json");
|
|
1626
|
+
clock.advance(0);
|
|
1627
|
+
await Promise.resolve();
|
|
1628
|
+
await Promise.resolve();
|
|
1629
|
+
// Still exactly one active pass; the watcher trigger did not overlap.
|
|
1630
|
+
expect(maxConcurrent).toBe(1);
|
|
1631
|
+
// Release the gated pass; the collapsed change re-arms and runs next.
|
|
1632
|
+
release();
|
|
1633
|
+
await Promise.resolve();
|
|
1634
|
+
await Promise.resolve();
|
|
1635
|
+
clock.advance(0);
|
|
1636
|
+
await Promise.resolve();
|
|
1637
|
+
await Promise.resolve();
|
|
1638
|
+
// Never more than one pass concurrently across the whole run.
|
|
1639
|
+
expect(maxConcurrent).toBe(1);
|
|
1640
|
+
triggerShutdown();
|
|
1641
|
+
await loop;
|
|
1642
|
+
});
|
|
1643
|
+
it("poll-still-runs: the poll loop fires passes independent of the watcher", async () => {
|
|
1644
|
+
const watcher = makeWatcherStub();
|
|
1645
|
+
let triggerShutdown = () => { };
|
|
1646
|
+
let passes = 0;
|
|
1647
|
+
// Box the resolver so TS control-flow doesn't narrow it to `null` at the
|
|
1648
|
+
// call sites below (the assignment happens inside a deferred closure).
|
|
1649
|
+
const poll = { resolve: null };
|
|
1650
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
1651
|
+
passes++;
|
|
1652
|
+
return 0;
|
|
1653
|
+
});
|
|
1654
|
+
// A sleep we can step: resolve once to allow a second poll pass.
|
|
1655
|
+
const sleep = () => new Promise((resolve) => {
|
|
1656
|
+
poll.resolve = resolve;
|
|
1657
|
+
});
|
|
1658
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1659
|
+
runPass,
|
|
1660
|
+
clock: new FakeClock(),
|
|
1661
|
+
createWatcher: () => watcher,
|
|
1662
|
+
sleep,
|
|
1663
|
+
onShutdownSignal: (handler) => {
|
|
1664
|
+
triggerShutdown = handler;
|
|
1665
|
+
return () => { };
|
|
1666
|
+
},
|
|
1667
|
+
});
|
|
1668
|
+
// First poll pass runs, then awaits sleep.
|
|
1669
|
+
await Promise.resolve();
|
|
1670
|
+
await Promise.resolve();
|
|
1671
|
+
expect(passes).toBe(1);
|
|
1672
|
+
// Step the poll interval → a second poll pass runs (safety net cadence).
|
|
1673
|
+
poll.resolve?.();
|
|
1674
|
+
await Promise.resolve();
|
|
1675
|
+
await Promise.resolve();
|
|
1676
|
+
expect(passes).toBeGreaterThanOrEqual(2);
|
|
1677
|
+
triggerShutdown();
|
|
1678
|
+
// Release any pending sleep so the loop can observe shutdown and exit.
|
|
1679
|
+
poll.resolve?.();
|
|
1680
|
+
await loop;
|
|
1681
|
+
});
|
|
1682
|
+
it("clean shutdown: SIGTERM disposes the watcher and resolves the loop with no leaks", async () => {
|
|
1683
|
+
const watcher = makeWatcherStub();
|
|
1684
|
+
const clock = new FakeClock();
|
|
1685
|
+
let triggerShutdown = () => { };
|
|
1686
|
+
let detached = false;
|
|
1687
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
1688
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1689
|
+
runPass,
|
|
1690
|
+
clock,
|
|
1691
|
+
createWatcher: () => watcher,
|
|
1692
|
+
sleep: () => new Promise(() => { }),
|
|
1693
|
+
onShutdownSignal: (handler) => {
|
|
1694
|
+
triggerShutdown = handler;
|
|
1695
|
+
return () => {
|
|
1696
|
+
detached = true;
|
|
1697
|
+
};
|
|
1698
|
+
},
|
|
1699
|
+
});
|
|
1700
|
+
await Promise.resolve();
|
|
1701
|
+
await Promise.resolve();
|
|
1702
|
+
expect(watcher.started).toBe(true);
|
|
1703
|
+
triggerShutdown();
|
|
1704
|
+
const code = await loop;
|
|
1705
|
+
expect(code).toBe(0);
|
|
1706
|
+
expect(watcher.disposed).toBe(true); // watcher torn down
|
|
1707
|
+
expect(detached).toBe(true); // signal handler detached (no leaked listener)
|
|
1708
|
+
// No pending FakeClock timers left behind by the driver's debounce.
|
|
1709
|
+
expect(clock.pendingTimerCount()).toBe(0);
|
|
1710
|
+
});
|
|
1711
|
+
it("a non-zero poll pass exit returns immediately (hard error surfaces)", async () => {
|
|
1712
|
+
const watcher = makeWatcherStub();
|
|
1713
|
+
const runPass = vi.fn().mockResolvedValue(2);
|
|
1714
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
1715
|
+
runPass,
|
|
1716
|
+
clock: new FakeClock(),
|
|
1717
|
+
createWatcher: () => watcher,
|
|
1718
|
+
sleep: () => Promise.resolve(),
|
|
1719
|
+
onShutdownSignal: () => () => { },
|
|
1720
|
+
});
|
|
1721
|
+
expect(code).toBe(2);
|
|
1722
|
+
expect(watcher.disposed).toBe(true);
|
|
1723
|
+
});
|
|
1724
|
+
it("without --event-push, no watcher is created (poll-only safety net)", async () => {
|
|
1725
|
+
let triggerShutdown = () => { };
|
|
1726
|
+
const createWatcher = vi.fn(() => makeWatcherStub());
|
|
1727
|
+
const runPass = vi.fn().mockResolvedValue(0);
|
|
1728
|
+
const loop = runRunnerWithLoop(["--companies", "--watch"], {
|
|
1729
|
+
runPass,
|
|
1730
|
+
createWatcher,
|
|
1731
|
+
sleep: () => new Promise(() => { }),
|
|
1732
|
+
onShutdownSignal: (handler) => {
|
|
1733
|
+
triggerShutdown = handler;
|
|
1734
|
+
return () => { };
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
await Promise.resolve();
|
|
1738
|
+
await Promise.resolve();
|
|
1739
|
+
expect(createWatcher).not.toHaveBeenCalled();
|
|
1740
|
+
expect(runPass).toHaveBeenCalled();
|
|
1741
|
+
triggerShutdown();
|
|
1742
|
+
await loop;
|
|
1743
|
+
});
|
|
1373
1744
|
});
|
|
1374
1745
|
// ---------------------------------------------------------------------------
|
|
1375
1746
|
// Re-initialize for each test (mock state hygiene)
|