@indigoai-us/hq-cloud 6.15.75 → 6.15.76

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.
@@ -4,7 +4,7 @@ import * as os from "os";
4
4
  import * as path from "path";
5
5
  import { createHash } from "node:crypto";
6
6
  import { watch } from "chokidar";
7
- import { FakeClock, WatchPushDriver, TreeWatcher, ChokidarWatchBudget, createWatchPathFilter, toChokidarIgnored, PushEventEmitter, PUBLISH_OUTCOME_JOURNALD_CEILING_BYTES, PUBLISH_OUTCOME_SUCCESS_SAMPLE_CAP, resolveEventDebounceConfig, summarizePublishBatchOutcome, DEFAULT_EVENT_DEBOUNCE_MS, DEFAULT_EVENT_MAX_WAIT_MS, EVENT_DEBOUNCE_MS_ENV, EVENT_MAX_WAIT_MS_ENV, DEFAULT_TREE_WATCHER_MAX_WATCHED_PATHS, MAX_TREE_WATCHER_MAX_WATCHED_PATHS, TREE_WATCHER_MAX_WATCHED_PATHS_ENV, resolveMaxWatchedPaths, } from "./watcher.js";
7
+ import { FakeClock, WatchPushDriver, TreeWatcher, ChokidarWatchBudget, createWatchPathFilter, toChokidarIgnored, PushEventEmitter, PUBLISH_OUTCOME_JOURNALD_CEILING_BYTES, PUBLISH_OUTCOME_SUCCESS_SAMPLE_CAP, resolveEventDebounceConfig, summarizePublishBatchOutcome, DEFAULT_EVENT_DEBOUNCE_MS, DEFAULT_EVENT_MAX_WAIT_MS, DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS, DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS, DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES, DEFAULT_PUBLISH_MIN_INTERVAL_MS, EVENT_DEBOUNCE_MS_ENV, EVENT_MAX_WAIT_MS_ENV, EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV, EVENT_SMALL_BATCH_MAX_PATHS_ENV, EVENT_SMALL_BATCH_MAX_BYTES_ENV, DEFAULT_TREE_WATCHER_MAX_WATCHED_PATHS, MAX_TREE_WATCHER_MAX_WATCHED_PATHS, TREE_WATCHER_MAX_WATCHED_PATHS_ENV, resolveMaxWatchedPaths, PUBLISH_MIN_INTERVAL_MS_ENV, resolvePublishMinIntervalMs, } from "./watcher.js";
8
8
  import { StaticFlagProvider } from "./sync/feature-flags.js";
9
9
  import { computePersonalVaultPaths } from "./personal-vault.js";
