@rubytech/create-realagent-code 0.1.604 → 0.1.605

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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/routine-templates/dist/roster.d.ts +30 -2
  3. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  4. package/payload/platform/lib/routine-templates/dist/roster.js +76 -32
  5. package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
  6. package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +0 -0
  7. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +22 -1
  8. package/payload/platform/lib/routine-templates/src/roster.ts +108 -35
  9. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
  10. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +5 -0
  11. package/payload/platform/plugins/connector/PLUGIN.md +9 -1
  12. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts +2 -0
  13. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts.map +1 -0
  14. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js +67 -0
  15. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js.map +1 -0
  16. package/payload/platform/plugins/connector/mcp/dist/index.js +36 -5
  17. package/payload/platform/plugins/connector/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts +35 -0
  19. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts.map +1 -1
  20. package/payload/platform/plugins/connector/mcp/dist/lib/store.js +33 -0
  21. package/payload/platform/plugins/connector/mcp/dist/lib/store.js.map +1 -1
  22. package/payload/platform/plugins/connector/mcp/package.json +4 -2
  23. package/payload/platform/plugins/connector/mcp/vitest.config.ts +8 -0
  24. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  25. package/payload/platform/plugins/email/PLUGIN.md +4 -0
  26. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts +2 -0
  27. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js +233 -0
  29. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js.map +1 -0
  30. package/payload/platform/plugins/email/mcp/dist/sampler/main.js +19 -0
  31. package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
  32. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts +3 -0
  33. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts.map +1 -0
  34. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js +146 -0
  35. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js.map +1 -0
  36. package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts +150 -0
  37. package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts.map +1 -0
  38. package/payload/platform/plugins/email/mcp/dist/sampler/retention.js +199 -0
  39. package/payload/platform/plugins/email/mcp/dist/sampler/retention.js.map +1 -0
  40. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -1
  41. package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  43. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +40 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +322 -4
  46. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +236 -6
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +452 -7
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
@@ -5,7 +5,7 @@
5
5
  // that only ever ran all four together could not tell a working check from an
6
6
  // always-true one riding on a sibling.
