@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.
- package/package.json +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts +30 -2
- package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.js +76 -32
- package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
- package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +0 -0
- package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +22 -1
- package/payload/platform/lib/routine-templates/src/roster.ts +108 -35
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +5 -0
- package/payload/platform/plugins/connector/PLUGIN.md +9 -1
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts +2 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts.map +1 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js +67 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js.map +1 -0
- package/payload/platform/plugins/connector/mcp/dist/index.js +36 -5
- package/payload/platform/plugins/connector/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts +35 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/store.js +33 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/store.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/package.json +4 -2
- package/payload/platform/plugins/connector/mcp/vitest.config.ts +8 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
- package/payload/platform/plugins/email/PLUGIN.md +4 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js +233 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js +19 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts +3 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js +146 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts +150 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.js +199 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.js.map +1 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +40 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +322 -4
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +236 -6
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +452 -7
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js
CHANGED
|
@@ -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
|