10
10
  /**
@@ -790,6 +790,167 @@ describe("US-002: TreeWatcher — debounce coalesce (FakeClock seam)", () => {
790
790
  clock.advance(1);
791
791
  expect(changed).toHaveBeenCalledTimes(1);
792
792
  });
793
+ it("flushes a quiet, known-small batch through the short debounce window", () => {
794
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-small-debounce-"));
795
+ try {
796
+ const small = path.join(dir, "small.md");
797
+ fs.writeFileSync(small, "sixteen-byte-doc");
798
+ const clock = new FakeClock();
799
+ const changed = vi.fn();
800
+ const watcher = new TreeWatcher({
801
+ hqRoot: dir,
802
+ debounceMs: 15_000,
803
+ smallBatchDebounceMs: 2_500,
804
+ smallBatchMaxPaths: 8,
805
+ smallBatchMaxBytes: 1024 * 1024,
806
+ clock,
807
+ pathFilter: () => true,
808
+ });
809
+ watcher.onChange(changed);
810
+ watcher.handleEvent(small);
811
+ clock.advance(2_499);
812
+ expect(changed).not.toHaveBeenCalled();
813
+ clock.advance(1);
814
+ expect(changed).toHaveBeenCalledTimes(1);
815
+ expect(changed.mock.calls[0][1].paths.get(small)).toBe("small.md");
816
+ watcher.dispose();
817
+ }
818
+ finally {
819
+ fs.rmSync(dir, { recursive: true, force: true });
820
+ }
821
+ });
822
+ it("keeps a burst above the small-path cap on the 15s debounce", () => {
823
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-burst-debounce-"));
824
+ try {
825
+ const clock = new FakeClock();
826
+ const changed = vi.fn();
827
+ const watcher = new TreeWatcher({
828
+ hqRoot: dir,
829
+ debounceMs: 15_000,
830
+ smallBatchDebounceMs: 2_500,
831
+ smallBatchMaxPaths: 8,
832
+ smallBatchMaxBytes: 1024 * 1024,
833
+ clock,
834
+ pathFilter: () => true,
835
+ });
836
+ watcher.onChange(changed);
837
+ for (let index = 0; index < 9; index += 1) {
838
+ const file = path.join(dir, `burst-${index}.md`);
839
+ fs.writeFileSync(file, "small");
840
+ watcher.handleEvent(file);
841
+ clock.advance(100);
842
+ }
843
+ // The final event landed at t=800ms; the loop's final 100ms advance is
844
+ // outside the quiet window, so stop one millisecond before t=15_800ms.
845
+ clock.advance(14_899);
846
+ expect(changed).not.toHaveBeenCalled();
847
+ clock.advance(1);
848
+ expect(changed).toHaveBeenCalledTimes(1);
849
+ expect(changed.mock.calls[0][1].paths.size).toBe(9);
850
+ watcher.dispose();
851
+ }
852
+ finally {
853
+ fs.rmSync(dir, { recursive: true, force: true });
854
+ }
855
+ });
856
+ it("preserves max-wait for a continuous small batch that never becomes quiet", () => {
857
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-small-max-wait-"));
858
+ try {
859
+ const file = path.join(dir, "continuous.md");
860
+ fs.writeFileSync(file, "small");
861
+ const clock = new FakeClock();
862
+ const changed = vi.fn();
863
+ const watcher = new TreeWatcher({
864
+ hqRoot: dir,
865
+ debounceMs: 15_000,
866
+ maxWaitMs: 120_000,
867
+ smallBatchDebounceMs: 2_500,
868
+ smallBatchMaxPaths: 8,
869
+ smallBatchMaxBytes: 1024 * 1024,
870
+ clock,
871
+ pathFilter: () => true,
872
+ });
873
+ watcher.onChange(changed);
874
+ for (let elapsed = 0; elapsed < 120_000; elapsed += 2_000) {
875
+ watcher.handleEvent(file);
876
+ if (elapsed < 118_000)
877
+ clock.advance(2_000);
878
+ }
879
+ expect(changed).not.toHaveBeenCalled();
880
+ clock.advance(2_000);
881
+ expect(changed).toHaveBeenCalledTimes(1);
882
+ watcher.dispose();
883
+ }
884
+ finally {
885
+ fs.rmSync(dir, { recursive: true, force: true });
886
+ }
887
+ });
888
+ it("keeps a duplicate directory delete out of the short debounce lane", () => {
889
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-delete-debounce-"));
890
+ try {
891
+ const deleted = path.join(dir, "deleted-directory");
892
+ const clock = new FakeClock();
893
+ const changed = vi.fn();
894
+ const watcher = new TreeWatcher({
895
+ hqRoot: dir,
896
+ debounceMs: 15_000,
897
+ smallBatchDebounceMs: 2_500,
898
+ smallBatchMaxPaths: 8,
899
+ smallBatchMaxBytes: 1024 * 1024,
900
+ clock,
901
+ pathFilter: () => true,
902
+ });
903
+ watcher.onChange(changed);
904
+ watcher.handleEvent(deleted, "unlinkDir");
905
+ watcher.handleEvent(deleted, "unlink");
906
+ clock.advance(2_500);
907
+ expect(changed).not.toHaveBeenCalled();
908
+ clock.advance(12_500);
909
+ expect(changed).toHaveBeenCalledTimes(1);
910
+ expect(changed.mock.calls[0][1].changes.get(deleted)).toMatchObject({
911
+ kind: "unlinkDir",
912
+ });
913
+ watcher.dispose();
914
+ }
915
+ finally {
916
+ fs.rmSync(dir, { recursive: true, force: true });
917
+ }
918
+ });
919
+ it("keeps an overflowed batch out of the short debounce lane", () => {
920
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-overflow-debounce-"));
921
+ try {
922
+ const first = path.join(dir, "first.md");
923
+ const dropped = path.join(dir, "dropped.md");
924
+ fs.writeFileSync(first, "small");
925
+ fs.writeFileSync(dropped, "small");
926
+ const clock = new FakeClock();
927
+ const changed = vi.fn();
928
+ const watcher = new TreeWatcher({
929
+ hqRoot: dir,
930
+ debounceMs: 15_000,
931
+ smallBatchDebounceMs: 2_500,
932
+ smallBatchMaxPaths: 8,
933
+ smallBatchMaxBytes: 1024 * 1024,
934
+ maxPendingPaths: 1,
935
+ maxPendingBytes: 1024 * 1024,
936
+ clock,
937
+ pathFilter: () => true,
938
+ onBacklogOverflow: () => undefined,
939
+ });
940
+ watcher.onChange(changed);
941
+ watcher.handleEvent(first);
942
+ watcher.handleEvent(dropped);
943
+ clock.advance(2_500);
944
+ expect(changed).not.toHaveBeenCalled();
945
+ clock.advance(12_500);
946
+ expect(changed).toHaveBeenCalledTimes(1);
947
+ expect(changed.mock.calls[0][1]).toMatchObject({ overflowed: true });
948
+ watcher.dispose();
949
+ }
950
+ finally {
951
+ fs.rmSync(dir, { recursive: true, force: true });
952
+ }
953
+ });
793
954
  it("does NOT emit for an ignored / excluded path", () => {
794
955
  const { clock, changed, watcher } = makeWatcher({ personalMode: true });
795
956
  watcher.handleEvent(path.join(ROOT, ".env"));
@@ -1249,9 +1410,11 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
1249
1410
  transport,
1250
1411
  flagProvider: new StaticFlagProvider(["tenant-indigo"]),
1251
1412
  now: () => new Date("2026-06-18T12:00:00.000Z"),
1413
+ clock: opts?.clock,
1252
1414
  getSequenceNumber: opts?.getSequenceNumber,
1253
1415
  onError: opts?.onError,
1254
1416
  lastPublishedContentHashMaxPaths: opts?.lastPublishedContentHashMaxPaths,
1417
+ publishMinIntervalMs: opts?.publishMinIntervalMs ?? 0,
1255
1418
  ...(opts?.lastPublishedContentHashStateDir
1256
1419
  ? { lastPublishedContentHashStateDir: opts.lastPublishedContentHashStateDir }
1257
1420
  : {}),
@@ -1371,6 +1534,109 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
1371
1534
  ]);
1372
1535
  expect(published[0].contentHash).not.toBe(published[1].contentHash);
1373
1536
  });
1537
+ it("coalesces ten rewrites of one path into an immediate and trailing latest publish", async () => {
1538
+ const clock = new FakeClock();
1539
+ const published = [];
1540
+ const rewritten = path.join(dir, "high-churn-prd.json");
1541
+ const relativePath = "personal/projects/churn/prd.json";
1542
+ const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
1543
+ const batch = { paths: new Map([[rewritten, relativePath]]) };
1544
+ for (let version = 0; version < 10; version += 1) {
1545
+ fs.writeFileSync(rewritten, `{"version":${version}}\n`);
1546
+ await emitter.emitForBatch(batch);
1547
+ if (version < 9)
1548
+ clock.advance(5_000);
1549
+ }
1550
+ expect(published).toHaveLength(1);
1551
+ clock.advance(15_000);
1552
+ await emitter.emitForBatch({ paths: new Map() });
1553
+ expect(published).toHaveLength(2);
1554
+ expect(clock.pendingTimerCount()).toBe(0);
1555
+ expect(published[1]).toMatchObject({ kind: "upsert", relativePath });
1556
+ expect(published[1].contentHash).toBe(`sha256:${createHash("sha256").update('{"version":9}\n').digest("hex")}`);
1557
+ });
1558
+ it("publishes the uploaded deferred revision when the local file changes before flush", async () => {
1559
+ const clock = new FakeClock();
1560
+ const published = [];
1561
+ const rewritten = path.join(dir, "captured-deferred-revision.json");
1562
+ const relativePath = "personal/projects/captured/prd.json";
1563
+ const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
1564
+ const batch = { paths: new Map([[rewritten, relativePath]]) };
1565
+ fs.writeFileSync(rewritten, '{"revision":"uploaded"}\n');
1566
+ await emitter.emitForBatch(batch);
1567
+ fs.writeFileSync(rewritten, '{"revision":"deferred-uploaded"}\n');
1568
+ await emitter.emitForBatch(batch);
1569
+ const expectedHash = `sha256:${createHash("sha256")
1570
+ .update('{"revision":"deferred-uploaded"}\n')
1571
+ .digest("hex")}`;
1572
+ // This local revision has not completed its scoped push yet. The trailing
1573
+ // announcement must remain bound to the previously uploaded bytes.
1574
+ fs.writeFileSync(rewritten, '{"revision":"newer-local"}\n');
1575
+ clock.advance(60_000);
1576
+ await emitter.emitForBatch({ paths: new Map() });
1577
+ expect(published).toHaveLength(2);
1578
+ expect(published[1]).toMatchObject({
1579
+ kind: "upsert",
1580
+ relativePath,
1581
+ contentHash: expectedHash,
1582
+ });
1583
+ expect(published[1].contentHash).not.toBe(`sha256:${createHash("sha256")
1584
+ .update('{"revision":"newer-local"}\n')
1585
+ .digest("hex")}`);
1586
+ });
1587
+ it("skips the trailing publish when the final rewrite restores the last published hash", async () => {
1588
+ const clock = new FakeClock();
1589
+ const published = [];
1590
+ const rewritten = path.join(dir, "reverted-prd.json");
1591
+ const relativePath = "personal/projects/reverted/prd.json";
1592
+ const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
1593
+ const batch = { paths: new Map([[rewritten, relativePath]]) };
1594
+ fs.writeFileSync(rewritten, "A\n");
1595
+ await emitter.emitForBatch(batch);
1596
+ fs.writeFileSync(rewritten, "B\n");
1597
+ await emitter.emitForBatch(batch);
1598
+ fs.writeFileSync(rewritten, "A\n");
1599
+ await emitter.emitForBatch(batch);
1600
+ clock.advance(60_000);
1601
+ await emitter.emitForBatch({ paths: new Map() });
1602
+ expect(published).toHaveLength(1);
1603
+ expect(clock.pendingTimerCount()).toBe(0);
1604
+ expect(published[0]).toMatchObject({ kind: "upsert", relativePath });
1605
+ });
1606
+ it("publishes a delete immediately while an upsert is deferred", async () => {
1607
+ const clock = new FakeClock();
1608
+ const published = [];
1609
+ const deleted = path.join(dir, "delete-during-quiet.jsonl");
1610
+ const relativePath = "workspace/.session-logs/delete-during-quiet.jsonl";
1611
+ const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
1612
+ const batch = { paths: new Map([[deleted, relativePath]]) };
1613
+ fs.writeFileSync(deleted, "A\n");
1614
+ await emitter.emitForBatch(batch);
1615
+ fs.writeFileSync(deleted, "B\n");
1616
+ await emitter.emitForBatch(batch);
1617
+ fs.rmSync(deleted);
1618
+ await emitter.emitForBatch(batch);
1619
+ expect(published.map((event) => event.kind)).toEqual(["upsert", "delete"]);
1620
+ clock.advance(60_000);
1621
+ await emitter.emitForBatch({ paths: new Map() });
1622
+ expect(published).toHaveLength(2);
1623
+ });
1624
+ it("does not make a second path wait behind another path's quiet interval", async () => {
1625
+ const published = [];
1626
+ const first = path.join(dir, "first-high-churn.jsonl");
1627
+ const second = path.join(dir, "independent.jsonl");
1628
+ const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000 });
1629
+ fs.writeFileSync(first, "A\n");
1630
+ fs.writeFileSync(second, "B\n");
1631
+ await emitter.emitForBatch({ paths: new Map([[first, "workspace/.session-logs/first.jsonl"]]) });
1632
+ fs.writeFileSync(first, "A2\n");
1633
+ await emitter.emitForBatch({ paths: new Map([[first, "workspace/.session-logs/first.jsonl"]]) });
1634
+ await emitter.emitForBatch({ paths: new Map([[second, "workspace/.session-logs/second.jsonl"]]) });
1635
+ expect(published.map((event) => event.relativePath)).toEqual([
1636
+ "workspace/.session-logs/first.jsonl",
1637
+ "workspace/.session-logs/second.jsonl",
1638
+ ]);
1639
+ });
1374
1640
  it("publishes a content revert because it compares only with the last publication", async () => {
1375
1641
  const published = [];
1376
1642
  const reverted = path.join(dir, "reverted-session-log.jsonl");
@@ -1395,6 +1661,7 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
1395
1661
  originTenantId: "tenant-indigo",
1396
1662
  originDeviceId: "device-a",
1397
1663
  flagProvider: new StaticFlagProvider(["tenant-indigo"]),
1664
+ publishMinIntervalMs: 0,
1398
1665
  transport: {
1399
1666
  start: async () => { },
1400
1667
  dispose: async () => { },
@@ -1758,7 +2025,13 @@ describe("TreeWatcher deferred delete snapshots", () => {
1758
2025
  // Event-pass debounce configuration (HQ_SYNC_EVENT_DEBOUNCE_MS / _MAX_WAIT_MS)
1759
2026
  // ---------------------------------------------------------------------------
1760
2027
  describe("resolveEventDebounceConfig", () => {
1761
- const ENV_KEYS = [EVENT_DEBOUNCE_MS_ENV, EVENT_MAX_WAIT_MS_ENV];
2028
+ const ENV_KEYS = [
2029
+ EVENT_DEBOUNCE_MS_ENV,
2030
+ EVENT_MAX_WAIT_MS_ENV,
2031
+ EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV,
2032
+ EVENT_SMALL_BATCH_MAX_PATHS_ENV,
2033
+ EVENT_SMALL_BATCH_MAX_BYTES_ENV,
2034
+ ];
1762
2035
  let saved;
1763
2036
  beforeEach(() => {
1764
2037
  saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
@@ -1777,6 +2050,9 @@ describe("resolveEventDebounceConfig", () => {
1777
2050
  expect(resolveEventDebounceConfig()).toEqual({
1778
2051
  debounceMs: DEFAULT_EVENT_DEBOUNCE_MS,
1779
2052
  maxWaitMs: DEFAULT_EVENT_MAX_WAIT_MS,
2053
+ smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
2054
+ smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
2055
+ smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
1780
2056
  });
1781
2057
  expect(DEFAULT_EVENT_DEBOUNCE_MS).toBe(15_000);
1782
2058
  expect(DEFAULT_EVENT_MAX_WAIT_MS).toBe(120_000);
@@ -1784,9 +2060,15 @@ describe("resolveEventDebounceConfig", () => {
1784
2060
  it("honors env overrides", () => {
1785
2061
  process.env[EVENT_DEBOUNCE_MS_ENV] = "5000";
1786
2062
  process.env[EVENT_MAX_WAIT_MS_ENV] = "30000";
2063
+ process.env[EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV] = "2000";
2064
+ process.env[EVENT_SMALL_BATCH_MAX_PATHS_ENV] = "6";
2065
+ process.env[EVENT_SMALL_BATCH_MAX_BYTES_ENV] = "4096";
1787
2066
  expect(resolveEventDebounceConfig()).toEqual({
1788
2067
  debounceMs: 5000,
1789
2068
  maxWaitMs: 30_000,
2069
+ smallBatchDebounceMs: 2000,
2070
+ smallBatchMaxPaths: 6,
2071
+ smallBatchMaxBytes: 4096,
1790
2072
  });
1791
2073
  });
1792
2074
  it("treats 0, negative, and non-numeric values as unset (defaults)", () => {
@@ -1796,6 +2078,9 @@ describe("resolveEventDebounceConfig", () => {
1796
2078
  expect(resolveEventDebounceConfig()).toEqual({
1797
2079
  debounceMs: DEFAULT_EVENT_DEBOUNCE_MS,
1798
2080
  maxWaitMs: DEFAULT_EVENT_MAX_WAIT_MS,
2081
+ smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
2082
+ smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
2083
+ smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
1799
2084
  });
1800
2085
  }
1801
2086
  });
@@ -1805,13 +2090,36 @@ describe("resolveEventDebounceConfig", () => {
1805
2090
  expect(resolveEventDebounceConfig()).toEqual({
1806
2091
  debounceMs: 60_000,
1807
2092
  maxWaitMs: 60_000,
2093
+ smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
2094
+ smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
2095
+ smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
1808
2096
  });
1809
2097
  });
1810
2098
  it("accepts an injected env record without touching process.env", () => {
1811
2099
  expect(resolveEventDebounceConfig({
1812
2100
  [EVENT_DEBOUNCE_MS_ENV]: "1000",
1813
2101
  [EVENT_MAX_WAIT_MS_ENV]: "2000",
1814
- })).toEqual({ debounceMs: 1000, maxWaitMs: 2000 });
2102
+ [EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV]: "750",
2103
+ [EVENT_SMALL_BATCH_MAX_PATHS_ENV]: "4",
2104
+ [EVENT_SMALL_BATCH_MAX_BYTES_ENV]: "1024",
2105
+ })).toEqual({
2106
+ debounceMs: 1000,
2107
+ maxWaitMs: 2000,
2108
+ smallBatchDebounceMs: 750,
2109
+ smallBatchMaxPaths: 4,
2110
+ smallBatchMaxBytes: 1024,
2111
+ });
2112
+ });
2113
+ });
2114
+ describe("resolvePublishMinIntervalMs", () => {
2115
+ it("defaults to 60 seconds, accepts zero to disable coalescing, and rejects invalid values", () => {
2116
+ expect(resolvePublishMinIntervalMs({})).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
2117
+ expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "0" })).toBe(0);
2118
+ expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "invalid" })).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
2119
+ expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "-1" })).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
2120
+ });
2121
+ it("uses the environment override in milliseconds", () => {
2122
+ expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "1234.9" })).toBe(1234);
1815
2123
  });
1816
2124
  });
1817
2125
  //# sourceMappingURL=watcher.test.js.map