@rubytech/create-maxy-code 0.1.599 → 0.1.601
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/__tests__/email-sampler-unit.test.js +31 -0
- package/dist/index.js +18 -2
- package/dist/port-resolution.js +24 -0
- package/package.json +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts +8 -2
- package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.js +10 -3
- 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 +1 -1
- package/payload/platform/lib/routine-templates/src/roster.ts +20 -4
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
- package/payload/platform/plugins/email/PLUGIN.md +7 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts +27 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +78 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js +96 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js +75 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts +48 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js +77 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +81 -3
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +29 -2
- 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 +67 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/scripts/logs-read.sh +18 -3
- package/payload/server/public/operator.html +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-mailbox.test.js","sourceRoot":"","sources":["../../../src/sampler/__tests__/sample-mailbox.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/sampler/main.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { appendFileSync, statSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { fetchSinceUid, resolveDefaultRecipients } from "../lib/imap.js";
|
|
4
|
+
import { dispatchEmailMessagesToConversationArchive } from "../lib/conversation-archive-dispatch.js";
|
|
5
|
+
import { listAllImapAccounts, readSamplerCursor, advanceSamplerCursor } from "../lib/credentials.js";
|
|
6
|
+
import { sampleMailbox } from "./sample-mailbox.js";
|
|
7
|
+
// The standing email-sampler (Task 2682): a per-brand systemd user unit that
|
|
8
|
+
// each cycle enumerates every IMAP mailbox on the install and writes new
|
|
9
|
+
// inbound messages to the graph via the plugin's own dispatch, independent of
|
|
10
|
+
// any agent turn. The cursor is the only state; it advances only on a
|
|
11
|
+
// successful write, so the loop is restart-safe by construction.
|
|
12
|
+
const LOG_PATH = process.env.EMAIL_SAMPLER_LOG_PATH ?? "/tmp/email-sampler.log";
|
|
13
|
+
const INTERVAL_MS = (Number(process.env.EMAIL_SAMPLER_INTERVAL_SEC ?? "60") || 60) * 1000;
|
|
14
|
+
const LOG_MAX = Number(process.env.EMAIL_SAMPLER_LOG_MAX_BYTES ?? "10485760") || 10485760;
|
|
15
|
+
/** Append one structured line, capping the file at LOG_MAX by keeping the tail
|
|
16
|
+
* (mirrors rss-sampler's self-cap). Logging must never kill the loop. */
|
|
17
|
+
function logLine(line) {
|
|
18
|
+
const stamped = `${new Date().toISOString()} [email-sampler] ${line}\n`;
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(dirname(LOG_PATH), { recursive: true });
|
|
21
|
+
appendFileSync(LOG_PATH, stamped);
|
|
22
|
+
if (statSync(LOG_PATH).size > LOG_MAX) {
|
|
23
|
+
const buf = readFileSync(LOG_PATH);
|
|
24
|
+
// Keep roughly the last half, but start at the next line boundary so the
|
|
25
|
+
// first retained line is whole, not a mid-line fragment a tail reader
|
|
26
|
+
// would show as garbage.
|
|
27
|
+
const approx = buf.length - Math.floor(LOG_MAX / 2);
|
|
28
|
+
const nl = buf.indexOf(0x0a, approx);
|
|
29
|
+
const start = nl === -1 ? approx : nl + 1;
|
|
30
|
+
writeFileSync(LOG_PATH, buf.subarray(start));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* a logging failure is never fatal to ingest */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const deps = {
|
|
38
|
+
fetchSince: fetchSinceUid,
|
|
39
|
+
write: (m) => dispatchEmailMessagesToConversationArchive(m),
|
|
40
|
+
readCursor: readSamplerCursor,
|
|
41
|
+
advanceCursor: advanceSamplerCursor,
|
|
42
|
+
recipients: (c) => resolveDefaultRecipients(c),
|
|
43
|
+
log: logLine,
|
|
44
|
+
};
|
|
45
|
+
async function cycle() {
|
|
46
|
+
let accounts;
|
|
47
|
+
try {
|
|
48
|
+
accounts = await listAllImapAccounts();
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
logLine(`op=sample-error mailbox=<enumeration> detail=${e instanceof Error ? e.message : String(e)}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const account of accounts) {
|
|
55
|
+
try {
|
|
56
|
+
await sampleMailbox(deps, account);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
logLine(`op=sample-error mailbox=${account.config.email} detail=${e instanceof Error ? e.message : String(e)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function main() {
|
|
64
|
+
logLine(`boot interval_sec=${INTERVAL_MS / 1000} log=${LOG_PATH}`);
|
|
65
|
+
// eslint-disable-next-line no-constant-condition
|
|
66
|
+
while (true) {
|
|
67
|
+
await cycle();
|
|
68
|
+
await new Promise((r) => setTimeout(r, INTERVAL_MS));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
main().catch((e) => {
|
|
72
|
+
logLine(`op=sample-error mailbox=<fatal> detail=${e instanceof Error ? e.message : String(e)}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/sampler/main.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EmailConfig, SamplerCursor } from "../lib/credentials.js";
|
|
2
|
+
import type { FetchedEmail } from "../lib/imap.js";
|
|
3
|
+
import type { EmailData } from "../lib/graph.js";
|
|
4
|
+
import type { DispatchResult } from "../lib/conversation-archive-dispatch.js";
|
|
5
|
+
/**
|
|
6
|
+
* The email-sampler's single ingest step, isolated as a pure function with
|
|
7
|
+
* injected fetch / writer / cursor so the loop and the systemd unit stay thin
|
|
8
|
+
* and every branch is testable without IMAP, Neo4j, or a clock (Task 2682).
|
|
9
|
+
*/
|
|
10
|
+
export interface SamplerDeps {
|
|
11
|
+
fetchSince: (config: EmailConfig, password: string, sinceUid: number, recipients: string[] | undefined) => Promise<{
|
|
12
|
+
uidValidity: number;
|
|
13
|
+
emails: FetchedEmail[];
|
|
14
|
+
maxUid: number;
|
|
15
|
+
}>;
|
|
16
|
+
write: (messages: EmailData[]) => Promise<DispatchResult>;
|
|
17
|
+
readCursor: (accountId: string, email: string) => Promise<SamplerCursor | null>;
|
|
18
|
+
advanceCursor: (accountId: string, email: string, cursorUid: number, uidValidity: number) => Promise<void>;
|
|
19
|
+
recipients: (config: EmailConfig) => string[] | undefined;
|
|
20
|
+
log: (line: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface SamplerAccount {
|
|
23
|
+
accountId: string;
|
|
24
|
+
config: EmailConfig;
|
|
25
|
+
password: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SampleResult {
|
|
28
|
+
mailbox: string;
|
|
29
|
+
fetched: number;
|
|
30
|
+
cursor: number;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sample one mailbox: fetch inbound UIDs above the sampler's own cursor, write
|
|
35
|
+
* them to the graph, and advance the cursor — but only after a successful write.
|
|
36
|
+
*
|
|
37
|
+
* Cases, in order:
|
|
38
|
+
* - fetch throws → log op=sample-error, cursor unchanged, retried next cycle.
|
|
39
|
+
* - the mailbox's live uidValidity differs from the stored one → the stored UID
|
|
40
|
+
* is void (the server renumbered), so reset the cursor to 0 for the new
|
|
41
|
+
* uidValidity and let the next cycle backfill cleanly. This cycle writes
|
|
42
|
+
* nothing, because `UID > oldCursor` against a new sequence is unreliable.
|
|
43
|
+
* - no messages above the cursor → no-op, cursor unchanged.
|
|
44
|
+
* - write throws → log op=sample-error, cursor unchanged, retried next cycle.
|
|
45
|
+
* - otherwise → advance the cursor to the fetched maxUid.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sampleMailbox(deps: SamplerDeps, account: SamplerAccount): Promise<SampleResult>;
|
|
48
|
+
//# sourceMappingURL=sample-mailbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-mailbox.d.ts","sourceRoot":"","sources":["../../src/sampler/sample-mailbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAE9E;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,CACV,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,KAC7B,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1D,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAChF,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3G,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;IAC1D,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgCD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAoCrG"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function msg(e) {
|
|
2
|
+
return e instanceof Error ? e.message : String(e);
|
|
3
|
+
}
|
|
4
|
+
/** Map a fetched IMAP message to the archive-writer's record shape. The mapping
|
|
5
|
+
* matches what email-ingest builds, EXCEPT the screening label: email-ingest
|
|
6
|
+
* stamps "clean" because an operator approved the message, whereas the sampler
|
|
7
|
+
* runs no screen and has no operator, so it stamps "unscreened" — the honest
|
|
8
|
+
* value a later gate needs to tell auto-sampled mail from operator-vetted mail. */
|
|
9
|
+
function toEmailData(e, accountId, config, uidValidity) {
|
|
10
|
+
return {
|
|
11
|
+
messageId: e.messageId,
|
|
12
|
+
inReplyTo: e.inReplyTo,
|
|
13
|
+
toAddresses: e.toAddresses,
|
|
14
|
+
ccAddresses: e.ccAddresses,
|
|
15
|
+
uid: e.uid,
|
|
16
|
+
uidValidity,
|
|
17
|
+
mailbox: "INBOX",
|
|
18
|
+
recipientAddress: config.email,
|
|
19
|
+
fromAddress: e.fromAddress,
|
|
20
|
+
fromName: e.fromName,
|
|
21
|
+
subject: e.subject,
|
|
22
|
+
bodyPreview: e.bodyPreview,
|
|
23
|
+
receivedAt: e.date,
|
|
24
|
+
accountId,
|
|
25
|
+
screening: "unscreened",
|
|
26
|
+
screeningReason: "sampler-ingest",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sample one mailbox: fetch inbound UIDs above the sampler's own cursor, write
|
|
31
|
+
* them to the graph, and advance the cursor — but only after a successful write.
|
|
32
|
+
*
|
|
33
|
+
* Cases, in order:
|
|
34
|
+
* - fetch throws → log op=sample-error, cursor unchanged, retried next cycle.
|
|
35
|
+
* - the mailbox's live uidValidity differs from the stored one → the stored UID
|
|
36
|
+
* is void (the server renumbered), so reset the cursor to 0 for the new
|
|
37
|
+
* uidValidity and let the next cycle backfill cleanly. This cycle writes
|
|
38
|
+
* nothing, because `UID > oldCursor` against a new sequence is unreliable.
|
|
39
|
+
* - no messages above the cursor → no-op, cursor unchanged.
|
|
40
|
+
* - write throws → log op=sample-error, cursor unchanged, retried next cycle.
|
|
41
|
+
* - otherwise → advance the cursor to the fetched maxUid.
|
|
42
|
+
*/
|
|
43
|
+
export async function sampleMailbox(deps, account) {
|
|
44
|
+
const { accountId, config, password } = account;
|
|
45
|
+
const box = config.email;
|
|
46
|
+
const stored = await deps.readCursor(accountId, box);
|
|
47
|
+
const sinceUid = stored?.cursorUid ?? 0;
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await deps.fetchSince(config, password, sinceUid, deps.recipients(config));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
deps.log(`op=sample-error mailbox=${box} detail=${msg(e)}`);
|
|
54
|
+
return { mailbox: box, fetched: 0, cursor: sinceUid, error: msg(e) };
|
|
55
|
+
}
|
|
56
|
+
if (stored?.uidValidity != null && stored.uidValidity !== result.uidValidity) {
|
|
57
|
+
await deps.advanceCursor(accountId, box, 0, result.uidValidity);
|
|
58
|
+
deps.log(`op=sample mailbox=${box} fetched=0 cursor=0 note=uidvalidity-reset`);
|
|
59
|
+
return { mailbox: box, fetched: 0, cursor: 0 };
|
|
60
|
+
}
|
|
61
|
+
if (result.emails.length === 0) {
|
|
62
|
+
deps.log(`op=sample mailbox=${box} fetched=0 cursor=${sinceUid}`);
|
|
63
|
+
return { mailbox: box, fetched: 0, cursor: sinceUid };
|
|
64
|
+
}
|
|
65
|
+
const messages = result.emails.map((e) => toEmailData(e, accountId, config, result.uidValidity));
|
|
66
|
+
try {
|
|
67
|
+
await deps.write(messages);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
deps.log(`op=sample-error mailbox=${box} detail=${msg(e)}`);
|
|
71
|
+
return { mailbox: box, fetched: messages.length, cursor: sinceUid, error: msg(e) };
|
|
72
|
+
}
|
|
73
|
+
await deps.advanceCursor(accountId, box, result.maxUid, result.uidValidity);
|
|
74
|
+
deps.log(`op=sample mailbox=${box} fetched=${messages.length} cursor=${result.maxUid}`);
|
|
75
|
+
return { mailbox: box, fetched: messages.length, cursor: result.maxUid };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=sample-mailbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-mailbox.js","sourceRoot":"","sources":["../../src/sampler/sample-mailbox.ts"],"names":[],"mappings":"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"}
|
package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js
CHANGED
|
@@ -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, } from "../scheduling-gate.js";
|
|
20
|
+
import { runSchedulingGate, advanceGateMark, watermarkPath, readWatermarks, readWatermarksChecked, foldMailWatermarks, parseD1Count, digestBusy, slotKey, slotPosition, slotsIn, promoteSlot, } 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;
|
|
@@ -34,6 +34,9 @@ function quietSources(over = {}) {
|
|
|
34
34
|
telegramInbound: async () => ({ newestInboundAt: null }),
|
|
35
35
|
// Task 2657 — "quiet" for the Outlook arm is a folder holding no message.
|
|
36
36
|
mailOutlook: async () => ({ newestReceivedAt: null }),
|
|
37
|
+
// Task 2681 — "quiet" for the graph arm is an account whose graph holds no
|
|
38
|
+
// appointment activity at all.
|
|
39
|
+
graphActivity: async () => ({ newestActivityAt: null }),
|
|
37
40
|
...over,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
@@ -959,7 +962,7 @@ describe("unrecognised check type", () => {
|
|
|
959
962
|
errored: [
|
|
960
963
|
{
|
|
961
964
|
kind: "nonsense",
|
|
962
|
-
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram',
|
|
965
|
+
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph',
|
|
963
966
|
},
|
|
964
967
|
],
|
|
965
968
|
});
|
|
@@ -983,7 +986,7 @@ describe("unrecognised check type", () => {
|
|
|
983
986
|
errored: [
|
|
984
987
|
{
|
|
985
988
|
kind: "nonsense",
|
|
986
|
-
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram',
|
|
989
|
+
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram, graph',
|
|
987
990
|
},
|
|
988
991
|
],
|
|
989
992
|
});
|
|
@@ -1282,4 +1285,79 @@ describe("observed positions (Task 2663)", () => {
|
|
|
1282
1285
|
expect(result.observed).toEqual({});
|
|
1283
1286
|
});
|
|
1284
1287
|
});
|
|
1288
|
+
// Task 2681 — the graph activity check: appointment activity in the account's
|
|
1289
|
+
// own graph, newest timestamp against a stored watermark, mirroring telegram.
|
|
1290
|
+
describe("graph check", () => {
|
|
1291
|
+
const GRAPH = { type: "graph", scope: "appointments" };
|
|
1292
|
+
const graphAt = (iso) => ({
|
|
1293
|
+
graphActivity: async () => ({ newestActivityAt: iso }),
|
|
1294
|
+
});
|
|
1295
|
+
it("is false on an empty graph (null newest), and records no position", async () => {
|
|
1296
|
+
expect(await run([GRAPH], quietSources(graphAt(null)))).toMatchObject({
|
|
1297
|
+
hasWork: false,
|
|
1298
|
+
reasons: [],
|
|
1299
|
+
errored: [],
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
it("is true when activity is newer than the watermark", async () => {
|
|
1303
|
+
seedWatermarks({ graph: { updatedAt: "2026-08-13T06:00:00.000Z" } });
|
|
1304
|
+
expect(await run([GRAPH], quietSources(graphAt("2026-08-13T07:00:00.000Z")))).toMatchObject({
|
|
1305
|
+
hasWork: true,
|
|
1306
|
+
reasons: ["graph"],
|
|
1307
|
+
errored: [],
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
it("is false when activity equals the watermark (>, never >=)", async () => {
|
|
1311
|
+
seedWatermarks({ graph: { updatedAt: "2026-08-13T07:00:00.000Z" } });
|
|
1312
|
+
expect(await run([GRAPH], quietSources(graphAt("2026-08-13T07:00:00.000Z")))).toMatchObject({
|
|
1313
|
+
hasWork: false,
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
it("is true on a first run (no watermark) when the graph has any activity", async () => {
|
|
1317
|
+
expect(await run([GRAPH], quietSources(graphAt("2026-08-13T07:00:00.000Z")))).toMatchObject({
|
|
1318
|
+
hasWork: true,
|
|
1319
|
+
reasons: ["graph"],
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
it("fails open when the graph source throws, never a silent mute", async () => {
|
|
1323
|
+
const result = await run([GRAPH], quietSources({ graphActivity: async () => { throw new Error("neo4j down"); } }));
|
|
1324
|
+
expect(result.hasWork).toBe(true);
|
|
1325
|
+
expect(result.errored.map((e) => e.kind)).toContain("graph");
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
describe("graph slot plumbing", () => {
|
|
1329
|
+
it("round-trips through slotKey, slotPosition, slotsIn and promoteSlot", () => {
|
|
1330
|
+
const marks = { graph: { updatedAt: "2026-08-13T07:00:00.000Z" } };
|
|
1331
|
+
expect(slotKey({ kind: "graph" })).toBe("graph");
|
|
1332
|
+
expect(slotPosition({ kind: "graph" }, marks)).toBe("2026-08-13T07:00:00.000Z");
|
|
1333
|
+
expect(slotsIn(marks)).toContainEqual({ kind: "graph" });
|
|
1334
|
+
const into = {};
|
|
1335
|
+
expect(promoteSlot({ kind: "graph" }, marks, into)).toBe(true);
|
|
1336
|
+
expect(into.graph?.updatedAt).toBe("2026-08-13T07:00:00.000Z");
|
|
1337
|
+
});
|
|
1338
|
+
it("promoteSlot returns false when the source holds no graph position", () => {
|
|
1339
|
+
expect(promoteSlot({ kind: "graph" }, {}, {})).toBe(false);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
describe("REAL_SOURCES.graphActivity", () => {
|
|
1343
|
+
it("returns the newest ISO string the graph holds", async () => {
|
|
1344
|
+
neo4jRecords = [{ get: (k) => (k === "newest" ? "2026-08-13T07:00:00.000Z" : null) }];
|
|
1345
|
+
const { REAL_SOURCES: S } = await import("../scheduling-gate.js");
|
|
1346
|
+
expect(await S.graphActivity(ACCOUNT, "appointments")).toEqual({
|
|
1347
|
+
newestActivityAt: "2026-08-13T07:00:00.000Z",
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
it("returns null when the graph holds no such node (max over no rows)", async () => {
|
|
1351
|
+
neo4jRecords = [{ get: () => null }];
|
|
1352
|
+
const { REAL_SOURCES: S } = await import("../scheduling-gate.js");
|
|
1353
|
+
expect(await S.graphActivity(ACCOUNT, "appointments")).toEqual({ newestActivityAt: null });
|
|
1354
|
+
});
|
|
1355
|
+
it("throws on a non-string timestamp, so the probe fails open rather than muting", async () => {
|
|
1356
|
+
// A :Meeting written by a path storing a neo4j datetime/Integer, not an ISO
|
|
1357
|
+
// string. Coercing to null would answer "quiet" about a graph never read.
|
|
1358
|
+
neo4jRecords = [{ get: (k) => (k === "newest" ? 1723531200000 : null) }];
|
|
1359
|
+
const { REAL_SOURCES: S } = await import("../scheduling-gate.js");
|
|
1360
|
+
await expect(S.graphActivity(ACCOUNT, "appointments")).rejects.toThrow(/expected an ISO string/);
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1285
1363
|
//# sourceMappingURL=scheduling-gate.test.js.map
|