7
7
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
8
- import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
8
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, symlinkSync, } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  /** Records the mocked neo4j session returns. Empty for every test but the mail
@@ -17,7 +17,7 @@ vi.mock("../../lib/neo4j.js", () => ({
17
17
  close: vi.fn(async () => { }),
18
18
  }),
19
19
  }));
20
- import { runSchedulingGate, advanceGateMark, watermarkPath, readWatermarks, readWatermarksChecked, foldMailWatermarks, parseD1Count, digestBusy, slotKey, slotPosition, slotsIn, promoteSlot, UNFLAGGED_ID_CAP, } from "../scheduling-gate.js";
20
+ import { runSchedulingGate, advanceGateMark, watermarkPath, readWatermarks, readWatermarksChecked, foldMailWatermarks, parseD1Count, digestBusy, digestSweepState, slotKey, slotPosition, slotsIn, promoteSlot, UNFLAGGED_ID_CAP, } from "../scheduling-gate.js";
21
21
  const ACCOUNT = "20028fb5-0000-4000-8000-000000000000";
22
22
  const EVENT = "65ef758c-0000-4000-8000-000000000000";
23
23
  let platformRoot;
@@ -40,6 +40,17 @@ function quietSources(over = {}) {
40
40
  // Task 2684 — "quiet" for the unflagged arm is a label holding nothing this
41
41
  // routine has not already flagged.
42
42
  graphUnflagged: async () => ({ ids: [] }),
43
+ // Task 2688 — "quiet" for the connector arm is an account that registered
44
+ // no queue of the requested kind, which is what every install answers until
45
+ // an operator describes one.
46
+ connectorQueues: async () => ({ pluginPresent: true, bindings: [] }),
47
+ connectorQueueRead: async () => ({ status: 200, items: 0, truncated: false }),
48
+ // Task 2689 — "quiet" for the sweep arm is a sweep that ran just now with
49
+ // both review queues empty.
50
+ sweepState: async () => ({ newestReportAt: new Date().toISOString(), orphans: 0, citations: 0 }),
51
+ // Task 2689 — "quiet" for the filesystem arm is an account directory that
52
+ // has not grown since the stored measurement.
53
+ filesystemBytes: async () => ({ bytes: 0, capped: false }),
43
54
  ...over,
44
55
  };
45
56
  }
@@ -1040,7 +1051,7 @@ describe("unrecognised check type", () => {
1040
1051
  errored: [
1041
1052
  {
1042
1053
  kind: "nonsense",
1043
- detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph, graph-unflagged',
1054
+ detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph, graph-unflagged, connector, sweep, filesystem',
1044
1055
  },
1045
1056
  ],
1046
1057
  });
@@ -1064,7 +1075,7 @@ describe("unrecognised check type", () => {
1064
1075
  errored: [
1065
1076
  {
1066
1077
  kind: "nonsense",
1067
- detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph, graph-unflagged',
1078
+ detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph, graph-unflagged, connector, sweep, filesystem',
1068
1079
  },
1069
1080
  ],
1070
1081
  });
@@ -1549,4 +1560,311 @@ describe("REAL_SOURCES.graphUnflagged", () => {
1549
1560
  await expect(S.graphUnflagged(ACCOUNT, "Section", EVENT, 500)).rejects.toThrow(/expected a string/);
1550
1561
  });
1551
1562
  });
1563
+ // Task 2688 — the connector queue arm.
1564
+ //
1565
+ // The three quiet cases are asserted apart from the error cases on purpose:
1566
+ // this is the only check whose source can fail and still answer "no work", so a
1567
+ // suite that ran them together could not tell an unauthorised connector from an
1568
+ // unreachable one, which is exactly the distinction the arm exists to make.
1569
+ describe("connector check", () => {
1570
+ const CHECK = { type: "connector", scope: "operational" };
1571
+ const QUEUE = {
1572
+ kind: "operational",
1573
+ path: "/v2/jobs/changed",
1574
+ changedSinceParam: "updatedSince",
1575
+ itemsField: "items",
1576
+ };
1577
+ const BINDING = { name: "jobs", queue: QUEUE };
1578
+ const AT = "2026-08-14T06:00:00.000Z";
1579
+ const YESTERDAY = "2026-08-13T06:00:00.000Z";
1580
+ const bound = (over = {}) => quietSources({
1581
+ connectorQueues: async () => ({ pluginPresent: true, bindings: [BINDING] }),
1582
+ ...over,
1583
+ });
1584
+ beforeEach(() => {
1585
+ vi.useFakeTimers();
1586
+ vi.setSystemTime(new Date(AT));
1587
+ });
1588
+ afterEach(() => {
1589
+ vi.useRealTimers();
1590
+ });
1591
+ it("reports work and records the probe instant on a first run, without calling the queue", async () => {
1592
+ let called = 0;
1593
+ const res = await run([CHECK], bound({
1594
+ connectorQueueRead: async () => {
1595
+ called++;
1596
+ return { status: 200, items: 0, truncated: false };
1597
+ },
1598
+ }));
1599
+ // An unfiltered read of a queue that has never been filtered is the body
1600
+ // connector-call caps, and a capped body cannot be counted.
1601
+ expect(called).toBe(0);
1602
+ expect(res).toMatchObject({ hasWork: true, genuineWork: true, reasons: ["connector"] });
1603
+ expect(res.observed.connector).toEqual({ jobs: { changedSince: AT } });
1604
+ });
1605
+ it("reports work when the queue holds records above the stored position", async () => {
1606
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1607
+ const res = await run([CHECK], bound({ connectorQueueRead: async () => ({ status: 200, items: 3, truncated: false }) }));
1608
+ expect(res).toMatchObject({ hasWork: true, reasons: ["connector"], errored: [] });
1609
+ expect(res.observed.connector).toEqual({ jobs: { changedSince: AT } });
1610
+ });
1611
+ it("reports no work on an empty queue, and still records the position", async () => {
1612
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1613
+ const res = await run([CHECK], bound());
1614
+ expect(res).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1615
+ expect(res.observed.connector).toEqual({ jobs: { changedSince: AT } });
1616
+ });
1617
+ it("passes the stored position as the changed-since argument", async () => {
1618
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1619
+ const seen = [];
1620
+ await run([CHECK], bound({
1621
+ connectorQueueRead: async (_a, _b, changedSince) => {
1622
+ seen.push(changedSince);
1623
+ return { status: 200, items: 0, truncated: false };
1624
+ },
1625
+ }));
1626
+ expect(seen).toEqual([YESTERDAY]);
1627
+ });
1628
+ it("asks only for the queues whose kind matches the scope", async () => {
1629
+ const asked = [];
1630
+ await run([{ type: "connector", scope: "accounting" }], quietSources({
1631
+ connectorQueues: async (_a, kind) => {
1632
+ asked.push(kind);
1633
+ return { pluginPresent: true, bindings: [] };
1634
+ },
1635
+ }));
1636
+ expect(asked).toEqual(["accounting"]);
1637
+ });
1638
+ it("answers quiet when the connector plugin is not installed", async () => {
1639
+ const res = await run([CHECK], quietSources({ connectorQueues: async () => ({ pluginPresent: false, bindings: [] }) }));
1640
+ expect(res).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1641
+ expect(res.observed.connector).toBeUndefined();
1642
+ });
1643
+ it("answers quiet when no registered queue carries that kind", async () => {
1644
+ const res = await run([CHECK], quietSources());
1645
+ expect(res).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1646
+ expect(res.observed.connector).toBeUndefined();
1647
+ });
1648
+ it.each([401, 403])("answers quiet on %i and records no position", async (status) => {
1649
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1650
+ const res = await run([CHECK], bound({ connectorQueueRead: async () => ({ status, items: 0, truncated: false }) }));
1651
+ expect(res).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1652
+ // No position: an unauthorised connector was never read, so advancing past
1653
+ // it would skip whatever arrived while the credential was dead.
1654
+ expect(res.observed.connector).toBeUndefined();
1655
+ });
1656
+ it("counts any other non-2xx as work, so an outage still fires", async () => {
1657
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1658
+ const res = await run([CHECK], bound({ connectorQueueRead: async () => ({ status: 500, items: 0, truncated: false }) }));
1659
+ expect(res).toMatchObject({ hasWork: true, genuineWork: false, reasons: ["connector"] });
1660
+ expect(res.errored[0]).toMatchObject({ kind: "connector", source: "connector:operational" });
1661
+ expect(res.observed.connector).toBeUndefined();
1662
+ });
1663
+ it("treats a truncated body as work AND records the position, so the window narrows", async () => {
1664
+ // A body the connector capped cannot be counted, but it is proof the queue
1665
+ // moved. Recording nothing would leave the same window on the next fire,
1666
+ // which would truncate again: the routine would then wake the model on
1667
+ // every fire for ever, with no way back to quiet.
1668
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1669
+ const res = await run([CHECK], bound({ connectorQueueRead: async () => ({ status: 200, items: 0, truncated: true }) }));
1670
+ expect(res).toMatchObject({ hasWork: true, genuineWork: true, reasons: ["connector"], errored: [] });
1671
+ expect(res.observed.connector).toEqual({ jobs: { changedSince: AT } });
1672
+ });
1673
+ it("counts a transport failure as work", async () => {
1674
+ seedWatermarks({ connector: { jobs: { changedSince: YESTERDAY } } });
1675
+ const res = await run([CHECK], bound({
1676
+ connectorQueueRead: async () => {
1677
+ throw new Error("ETIMEDOUT");
1678
+ },
1679
+ }));
1680
+ expect(res).toMatchObject({ hasWork: true, genuineWork: false });
1681
+ expect(res.errored).toEqual([
1682
+ { kind: "connector", detail: "ETIMEDOUT", source: "connector:operational" },
1683
+ ]);
1684
+ });
1685
+ it("holds one position per connector under a single check", async () => {
1686
+ seedWatermarks({
1687
+ connector: {
1688
+ jobs: { changedSince: YESTERDAY },
1689
+ crm: { changedSince: "2026-08-12T06:00:00.000Z" },
1690
+ },
1691
+ });
1692
+ const res = await run([CHECK], quietSources({
1693
+ connectorQueues: async () => ({
1694
+ pluginPresent: true,
1695
+ bindings: [BINDING, { name: "crm", queue: QUEUE }],
1696
+ }),
1697
+ connectorQueueRead: async (_a, binding) => ({
1698
+ status: 200,
1699
+ items: binding.name === "crm" ? 2 : 0,
1700
+ truncated: false,
1701
+ }),
1702
+ }));
1703
+ expect(res).toMatchObject({ hasWork: true, reasons: ["connector"] });
1704
+ expect(res.observed.connector).toEqual({
1705
+ jobs: { changedSince: AT },
1706
+ crm: { changedSince: AT },
1707
+ });
1708
+ });
1709
+ it("names each connector slot in the watermark helpers", () => {
1710
+ const marks = { connector: { jobs: { changedSince: AT } } };
1711
+ expect(slotsIn(marks)).toEqual([{ kind: "connector", key: "jobs" }]);
1712
+ expect(slotKey({ kind: "connector", key: "jobs" })).toBe("connector:jobs");
1713
+ expect(slotPosition({ kind: "connector", key: "jobs" }, marks)).toBe(AT);
1714
+ const into = {};
1715
+ expect(promoteSlot({ kind: "connector", key: "jobs" }, marks, into)).toBe(true);
1716
+ expect(into.connector).toEqual({ jobs: { changedSince: AT } });
1717
+ });
1718
+ });
1719
+ // Task 2689 — the health-delta checks.
1720
+ describe("sweep check", () => {
1721
+ const CHECK = { type: "sweep" };
1722
+ const FRESH = { newestReportAt: new Date().toISOString(), orphans: 2, citations: 1 };
1723
+ it("is false when the digest matches the stored one", async () => {
1724
+ seedWatermarks({ sweep: { digest: digestSweepState(FRESH, Date.now()) } });
1725
+ const sources = quietSources({ sweepState: async () => FRESH });
1726
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1727
+ });
1728
+ it("is true when a review queue moved", async () => {
1729
+ seedWatermarks({ sweep: { digest: digestSweepState(FRESH, Date.now()) } });
1730
+ const sources = quietSources({ sweepState: async () => ({ ...FRESH, orphans: 3 }) });
1731
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["sweep"] });
1732
+ });
1733
+ it("is true on a first run, when no digest is stored", async () => {
1734
+ const sources = quietSources({ sweepState: async () => FRESH });
1735
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["sweep"] });
1736
+ });
1737
+ it("reports work rather than an error when the source is unreachable", async () => {
1738
+ const sources = quietSources({
1739
+ sweepState: async () => {
1740
+ throw new Error("neo4j unreachable");
1741
+ },
1742
+ });
1743
+ expect(await run([CHECK], sources)).toMatchObject({
1744
+ hasWork: true,
1745
+ reasons: ["sweep"],
1746
+ errored: [{ kind: "sweep", source: "sweep", detail: "neo4j unreachable" }],
1747
+ });
1748
+ });
1749
+ it("changes once when the sweep goes stale, then settles", () => {
1750
+ const now = Date.parse("2026-08-14T12:00:00.000Z");
1751
+ const state = { newestReportAt: "2026-08-14T11:00:00.000Z", orphans: 0, citations: 0 };
1752
+ const healthy = digestSweepState(state, now);
1753
+ const stale = digestSweepState(state, now + 27 * 60 * 60 * 1000);
1754
+ expect(stale).not.toBe(healthy);
1755
+ expect(digestSweepState(state, now + 30 * 60 * 60 * 1000)).toBe(stale);
1756
+ });
1757
+ it("treats a sweep that has never run as stale", () => {
1758
+ const now = Date.parse("2026-08-14T12:00:00.000Z");
1759
+ const absent = { newestReportAt: null, orphans: 0, citations: 0 };
1760
+ const old = { newestReportAt: "2026-08-12T12:00:00.000Z", orphans: 0, citations: 0 };
1761
+ expect(digestSweepState(absent, now)).toBe(digestSweepState(old, now));
1762
+ });
1763
+ it("does not move on the sweep merely running again", () => {
1764
+ const now = Date.parse("2026-08-14T12:00:00.000Z");
1765
+ const before = { newestReportAt: "2026-08-14T10:00:00.000Z", orphans: 4, citations: 2 };
1766
+ const after = { newestReportAt: "2026-08-14T11:00:00.000Z", orphans: 4, citations: 2 };
1767
+ expect(digestSweepState(after, now)).toBe(digestSweepState(before, now));
1768
+ });
1769
+ it("records its digest as a promotable slot", async () => {
1770
+ const sources = quietSources({ sweepState: async () => FRESH });
1771
+ const res = await run([CHECK], sources);
1772
+ expect(slotsIn(res.observed)).toEqual([{ kind: "sweep" }]);
1773
+ const into = {};
1774
+ expect(promoteSlot({ kind: "sweep" }, res.observed, into)).toBe(true);
1775
+ expect(into.sweep?.digest).toBe(res.observed.sweep?.digest);
1776
+ expect(slotKey({ kind: "sweep" })).toBe("sweep");
1777
+ expect(slotPosition({ kind: "sweep" }, into)).toBe(res.observed.sweep.digest.slice(0, 8));
1778
+ });
1779
+ });
1780
+ describe("filesystem check", () => {
1781
+ const CHECK = { type: "filesystem", minGrowthBytes: 1_000 };
1782
+ it("is false when growth is below the threshold", async () => {
1783
+ seedWatermarks({ filesystem: { bytes: 10_000 } });
1784
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 10_999, capped: false }) });
1785
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: false, reasons: [], errored: [] });
1786
+ });
1787
+ it("is true when growth reaches the threshold", async () => {
1788
+ seedWatermarks({ filesystem: { bytes: 10_000 } });
1789
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 11_000, capped: false }) });
1790
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["filesystem"] });
1791
+ });
1792
+ it("is false when the directory shrank", async () => {
1793
+ seedWatermarks({ filesystem: { bytes: 10_000 } });
1794
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 4_000, capped: false }) });
1795
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: false, reasons: [] });
1796
+ });
1797
+ it("is true on a first run, when no size is stored", async () => {
1798
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 1, capped: false }) });
1799
+ expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["filesystem"] });
1800
+ });
1801
+ it("reports work and records no position when the walk hit its cap", async () => {
1802
+ seedWatermarks({ filesystem: { bytes: 10_000 } });
1803
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 10_001, capped: true }) });
1804
+ const res = await run([CHECK], sources);
1805
+ expect(res).toMatchObject({ hasWork: true, reasons: ["filesystem"] });
1806
+ expect(res.observed.filesystem).toBeUndefined();
1807
+ });
1808
+ it("refuses a non-positive threshold rather than passing on every fire", async () => {
1809
+ const res = await run([{ type: "filesystem", minGrowthBytes: 0 }], quietSources());
1810
+ expect(res).toMatchObject({ hasWork: true, errored: [{ kind: "filesystem" }] });
1811
+ });
1812
+ it("reports work rather than an error when the source is unreachable", async () => {
1813
+ const sources = quietSources({
1814
+ filesystemBytes: async () => {
1815
+ throw new Error("ENOENT: no account directory");
1816
+ },
1817
+ });
1818
+ expect(await run([CHECK], sources)).toMatchObject({
1819
+ hasWork: true,
1820
+ reasons: ["filesystem"],
1821
+ errored: [{ kind: "filesystem", source: "filesystem" }],
1822
+ });
1823
+ });
1824
+ it("records its size as a promotable slot", async () => {
1825
+ const sources = quietSources({ filesystemBytes: async () => ({ bytes: 77, capped: false }) });
1826
+ const res = await run([CHECK], sources);
1827
+ expect(slotsIn(res.observed)).toEqual([{ kind: "filesystem" }]);
1828
+ const into = {};
1829
+ expect(promoteSlot({ kind: "filesystem" }, res.observed, into)).toBe(true);
1830
+ expect(into.filesystem?.bytes).toBe(77);
1831
+ expect(slotKey({ kind: "filesystem" })).toBe("filesystem");
1832
+ expect(slotPosition({ kind: "filesystem" }, into)).toBe("77");
1833
+ });
1834
+ });
1835
+ describe("measureAccountBytes", () => {
1836
+ it("sums nested regular files and never follows a symlink", async () => {
1837
+ const { measureAccountBytes } = await import("../scheduling-gate.js");
1838
+ const root = mkdtempSync(join(tmpdir(), "fs-measure-"));
1839
+ mkdirSync(join(root, "nested"), { recursive: true });
1840
+ writeFileSync(join(root, "a.txt"), "12345");
1841
+ writeFileSync(join(root, "nested", "b.txt"), "678");
1842
+ const outside = mkdtempSync(join(tmpdir(), "fs-outside-"));
1843
+ writeFileSync(join(outside, "big.txt"), "x".repeat(5000));
1844
+ symlinkSync(outside, join(root, "link"));
1845
+ expect(measureAccountBytes(root, 1000)).toEqual({ bytes: 8, capped: false });
1846
+ });
1847
+ it("stops at the cap and says so", async () => {
1848
+ const { measureAccountBytes } = await import("../scheduling-gate.js");
1849
+ const root = mkdtempSync(join(tmpdir(), "fs-cap-"));
1850
+ for (let i = 0; i < 5; i++)
1851
+ writeFileSync(join(root, `f${i}.txt`), "ab");
1852
+ expect(measureAccountBytes(root, 3)).toMatchObject({ capped: true });
1853
+ });
1854
+ it("throws on a missing root rather than reading zero", async () => {
1855
+ const { measureAccountBytes } = await import("../scheduling-gate.js");
1856
+ expect(() => measureAccountBytes(join(tmpdir(), "fs-absent-000"), 10)).toThrow();
1857
+ });
1858
+ });
1859
+ describe("measureAccountBytes cap covers directories too", () => {
1860
+ it("stops on a tree that is wide in directories and holds no file", async () => {
1861
+ const { measureAccountBytes } = await import("../scheduling-gate.js");
1862
+ const root = mkdtempSync(join(tmpdir(), "fs-dirs-"));
1863
+ for (let i = 0; i < 6; i++)
1864
+ mkdirSync(join(root, `d${i}`));
1865
+ // Files alone would never reach a cap here, so a file-only counter would
1866
+ // walk the whole tree however wide it got.
1867
+ expect(measureAccountBytes(root, 4)).toMatchObject({ capped: true });
1868
+ });
1869
+ });
1552
1870
  //# sourceMappingURL=scheduling-gate.test.js.map