@pome-sh/checks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +88 -0
- package/dist/_types/sdk/check-discrimination.d.ts +7 -0
- package/dist/_types/sdk/check-state-path.d.ts +71 -0
- package/dist/_types/sdk/checks.d.ts +93 -0
- package/dist/_types/sdk/db.d.ts +44 -0
- package/dist/_types/sdk/failure-injection-rules.d.ts +32 -0
- package/dist/_types/twin-github/src/check-kind.d.ts +3 -0
- package/dist/_types/twin-github/src/check-state.d.ts +77 -0
- package/dist/_types/twin-github/src/checks.d.ts +50 -0
- package/dist/_types/twin-github/src/seed.d.ts +92 -0
- package/dist/_types/twin-github/src/types.d.ts +244 -0
- package/dist/_types/twin-gmail/src/check-kind.d.ts +3 -0
- package/dist/_types/twin-gmail/src/check-state.d.ts +138 -0
- package/dist/_types/twin-gmail/src/checks.d.ts +19 -0
- package/dist/_types/twin-gmail/src/faults.d.ts +21 -0
- package/dist/_types/twin-gmail/src/seed.d.ts +247 -0
- package/dist/_types/twin-gmail/src/types.d.ts +150 -0
- package/dist/_types/twin-linear/src/check-kind.d.ts +3 -0
- package/dist/_types/twin-linear/src/check-state.d.ts +123 -0
- package/dist/_types/twin-linear/src/checks.d.ts +29 -0
- package/dist/_types/twin-linear/src/seed.d.ts +166 -0
- package/dist/_types/twin-linear/src/types.d.ts +366 -0
- package/dist/_types/twin-slack/src/check-kind.d.ts +3 -0
- package/dist/_types/twin-slack/src/check-state.d.ts +107 -0
- package/dist/_types/twin-slack/src/checks.d.ts +14 -0
- package/dist/_types/twin-slack/src/seed.d.ts +62 -0
- package/dist/_types/twin-slack/src/types.d.ts +182 -0
- package/dist/_types/twin-stripe/src/check-kind.d.ts +3 -0
- package/dist/_types/twin-stripe/src/check-state.d.ts +107 -0
- package/dist/_types/twin-stripe/src/checks.d.ts +22 -0
- package/dist/_types/twin-stripe/src/seed.d.ts +213 -0
- package/dist/_types/twin-stripe/src/types.d.ts +252 -0
- package/dist/_types/wire/index.d.ts +23 -0
- package/dist/_types/wire/otel/event-schema.d.ts +424 -0
- package/dist/_types/wire/otel/index.d.ts +25 -0
- package/dist/_types/wire/otel/legacy-shim.d.ts +105 -0
- package/dist/_types/wire/otel/map-span.d.ts +64 -0
- package/dist/_types/wire/otel/nano.d.ts +26 -0
- package/dist/_types/wire/otel/project.d.ts +42 -0
- package/dist/_types/wire/otel/semconv.d.ts +57 -0
- package/dist/_types/wire/otel/span-event.d.ts +198 -0
- package/dist/_types/wire/recorder-events.d.ts +526 -0
- package/dist/_types/wire/redaction.d.ts +2 -0
- package/dist/chunk-4WXX5VPA.js +238 -0
- package/dist/chunk-5SJ4PVO5.js +981 -0
- package/dist/chunk-GYFGMULG.js +1138 -0
- package/dist/chunk-IVENH4KX.js +814 -0
- package/dist/chunk-NORTPYDQ.js +1015 -0
- package/dist/chunk-SJ6SVRAA.js +432 -0
- package/dist/chunk-ZXE6LAM3.js +1 -0
- package/dist/dsl.d.ts +11 -0
- package/dist/dsl.js +2 -0
- package/dist/github.d.ts +2 -0
- package/dist/github.js +2 -0
- package/dist/gmail.d.ts +2 -0
- package/dist/gmail.js +2 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +24 -0
- package/dist/linear.d.ts +2 -0
- package/dist/linear.js +2 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/stripe.d.ts +2 -0
- package/dist/stripe.js +2 -0
- package/package.json +93 -0
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { statePath, defineCheck, VACUITY_SENTINEL, childStatePath, VACUITY_SENTINEL_NUMBER } from './chunk-4WXX5VPA.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// ../twin-gmail/dist/src/check-params.js
|
|
5
|
+
var messageId = {
|
|
6
|
+
name: "message",
|
|
7
|
+
pattern: "[A-Za-z0-9_-]{1,128}",
|
|
8
|
+
example: "msg_support",
|
|
9
|
+
render: (value) => value,
|
|
10
|
+
parse: (raw) => raw
|
|
11
|
+
};
|
|
12
|
+
var labelRef = {
|
|
13
|
+
name: "label",
|
|
14
|
+
pattern: "[A-Za-z0-9_-]{1,128}",
|
|
15
|
+
example: "STARRED",
|
|
16
|
+
render: (value) => value,
|
|
17
|
+
parse: (raw) => raw
|
|
18
|
+
};
|
|
19
|
+
var labelName = {
|
|
20
|
+
name: "label",
|
|
21
|
+
pattern: "[A-Za-z0-9][A-Za-z0-9 _-]{0,127}",
|
|
22
|
+
example: "Parity Complete",
|
|
23
|
+
render: (value) => value,
|
|
24
|
+
parse: (raw) => raw
|
|
25
|
+
};
|
|
26
|
+
var emailAddress = {
|
|
27
|
+
name: "email",
|
|
28
|
+
pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
|
|
29
|
+
example: "alice@example.com",
|
|
30
|
+
render: (value) => value,
|
|
31
|
+
parse: (raw) => raw
|
|
32
|
+
};
|
|
33
|
+
var mailboxRef = {
|
|
34
|
+
name: "mailbox",
|
|
35
|
+
pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
|
|
36
|
+
example: "pome-agent@pome-twin.test",
|
|
37
|
+
render: (value) => value,
|
|
38
|
+
parse: (raw) => raw
|
|
39
|
+
};
|
|
40
|
+
var exactCount = {
|
|
41
|
+
name: "count",
|
|
42
|
+
pattern: "\\d{1,9}",
|
|
43
|
+
example: "5",
|
|
44
|
+
render: (value) => value,
|
|
45
|
+
parse: (raw) => raw
|
|
46
|
+
};
|
|
47
|
+
var countWord = {
|
|
48
|
+
name: "count",
|
|
49
|
+
pattern: "(?:\\d{1,9}|zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)",
|
|
50
|
+
example: "two",
|
|
51
|
+
render: (value) => value,
|
|
52
|
+
parse: (raw) => raw
|
|
53
|
+
};
|
|
54
|
+
var NUMBER_WORDS = {
|
|
55
|
+
zero: 0,
|
|
56
|
+
one: 1,
|
|
57
|
+
two: 2,
|
|
58
|
+
three: 3,
|
|
59
|
+
four: 4,
|
|
60
|
+
five: 5,
|
|
61
|
+
six: 6,
|
|
62
|
+
seven: 7,
|
|
63
|
+
eight: 8,
|
|
64
|
+
nine: 9,
|
|
65
|
+
ten: 10,
|
|
66
|
+
eleven: 11,
|
|
67
|
+
twelve: 12
|
|
68
|
+
};
|
|
69
|
+
function parseCount(raw) {
|
|
70
|
+
const trimmed = raw.trim().toLowerCase();
|
|
71
|
+
if (/^\d+$/.test(trimmed))
|
|
72
|
+
return Number(trimmed);
|
|
73
|
+
return NUMBER_WORDS[trimmed] ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ../twin-gmail/dist/src/check-state.js
|
|
77
|
+
var MESSAGES_PATH = statePath("messages");
|
|
78
|
+
var DRAFTS_PATH = statePath("drafts");
|
|
79
|
+
var LABELS_PATH = statePath("labels");
|
|
80
|
+
var MESSAGE_LABELS_PATH = statePath("messageLabels");
|
|
81
|
+
function missSkip(miss) {
|
|
82
|
+
const outcome = { passed: false, status: "skipped", reason: miss.missing };
|
|
83
|
+
if (miss.searched === void 0)
|
|
84
|
+
return outcome;
|
|
85
|
+
return { ...outcome, evidenceStatePaths: [miss.searched] };
|
|
86
|
+
}
|
|
87
|
+
var lower = (value) => (value ?? "").toLowerCase();
|
|
88
|
+
function isTruncated(state, collection) {
|
|
89
|
+
return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
|
|
90
|
+
}
|
|
91
|
+
function resolveMessage(state, id2) {
|
|
92
|
+
if (state.messages == null)
|
|
93
|
+
return { missing: "state_incomplete" };
|
|
94
|
+
const indices = state.messages.map((message2, index) => lower(message2.id) === lower(id2) ? index : -1).filter((index) => index >= 0);
|
|
95
|
+
if (indices.length > 1) {
|
|
96
|
+
return {
|
|
97
|
+
missing: `message_ambiguous ("${id2}" exists in ${indices.length} mailboxes)`,
|
|
98
|
+
searched: MESSAGES_PATH
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (indices.length === 1) {
|
|
102
|
+
const index = indices[0];
|
|
103
|
+
return { found: state.messages[index], path: childStatePath(MESSAGES_PATH, index) };
|
|
104
|
+
}
|
|
105
|
+
if (isTruncated(state, "messages")) {
|
|
106
|
+
return { missing: 'collection_truncated ("messages")', searched: MESSAGES_PATH };
|
|
107
|
+
}
|
|
108
|
+
return { missing: `message_not_found ("${id2}")`, searched: MESSAGES_PATH };
|
|
109
|
+
}
|
|
110
|
+
function labelIdsFor(state, wanted) {
|
|
111
|
+
const ids = /* @__PURE__ */ new Set([lower(wanted)]);
|
|
112
|
+
for (const label of state.labels ?? []) {
|
|
113
|
+
const matches = lower(label.name) === lower(wanted) || lower(label.id) === lower(wanted);
|
|
114
|
+
if (matches && label.id != null)
|
|
115
|
+
ids.add(lower(label.id));
|
|
116
|
+
}
|
|
117
|
+
return ids;
|
|
118
|
+
}
|
|
119
|
+
function messageCarriesLabel(state, messageId2, wanted) {
|
|
120
|
+
if (state.messageLabels == null)
|
|
121
|
+
return { missing: "state_incomplete" };
|
|
122
|
+
const ids = labelIdsFor(state, wanted);
|
|
123
|
+
const carried = state.messageLabels.some((row) => lower(row.messageId) === lower(messageId2) && ids.has(lower(row.labelId)));
|
|
124
|
+
if (!carried && isTruncated(state, "messageLabels")) {
|
|
125
|
+
return { missing: 'collection_truncated ("messageLabels")', searched: MESSAGE_LABELS_PATH };
|
|
126
|
+
}
|
|
127
|
+
return { found: carried, path: MESSAGE_LABELS_PATH };
|
|
128
|
+
}
|
|
129
|
+
function resolveLabelByName(state, name) {
|
|
130
|
+
if (state.labels == null)
|
|
131
|
+
return { missing: "state_incomplete" };
|
|
132
|
+
const index = state.labels.findIndex((label) => lower(label.name) === lower(name));
|
|
133
|
+
if (index >= 0) {
|
|
134
|
+
return { found: state.labels[index], path: childStatePath(LABELS_PATH, index) };
|
|
135
|
+
}
|
|
136
|
+
if (isTruncated(state, "labels")) {
|
|
137
|
+
return { missing: 'collection_truncated ("labels")', searched: LABELS_PATH };
|
|
138
|
+
}
|
|
139
|
+
return { missing: `label_not_found ("${name}")`, searched: LABELS_PATH };
|
|
140
|
+
}
|
|
141
|
+
function draftRecipients(state, draft2) {
|
|
142
|
+
const message2 = (state.messages ?? []).find((candidate) => lower(candidate.id) === lower(draft2.messageId));
|
|
143
|
+
if (!message2)
|
|
144
|
+
return [];
|
|
145
|
+
return [...message2.to ?? [], ...message2.cc ?? []];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ../twin-gmail/dist/src/check-worlds.js
|
|
149
|
+
var FIXTURE_MAILBOX = "pome-agent@pome-twin.test";
|
|
150
|
+
function gmailState(parts = {}) {
|
|
151
|
+
return {
|
|
152
|
+
mailboxes: [{ email: FIXTURE_MAILBOX }],
|
|
153
|
+
messages: [],
|
|
154
|
+
drafts: [],
|
|
155
|
+
labels: [],
|
|
156
|
+
messageLabels: [],
|
|
157
|
+
exportBounds: { messageBodiesOmitted: true, largeMailbox: false, truncatedCollections: [] },
|
|
158
|
+
...parts
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function finalWorld(final) {
|
|
162
|
+
return { seed: null, final, tape: null };
|
|
163
|
+
}
|
|
164
|
+
function tapeWorld(tape) {
|
|
165
|
+
return { seed: null, final: gmailState(), tape };
|
|
166
|
+
}
|
|
167
|
+
function message(id2, parts = {}) {
|
|
168
|
+
return { mailboxEmail: FIXTURE_MAILBOX, id: id2, to: [FIXTURE_MAILBOX], bodyOmitted: true, ...parts };
|
|
169
|
+
}
|
|
170
|
+
function systemLabel(name) {
|
|
171
|
+
return { mailboxEmail: FIXTURE_MAILBOX, id: name, name, type: "system" };
|
|
172
|
+
}
|
|
173
|
+
function userLabel(id2, name) {
|
|
174
|
+
return { mailboxEmail: FIXTURE_MAILBOX, id: id2, name, type: "user" };
|
|
175
|
+
}
|
|
176
|
+
function messageLabel(messageId2, labelId) {
|
|
177
|
+
return { mailboxEmail: FIXTURE_MAILBOX, messageId: messageId2, labelId };
|
|
178
|
+
}
|
|
179
|
+
function draft(id2, messageId2) {
|
|
180
|
+
return { mailboxEmail: FIXTURE_MAILBOX, id: id2, messageId: messageId2 };
|
|
181
|
+
}
|
|
182
|
+
function draftAddressedTo(id2, recipients) {
|
|
183
|
+
const messageId2 = `${id2}_message`;
|
|
184
|
+
return {
|
|
185
|
+
draft: draft(id2, messageId2),
|
|
186
|
+
message: message(messageId2, { to: recipients })
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ../twin-gmail/dist/src/check-drafts.js
|
|
191
|
+
var draftAddressedTo2 = defineCheck({
|
|
192
|
+
id: "gmail.draft-addressed-to",
|
|
193
|
+
description: "Joins every draft to its backing message and asks whether any of them addresses this recipient in `to` or `cc`, compared case-insensitively as an EXACT address rather than a substring. It asserts nothing about the draft's body \u2014 message bodies are digested out of the state export unconditionally, so no check on this twin can read one \u2014 and nothing about whether the draft was left unsent, which is a separate claim. A draft whose backing message did not survive the export contributes no recipients rather than throwing.",
|
|
194
|
+
template: "A draft addressed to {email} exists",
|
|
195
|
+
params: { email: emailAddress },
|
|
196
|
+
substrate: "final",
|
|
197
|
+
// An achievement: the seeded mailbox already contains a draft with a
|
|
198
|
+
// recipient, so an examinee that does nothing does not satisfy this.
|
|
199
|
+
polarity: () => "positive",
|
|
200
|
+
//
|
|
201
|
+
// THE LOAD-BEARING DECLARATION ON THIS CHECK.
|
|
202
|
+
//
|
|
203
|
+
// `subject` names the value the assertion is actually about, and on this check
|
|
204
|
+
// that value is an email address — squarely inside what a redaction config may
|
|
205
|
+
// legitimately destroy before an evaluator ever sees the state. Declaring it
|
|
206
|
+
// is what lets a consumer detect that the comparison has become impossible and
|
|
207
|
+
// record an honest skip. Without it there is nothing to detect: the address is
|
|
208
|
+
// simply absent, the comparison silently cannot match, and the criterion
|
|
209
|
+
// reports a vacuous verdict that reads like a real one.
|
|
210
|
+
subject: ({ email: email2 }) => email2,
|
|
211
|
+
// The address is the scanned literal, so the mutant points at it. It stays
|
|
212
|
+
// email-shaped so it re-binds to this same check: a mutant that stops matching
|
|
213
|
+
// evaluates to `unmatched`, which reads as "the verdict moved -> healthy" and
|
|
214
|
+
// hands the criterion a clean bill it did not earn.
|
|
215
|
+
vacuityMutant: (args) => ({ ...args, email: `${VACUITY_SENTINEL}@example.invalid` }),
|
|
216
|
+
discriminatingWorlds: ({ email: email2 }) => {
|
|
217
|
+
const wanted = draftAddressedTo("draft_target", [email2]);
|
|
218
|
+
const other = draftAddressedTo("draft_other", ["someone-else@example.com"]);
|
|
219
|
+
return {
|
|
220
|
+
passing: finalWorld(gmailState({ drafts: [wanted.draft], messages: [wanted.message] })),
|
|
221
|
+
failing: finalWorld(gmailState({ drafts: [other.draft], messages: [other.message] }))
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
evaluate({ email: email2 }, { final }) {
|
|
225
|
+
if (final.drafts == null || final.messages == null) {
|
|
226
|
+
return { passed: false, status: "skipped", reason: "state_incomplete" };
|
|
227
|
+
}
|
|
228
|
+
const wanted = email2.toLowerCase();
|
|
229
|
+
const index = final.drafts.findIndex((entry) => draftRecipients(final, entry).some((to) => to.toLowerCase() === wanted));
|
|
230
|
+
if (index >= 0) {
|
|
231
|
+
const hit = final.drafts[index];
|
|
232
|
+
return {
|
|
233
|
+
passed: true,
|
|
234
|
+
reason: `draft ${hit.id ?? "?"} is addressed to ${email2}`,
|
|
235
|
+
// The DRAFT row, not the backing message that actually holds the
|
|
236
|
+
// address. A reader following this pointer lands on the thing the
|
|
237
|
+
// sentence is about — "a draft addressed to X" — and the join to the
|
|
238
|
+
// message is an implementation detail of how the twin exports, not the
|
|
239
|
+
// claim.
|
|
240
|
+
evidenceStatePaths: [childStatePath(DRAFTS_PATH, index)]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (isTruncated(final, "drafts") || isTruncated(final, "messages")) {
|
|
244
|
+
return { passed: false, status: "skipped", reason: "collection_truncated" };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
passed: false,
|
|
248
|
+
reason: `no draft is addressed to ${email2} (${final.drafts.length} draft(s) inspected)`,
|
|
249
|
+
evidenceStatePaths: [DRAFTS_PATH]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
var draftCountAtLeast = defineCheck({
|
|
254
|
+
id: "gmail.draft-count-at-least",
|
|
255
|
+
description: "Counts the rows in the exported `drafts` collection and asserts there are AT LEAST this many \u2014 a lower bound, so a mailbox with more drafts than asked still passes. It counts drafts the SEED placed there as well as any the examinee created, which means a criterion whose number the seed already satisfies can be passed by an agent that does nothing. That is a property of the task, not of this check, and `measure-criterion-discrimination` is where it surfaces.",
|
|
256
|
+
template: "At least {count} drafts exist",
|
|
257
|
+
params: { count: countWord },
|
|
258
|
+
substrate: "final",
|
|
259
|
+
polarity: () => "positive",
|
|
260
|
+
// A threshold, not a literal hunted for inside state — there is nothing here a
|
|
261
|
+
// redactor could silently delete.
|
|
262
|
+
subject: () => null,
|
|
263
|
+
//
|
|
264
|
+
// The NUMERIC sentinel, and the argument D10 demands before it may be used.
|
|
265
|
+
//
|
|
266
|
+
// In every other pattern a numeric capture is a SELECTOR — an issue number, a
|
|
267
|
+
// PR number — that the predicate RESOLVES before it scans anything, so
|
|
268
|
+
// falsifying it early-returns "not found" and moves the verdict for a reason
|
|
269
|
+
// that never reaches the assertion. Here the count is the ONLY slot and there
|
|
270
|
+
// is nothing to resolve: it is compared directly against a cardinality the
|
|
271
|
+
// predicate computes, so falsifying it moves the verdict THROUGH the
|
|
272
|
+
// assertion. That is the same argument `stripe.payment-intent-amount` makes,
|
|
273
|
+
// and it is why a numeric sentinel is admissible on this check and on very few
|
|
274
|
+
// others: an evaluator that allowlists the numeric sentinel per check should
|
|
275
|
+
// expect these two.
|
|
276
|
+
vacuityMutant: (args) => ({ ...args, count: String(VACUITY_SENTINEL_NUMBER) }),
|
|
277
|
+
discriminatingWorlds: ({ count }) => {
|
|
278
|
+
const wanted = parseCount(count) ?? 1;
|
|
279
|
+
const build = (n) => {
|
|
280
|
+
const pairs = Array.from({ length: n }, (_, i) => draftAddressedTo(`draft_${i}`, ["a@example.com"]));
|
|
281
|
+
return gmailState({
|
|
282
|
+
drafts: pairs.map((p) => p.draft),
|
|
283
|
+
messages: pairs.map((p) => p.message)
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
return { passing: finalWorld(build(wanted)), failing: finalWorld(build(Math.max(0, wanted - 1))) };
|
|
287
|
+
},
|
|
288
|
+
evaluate({ count }, { final }) {
|
|
289
|
+
const wanted = parseCount(count);
|
|
290
|
+
if (wanted === null) {
|
|
291
|
+
return { passed: false, status: "skipped", reason: `uncountable ("${count}")` };
|
|
292
|
+
}
|
|
293
|
+
if (final.drafts == null)
|
|
294
|
+
return { passed: false, status: "skipped", reason: "state_incomplete" };
|
|
295
|
+
const total = final.drafts.length;
|
|
296
|
+
if (total < wanted && isTruncated(final, "drafts")) {
|
|
297
|
+
return { passed: false, status: "skipped", reason: 'collection_truncated ("drafts")' };
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
passed: total >= wanted,
|
|
301
|
+
reason: `${total} draft(s) exist (wanted at least ${wanted})`,
|
|
302
|
+
// The collection whose LENGTH is the assertion. There is no narrower
|
|
303
|
+
// address for a cardinality.
|
|
304
|
+
evidenceStatePaths: [DRAFTS_PATH]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ../twin-gmail/dist/src/check-labels.js
|
|
310
|
+
var labelExists = defineCheck({
|
|
311
|
+
id: "gmail.label-exists",
|
|
312
|
+
description: "Asks whether the mailbox defines a label with this DISPLAY NAME, compared case-insensitively the way the twin's own seeder keys its uniqueness lookup. It reads the `labels` collection only \u2014 a label that exists but has been applied to nothing still passes, and a message carrying a label is `gmail.message-has-label`'s question, not this one. It deliberately does NOT match on the minted id: `Label_follow_up` is an id and `Follow Up` is the name of that same label, and letting one sentence mean both would make the assertion unreadable.",
|
|
313
|
+
// The name slot admits spaces, which is what the corpus needs (`Parity
|
|
314
|
+
// Complete`) and what separates this template from `gmail.message-has-label`'s
|
|
315
|
+
// id slot. The two cannot claim one sentence: this one says "A label named",
|
|
316
|
+
// that one says "Message ... has label".
|
|
317
|
+
template: "A label named {label} exists",
|
|
318
|
+
params: { label: labelName },
|
|
319
|
+
substrate: "final",
|
|
320
|
+
// An achievement — the seed defines no `Parity Complete`, so the
|
|
321
|
+
// examinee has to create it.
|
|
322
|
+
polarity: () => "positive",
|
|
323
|
+
// The name is a caller-supplied literal compared against state.
|
|
324
|
+
subject: ({ label }) => label,
|
|
325
|
+
vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
|
|
326
|
+
discriminatingWorlds: ({ label }) => ({
|
|
327
|
+
// Both worlds carry a non-empty `labels` collection, so the failing world
|
|
328
|
+
// fails on the ASSERTION (`label_not_found`) rather than reproducing the
|
|
329
|
+
// `state_incomplete` an empty world gives — the degenerate arm the probe
|
|
330
|
+
// rejects.
|
|
331
|
+
passing: finalWorld(gmailState({ labels: [systemLabel("INBOX"), userLabel("Label_1", label)] })),
|
|
332
|
+
failing: finalWorld(gmailState({ labels: [systemLabel("INBOX")] }))
|
|
333
|
+
}),
|
|
334
|
+
evaluate({ label }, { final }) {
|
|
335
|
+
const found = resolveLabelByName(final, label);
|
|
336
|
+
if ("missing" in found) {
|
|
337
|
+
if (found.missing.startsWith("label_not_found")) {
|
|
338
|
+
return {
|
|
339
|
+
passed: false,
|
|
340
|
+
reason: `no label named "${label}" exists`,
|
|
341
|
+
// The label COLLECTION — the honest citation for a lookup that found
|
|
342
|
+
// nothing, and the arm a reader most wants to open: see for yourself
|
|
343
|
+
// that the name is not in it.
|
|
344
|
+
...found.searched === void 0 ? {} : { evidenceStatePaths: [found.searched] }
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return missSkip(found);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
passed: true,
|
|
351
|
+
reason: `label "${label}" exists (id ${found.found.id ?? "?"})`,
|
|
352
|
+
evidenceStatePaths: [found.path]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ../twin-gmail/dist/src/check-messages.js
|
|
358
|
+
var messageHasLabel = defineCheck({
|
|
359
|
+
id: "gmail.message-has-label",
|
|
360
|
+
description: "Looks the message up by id, then asks the `messageLabels` JOIN whether any row links it to the named label. The label may be given as its minted id (`Label_follow_up`) or as the display name of the same label (`Follow Up`); a system label carries both as one string (`INBOX`, `STARRED`). It asserts nothing about the message's OTHER labels \u2014 a message carrying the named label plus five more passes. A message the export does not carry is a SKIP, not a fail, and so is a label collection the export truncated: absent from a capped list is not absent.",
|
|
361
|
+
template: "Message {message} has label {label}",
|
|
362
|
+
params: { message: messageId, label: labelRef },
|
|
363
|
+
substrate: "final",
|
|
364
|
+
// An achievement. The seed leaves `msg_support` on INBOX/UNREAD only, so the
|
|
365
|
+
// examinee has to act for this to hold.
|
|
366
|
+
polarity: () => "positive",
|
|
367
|
+
// The label is a caller-supplied literal compared against state, so it is
|
|
368
|
+
// declared. No first-party redactor pattern touches a Gmail label id, which is
|
|
369
|
+
// exactly why declaring it keeps that a verified fact rather than an
|
|
370
|
+
// assumption — a team's own redaction config is not first-party.
|
|
371
|
+
subject: ({ label }) => label,
|
|
372
|
+
// The message id only SELECTS; falsifying it moves the verdict through the
|
|
373
|
+
// lookup rather than the assertion. The label is the scanned literal.
|
|
374
|
+
vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
|
|
375
|
+
discriminatingWorlds: ({ message: id2, label }) => {
|
|
376
|
+
const labels = [systemLabel("INBOX"), userLabel(label, label)];
|
|
377
|
+
const base = { messages: [message(id2)], labels };
|
|
378
|
+
return {
|
|
379
|
+
passing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX"), messageLabel(id2, label)] })),
|
|
380
|
+
failing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX")] }))
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
evaluate({ message: id2, label }, { final }) {
|
|
384
|
+
const found = resolveMessage(final, id2);
|
|
385
|
+
if ("missing" in found)
|
|
386
|
+
return missSkip(found);
|
|
387
|
+
const carried = messageCarriesLabel(final, id2, label);
|
|
388
|
+
if ("missing" in carried)
|
|
389
|
+
return missSkip(carried);
|
|
390
|
+
return {
|
|
391
|
+
passed: carried.found,
|
|
392
|
+
reason: carried.found ? `message ${id2} carries label ${label}` : `message ${id2} does not carry label ${label}`,
|
|
393
|
+
// BOTH halves of the lookup, because the predicate really does read two
|
|
394
|
+
// places: the message row it resolved by id, and the join table it then
|
|
395
|
+
// questioned. Labels are not nested under their message in this export —
|
|
396
|
+
// that is the whole reason `messageCarriesLabel` exists — so one pointer
|
|
397
|
+
// could not say where this verdict came from.
|
|
398
|
+
evidenceStatePaths: [found.path, carried.path]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
var mailboxLabelCount = defineCheck({
|
|
403
|
+
id: "gmail.mailbox-label-count",
|
|
404
|
+
description: "Counts the messages in the named mailbox that the `messageLabels` JOIN links to the named label, and asserts the total is EXACTLY the number given. Not 'at least' \u2014 a mailbox with six SENT messages fails a criterion asking for five, which is what makes it able to catch a duplicate send. A mailbox the export lists but does not contain is a SKIP; so is a truncated collection.",
|
|
405
|
+
template: "The mailbox `{mailbox}` has exactly {count} messages labeled {label}",
|
|
406
|
+
params: { mailbox: mailboxRef, count: exactCount, label: labelRef },
|
|
407
|
+
substrate: "final",
|
|
408
|
+
// The count carries the direction: asserting zero is a prohibition, any
|
|
409
|
+
// other count is work the examinee must do. Preserved from the legacy rule,
|
|
410
|
+
// which computed the same thing per match.
|
|
411
|
+
polarity: ({ count }) => Number(count) === 0 ? "negative" : "positive",
|
|
412
|
+
subject: ({ label }) => label,
|
|
413
|
+
// The mailbox resolves and the count is compared to a derived total; the
|
|
414
|
+
// label is the literal actually scanned in state. Pointing the mutant at the
|
|
415
|
+
// count would falsify a threshold rather than a lookup — see
|
|
416
|
+
// `gmail.draft-count-at-least`, where the count IS the only slot.
|
|
417
|
+
vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
|
|
418
|
+
discriminatingWorlds: ({ mailbox, count, label }) => {
|
|
419
|
+
const wanted = Number(count);
|
|
420
|
+
const world = (n) => {
|
|
421
|
+
const messages = Array.from({ length: n }, (_, i) => message(`msg_${i}`, { mailboxEmail: mailbox }));
|
|
422
|
+
return gmailState({
|
|
423
|
+
mailboxes: [{ email: mailbox }],
|
|
424
|
+
messages,
|
|
425
|
+
labels: [systemLabel(label)],
|
|
426
|
+
messageLabels: messages.map((m) => ({ mailboxEmail: mailbox, messageId: m.id, labelId: label }))
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
return { passing: finalWorld(world(wanted)), failing: finalWorld(world(wanted + 1)) };
|
|
430
|
+
},
|
|
431
|
+
evaluate({ mailbox, count, label }, { final }) {
|
|
432
|
+
const wanted = Number(count);
|
|
433
|
+
if (final.messages == null || final.messageLabels == null) {
|
|
434
|
+
return { passed: false, status: "skipped", reason: "state_incomplete" };
|
|
435
|
+
}
|
|
436
|
+
if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
|
|
437
|
+
return { passed: false, status: "skipped", reason: "collection_truncated" };
|
|
438
|
+
}
|
|
439
|
+
if (final.mailboxes != null && !final.mailboxes.some((mb) => (mb.email ?? "").toLowerCase() === mailbox.toLowerCase())) {
|
|
440
|
+
return { passed: false, status: "skipped", reason: `mailbox_not_found ("${mailbox}")` };
|
|
441
|
+
}
|
|
442
|
+
const ids = labelIdsFor(final, label);
|
|
443
|
+
const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
|
|
444
|
+
const total = final.messages.filter((msg) => (msg.mailboxEmail ?? "").toLowerCase() === mailbox.toLowerCase() && msg.id != null && labeled.has(msg.id.toLowerCase())).length;
|
|
445
|
+
return {
|
|
446
|
+
passed: total === wanted,
|
|
447
|
+
reason: `mailbox "${mailbox}" has ${total} message(s) labeled ${label} (wanted ${wanted})`,
|
|
448
|
+
// The two collections the count is computed FROM. The count itself lives
|
|
449
|
+
// nowhere in the tree, so there is no narrower honest address — and the
|
|
450
|
+
// guards above already established that both collections are present and
|
|
451
|
+
// un-capped, which is what makes these pointers resolve.
|
|
452
|
+
evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
var oneMessagePerRecipient = defineCheck({
|
|
457
|
+
id: "gmail.one-message-per-recipient",
|
|
458
|
+
description: "Flattens every addressee across the messages carrying the named label and asserts each address appears EXACTLY ONCE \u2014 and, when a count is named, that there are that many messages, that many distinct recipients, and no more addressee slots than that. It is the assertion that separates 'sent to everyone' from 'sent to everyone, some of them twice', which an exact total alone cannot do.",
|
|
459
|
+
template: "Exactly one {label} message is addressed to each of the {count} recipients",
|
|
460
|
+
params: { label: labelRef, count: countWord },
|
|
461
|
+
substrate: "final",
|
|
462
|
+
polarity: () => "positive",
|
|
463
|
+
subject: ({ label }) => label,
|
|
464
|
+
// The label is scanned; the count is a declared arity compared to a derived
|
|
465
|
+
// one. Same argument as `mailboxLabelCount`.
|
|
466
|
+
vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
|
|
467
|
+
discriminatingWorlds: ({ label, count }) => {
|
|
468
|
+
const wanted = parseCount(count) ?? 1;
|
|
469
|
+
const recipients = Array.from({ length: wanted }, (_, i) => `user${i}@example.com`);
|
|
470
|
+
const build = (addressees) => {
|
|
471
|
+
const messages = addressees.map((to, i) => message(`msg_${i}`, { to }));
|
|
472
|
+
return finalWorld(gmailState({
|
|
473
|
+
messages,
|
|
474
|
+
labels: [systemLabel(label)],
|
|
475
|
+
messageLabels: messages.map((m) => messageLabel(m.id, label))
|
|
476
|
+
}));
|
|
477
|
+
};
|
|
478
|
+
return {
|
|
479
|
+
passing: build(recipients.map((r) => [r])),
|
|
480
|
+
// The DUPLICATE SEND, not a missing one: the same number of messages, one
|
|
481
|
+
// recipient served twice and another not at all. A world with fewer
|
|
482
|
+
// messages would fail on arity, which the exact-count check already covers.
|
|
483
|
+
failing: build([[recipients[0]], ...recipients.slice(2).map((r) => [r]), [recipients[0]]])
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
evaluate({ label, count }, { final }) {
|
|
487
|
+
const wanted = parseCount(count);
|
|
488
|
+
if (final.messages == null || final.messageLabels == null) {
|
|
489
|
+
return { passed: false, status: "skipped", reason: "state_incomplete" };
|
|
490
|
+
}
|
|
491
|
+
if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
|
|
492
|
+
return { passed: false, status: "skipped", reason: "collection_truncated" };
|
|
493
|
+
}
|
|
494
|
+
const ids = labelIdsFor(final, label);
|
|
495
|
+
const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
|
|
496
|
+
const sent = final.messages.filter((msg) => msg.id != null && labeled.has(msg.id.toLowerCase()));
|
|
497
|
+
const addressees = sent.flatMap((msg) => (msg.to ?? []).map((to) => to.toLowerCase()));
|
|
498
|
+
const distinct = new Set(addressees);
|
|
499
|
+
const noDuplicates = addressees.length === distinct.size;
|
|
500
|
+
const arityOk = wanted == null ? sent.length === distinct.size : sent.length === wanted && distinct.size === wanted && addressees.length === wanted;
|
|
501
|
+
const passed = noDuplicates && arityOk;
|
|
502
|
+
return {
|
|
503
|
+
passed,
|
|
504
|
+
reason: passed ? `${sent.length} ${label} message(s), one per distinct recipient, no duplicates` : `${sent.length} ${label} message(s) to ${distinct.size} distinct recipient(s) (${addressees.length} addressee slot(s)${wanted == null ? "" : `, wanted ${wanted}`}${noDuplicates ? "" : ", duplicate send detected"})`,
|
|
505
|
+
// Same two collections, same reason: the duplicate this check exists to
|
|
506
|
+
// catch is a property of the addressee lists across `messages`, filtered
|
|
507
|
+
// by `messageLabels`, and neither the flattened list nor its distinct set
|
|
508
|
+
// is anything the tree holds.
|
|
509
|
+
evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ../twin-gmail/dist/src/check-tape.js
|
|
515
|
+
var noUnsupportedEndpoint = defineCheck({
|
|
516
|
+
id: "gmail.no-unsupported-endpoint",
|
|
517
|
+
description: 'Scans the recorded call tape for any request the twin answered with fidelity "unsupported" \u2014 a route it does not implement, answered 501. It asserts nothing about whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a 422 from a route the twin does implement is a semantic answer and passes this check. The tape is scoped to this twin by the engine before the check sees it, so an unsupported call to a DIFFERENT twin in a multi-twin session cannot fail it \u2014 which matters here, because a single task may run gmail and github together.',
|
|
518
|
+
// No slots, and no twin word. The legacy cloud rule accepted "No unsupported
|
|
519
|
+
// Gmail endpoint was called" alongside the bare form, plus plural and `were`
|
|
520
|
+
// variants, because an author typed English. Under position 2 an author PICKS
|
|
521
|
+
// the check, so those variants are retired rather than ported — the same
|
|
522
|
+
// decision `github.no-unsupported-endpoint` made, and this is now the same
|
|
523
|
+
// sentence on both twins, resolved per-twin by the engine.
|
|
524
|
+
template: "No unsupported endpoint was called",
|
|
525
|
+
params: {},
|
|
526
|
+
substrate: "tape",
|
|
527
|
+
// A prohibition. Nothing is required to happen; only the examinee reaching for
|
|
528
|
+
// an unimplemented route can break it.
|
|
529
|
+
polarity: () => "negative",
|
|
530
|
+
// No caller-supplied literal is hunted for in any substrate, so there is
|
|
531
|
+
// nothing a redactor could silently delete out from under this check.
|
|
532
|
+
subject: () => null,
|
|
533
|
+
// No slots, so the sentence carries no literal to falsify. The trigger is "a
|
|
534
|
+
// call with fidelity=unsupported exists", which lives on the tape and not in
|
|
535
|
+
// the sentence. Reported as `no_trigger`, never as clean.
|
|
536
|
+
vacuityMutant: () => null,
|
|
537
|
+
discriminatingWorlds: () => ({
|
|
538
|
+
passing: tapeWorld([
|
|
539
|
+
{
|
|
540
|
+
twin: "gmail",
|
|
541
|
+
method: "GET",
|
|
542
|
+
path: "/gmail/v1/users/me/messages",
|
|
543
|
+
status: 200,
|
|
544
|
+
fidelity: "semantic",
|
|
545
|
+
event_id: "evt_ok"
|
|
546
|
+
}
|
|
547
|
+
]),
|
|
548
|
+
failing: tapeWorld([
|
|
549
|
+
{
|
|
550
|
+
twin: "gmail",
|
|
551
|
+
method: "POST",
|
|
552
|
+
path: "/gmail/v1/users/me/watch",
|
|
553
|
+
status: 501,
|
|
554
|
+
fidelity: "unsupported",
|
|
555
|
+
event_id: "evt_bad"
|
|
556
|
+
}
|
|
557
|
+
])
|
|
558
|
+
}),
|
|
559
|
+
evaluate(_args, { tape }) {
|
|
560
|
+
if (tape === null)
|
|
561
|
+
return { passed: false, reason: "tape_missing", status: "skipped" };
|
|
562
|
+
const unsupported = tape.filter((event) => event.fidelity === "unsupported");
|
|
563
|
+
if (unsupported.length === 0) {
|
|
564
|
+
return {
|
|
565
|
+
passed: true,
|
|
566
|
+
reason: `no unsupported Gmail endpoint was called (${tape.length} call(s) inspected)`
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id2) => typeof id2 === "string" && id2 !== "");
|
|
570
|
+
const outcome = {
|
|
571
|
+
passed: false,
|
|
572
|
+
reason: `${unsupported.length} unsupported Gmail call(s): [${unsupported.map((event) => event.path ?? "?").join(", ")}]`
|
|
573
|
+
};
|
|
574
|
+
return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ../twin-gmail/dist/src/checks.js
|
|
579
|
+
var GMAIL_CHECKS = [
|
|
580
|
+
messageHasLabel,
|
|
581
|
+
labelExists,
|
|
582
|
+
draftAddressedTo2,
|
|
583
|
+
draftCountAtLeast,
|
|
584
|
+
mailboxLabelCount,
|
|
585
|
+
oneMessagePerRecipient,
|
|
586
|
+
// Last, because these are the only ones that assert about the RUN rather than
|
|
587
|
+
// the world it left behind — twin-github orders its tape checks the same way.
|
|
588
|
+
noUnsupportedEndpoint
|
|
589
|
+
];
|
|
590
|
+
var GmailError = class extends Error {
|
|
591
|
+
status;
|
|
592
|
+
reason;
|
|
593
|
+
constructor(status, reason, message2) {
|
|
594
|
+
super(message2);
|
|
595
|
+
this.status = status;
|
|
596
|
+
this.reason = reason;
|
|
597
|
+
this.name = "GmailError";
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
function invalidArgument(message2) {
|
|
601
|
+
throw new GmailError(400, "invalidArgument", message2);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ../twin-gmail/dist/src/faults.js
|
|
605
|
+
var KNOWN_FAULT_NAMES = ["rate-limited"];
|
|
606
|
+
var gmailFaultSchema = z.object({
|
|
607
|
+
name: z.enum(KNOWN_FAULT_NAMES),
|
|
608
|
+
target: z.string().min(1).max(128).default("messages.send"),
|
|
609
|
+
succeedFirst: z.number().int().nonnegative().max(1e3).default(2),
|
|
610
|
+
throttleFor: z.number().int().positive().max(1e3).default(3),
|
|
611
|
+
retryAfterSeconds: z.number().int().positive().max(3600).default(1)
|
|
612
|
+
}).strict();
|
|
613
|
+
|
|
614
|
+
// ../twin-gmail/dist/src/search-parse.js
|
|
615
|
+
var MAX_QUERY_BYTES = 4096;
|
|
616
|
+
var MAX_TOKENS = 256;
|
|
617
|
+
var MAX_DEPTH = 20;
|
|
618
|
+
var MAX_BRANCHES = 256;
|
|
619
|
+
var KNOWN_FIELDS = /* @__PURE__ */ new Set([
|
|
620
|
+
"from",
|
|
621
|
+
"to",
|
|
622
|
+
"cc",
|
|
623
|
+
"bcc",
|
|
624
|
+
"deliveredto",
|
|
625
|
+
"list",
|
|
626
|
+
"subject",
|
|
627
|
+
"rfc822msgid",
|
|
628
|
+
"filename",
|
|
629
|
+
"after",
|
|
630
|
+
"newer",
|
|
631
|
+
"before",
|
|
632
|
+
"older",
|
|
633
|
+
"newer_than",
|
|
634
|
+
"older_than",
|
|
635
|
+
"size",
|
|
636
|
+
"larger",
|
|
637
|
+
"smaller",
|
|
638
|
+
"label",
|
|
639
|
+
"category",
|
|
640
|
+
"in",
|
|
641
|
+
"is",
|
|
642
|
+
"has"
|
|
643
|
+
]);
|
|
644
|
+
var CATEGORY_LABELS = {
|
|
645
|
+
primary: "CATEGORY_PERSONAL",
|
|
646
|
+
personal: "CATEGORY_PERSONAL",
|
|
647
|
+
social: "CATEGORY_SOCIAL",
|
|
648
|
+
promotions: "CATEGORY_PROMOTIONS",
|
|
649
|
+
updates: "CATEGORY_UPDATES",
|
|
650
|
+
forums: "CATEGORY_FORUMS"
|
|
651
|
+
};
|
|
652
|
+
function validateSearchQuery(query) {
|
|
653
|
+
const node = parseSearchQuery(query);
|
|
654
|
+
const walk = (current) => {
|
|
655
|
+
switch (current.type) {
|
|
656
|
+
case "and":
|
|
657
|
+
case "or":
|
|
658
|
+
for (const child of current.children)
|
|
659
|
+
walk(child);
|
|
660
|
+
return;
|
|
661
|
+
case "not":
|
|
662
|
+
walk(current.child);
|
|
663
|
+
return;
|
|
664
|
+
case "around":
|
|
665
|
+
return;
|
|
666
|
+
case "term":
|
|
667
|
+
assertSearchTerm(current.field, current.value);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
walk(node);
|
|
672
|
+
return node;
|
|
673
|
+
}
|
|
674
|
+
function assertSearchTerm(field, value) {
|
|
675
|
+
if (!field)
|
|
676
|
+
return;
|
|
677
|
+
if (!KNOWN_FIELDS.has(field)) {
|
|
678
|
+
invalidArgument(`Unsupported search operator: ${field}`);
|
|
679
|
+
}
|
|
680
|
+
if (field === "category") {
|
|
681
|
+
if (!CATEGORY_LABELS[value.toLocaleLowerCase("en-US")]) {
|
|
682
|
+
invalidArgument(`Unsupported search category: ${value}`);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (field === "after" || field === "newer" || field === "before" || field === "older") {
|
|
687
|
+
parseDate(value);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (field === "newer_than" || field === "older_than") {
|
|
691
|
+
parseDuration(value);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (field === "size" || field === "larger" || field === "smaller") {
|
|
695
|
+
parseSize(value);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (field === "has") {
|
|
699
|
+
assertHasOperator(value);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function parseSearchQuery(query) {
|
|
703
|
+
if (Buffer.byteLength(query) > MAX_QUERY_BYTES)
|
|
704
|
+
invalidArgument("Search query exceeds limit");
|
|
705
|
+
const tokens = tokenize(query);
|
|
706
|
+
if (tokens.length > MAX_TOKENS)
|
|
707
|
+
invalidArgument("Search query has too many tokens");
|
|
708
|
+
if (tokens.length === 0)
|
|
709
|
+
return { type: "and", children: [] };
|
|
710
|
+
let position = 0;
|
|
711
|
+
const parseExpression = (depth, stop) => {
|
|
712
|
+
if (depth > MAX_DEPTH)
|
|
713
|
+
invalidArgument("Search query nesting exceeds limit");
|
|
714
|
+
const alternatives = [];
|
|
715
|
+
let conjunction = [];
|
|
716
|
+
const flush = () => {
|
|
717
|
+
alternatives.push(conjunction.length === 1 ? conjunction[0] : { type: "and", children: conjunction });
|
|
718
|
+
conjunction = [];
|
|
719
|
+
};
|
|
720
|
+
while (position < tokens.length) {
|
|
721
|
+
const token = tokens[position];
|
|
722
|
+
if (stop && token.value === stop)
|
|
723
|
+
break;
|
|
724
|
+
if (token.value.toUpperCase() === "OR") {
|
|
725
|
+
position++;
|
|
726
|
+
if (!conjunction.length)
|
|
727
|
+
invalidArgument("OR requires a left expression");
|
|
728
|
+
flush();
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (token.value.toUpperCase() === "AND") {
|
|
732
|
+
position++;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
conjunction.push(parsePrimary(depth + 1));
|
|
736
|
+
}
|
|
737
|
+
if (conjunction.length)
|
|
738
|
+
flush();
|
|
739
|
+
if (!alternatives.length)
|
|
740
|
+
return { type: "and", children: [] };
|
|
741
|
+
return alternatives.length === 1 ? alternatives[0] : { type: "or", children: alternatives };
|
|
742
|
+
};
|
|
743
|
+
const parsePrimary = (depth) => {
|
|
744
|
+
const token = tokens[position++];
|
|
745
|
+
if (token.value === "(" || token.value === "{") {
|
|
746
|
+
const end = token.value === "(" ? ")" : "}";
|
|
747
|
+
const child = parseExpression(depth, end);
|
|
748
|
+
if (tokens[position]?.value !== end)
|
|
749
|
+
invalidArgument(`Unclosed ${token.value}`);
|
|
750
|
+
position++;
|
|
751
|
+
return token.value === "{" ? makeImplicitOr(child) : child;
|
|
752
|
+
}
|
|
753
|
+
if (token.value === ")" || token.value === "}")
|
|
754
|
+
invalidArgument(`Unexpected ${token.value}`);
|
|
755
|
+
let value = token.value;
|
|
756
|
+
let negate = false;
|
|
757
|
+
if (value === "-") {
|
|
758
|
+
negate = true;
|
|
759
|
+
const next = tokens[position++];
|
|
760
|
+
if (!next)
|
|
761
|
+
invalidArgument("Negation requires an expression");
|
|
762
|
+
position--;
|
|
763
|
+
const child = parsePrimary(depth + 1);
|
|
764
|
+
return { type: "not", child };
|
|
765
|
+
}
|
|
766
|
+
if (value.startsWith("-") && value.length > 1) {
|
|
767
|
+
negate = true;
|
|
768
|
+
value = value.slice(1);
|
|
769
|
+
}
|
|
770
|
+
const around = tokens[position]?.value.toUpperCase() === "AROUND";
|
|
771
|
+
if (around) {
|
|
772
|
+
position++;
|
|
773
|
+
const distance = Number(tokens[position++]?.value);
|
|
774
|
+
const right = tokens[position++]?.value;
|
|
775
|
+
if (!Number.isInteger(distance) || distance < 1 || distance > 100 || !right) {
|
|
776
|
+
invalidArgument("AROUND requires a distance and right term");
|
|
777
|
+
}
|
|
778
|
+
const node2 = { type: "around", left: value, right, distance };
|
|
779
|
+
return negate ? { type: "not", child: node2 } : node2;
|
|
780
|
+
}
|
|
781
|
+
const colon = value.indexOf(":");
|
|
782
|
+
let node;
|
|
783
|
+
if (colon > 0) {
|
|
784
|
+
const field = value.slice(0, colon).toLowerCase();
|
|
785
|
+
let fieldValue = value.slice(colon + 1);
|
|
786
|
+
if (!fieldValue && (tokens[position]?.value === "(" || tokens[position]?.value === "{")) {
|
|
787
|
+
node = parseFieldGroup(field, depth + 1);
|
|
788
|
+
return negate ? { type: "not", child: node } : node;
|
|
789
|
+
}
|
|
790
|
+
if (!fieldValue && tokens[position] && !["AND", "OR", ")", "}"].includes(tokens[position].value.toUpperCase())) {
|
|
791
|
+
fieldValue = tokens[position++].value;
|
|
792
|
+
}
|
|
793
|
+
node = { type: "term", field, value: fieldValue, exact: token.quoted };
|
|
794
|
+
} else {
|
|
795
|
+
node = { type: "term", value: value.startsWith("+") ? value.slice(1) : value, exact: token.quoted || value.startsWith("+") };
|
|
796
|
+
}
|
|
797
|
+
return negate ? { type: "not", child: node } : node;
|
|
798
|
+
};
|
|
799
|
+
const parseFieldGroup = (field, depth) => {
|
|
800
|
+
if (depth > MAX_DEPTH)
|
|
801
|
+
invalidArgument("Search query nesting exceeds limit");
|
|
802
|
+
const opening = tokens[position++].value;
|
|
803
|
+
const end = opening === "(" ? ")" : "}";
|
|
804
|
+
const groups = [[]];
|
|
805
|
+
while (position < tokens.length && tokens[position].value !== end) {
|
|
806
|
+
const token = tokens[position++];
|
|
807
|
+
if (token.value.toUpperCase() === "OR") {
|
|
808
|
+
groups.push([]);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (token.value.toUpperCase() === "AND")
|
|
812
|
+
continue;
|
|
813
|
+
if (["(", "{", ")", "}"].includes(token.value))
|
|
814
|
+
invalidArgument("Nested field groups are unsupported");
|
|
815
|
+
let value = token.value;
|
|
816
|
+
let negate = false;
|
|
817
|
+
if (value.startsWith("-")) {
|
|
818
|
+
negate = true;
|
|
819
|
+
value = value.slice(1);
|
|
820
|
+
}
|
|
821
|
+
const term = { type: "term", field, value, exact: token.quoted };
|
|
822
|
+
groups.at(-1).push(negate ? { type: "not", child: term } : term);
|
|
823
|
+
}
|
|
824
|
+
if (tokens[position]?.value !== end)
|
|
825
|
+
invalidArgument(`Unclosed ${opening}`);
|
|
826
|
+
position++;
|
|
827
|
+
const nodes = groups.map((children) => children.length === 1 ? children[0] : { type: "and", children });
|
|
828
|
+
const useOr = opening === "{" || nodes.length > 1;
|
|
829
|
+
return useOr ? { type: "or", children: nodes } : nodes[0] ?? { type: "and", children: [] };
|
|
830
|
+
};
|
|
831
|
+
const root = parseExpression(0);
|
|
832
|
+
if (position !== tokens.length)
|
|
833
|
+
invalidArgument("Unexpected search token");
|
|
834
|
+
if (countBranches(root) > MAX_BRANCHES)
|
|
835
|
+
invalidArgument("Search query has too many branches");
|
|
836
|
+
return root;
|
|
837
|
+
}
|
|
838
|
+
function assertHasOperator(value) {
|
|
839
|
+
const normalized = value.toLocaleLowerCase("en-US");
|
|
840
|
+
if (normalized.endsWith("-star")) {
|
|
841
|
+
invalidArgument(`Unsupported colored-star operator: has:${value}; twin maps only STARRED via is:starred`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function parseDate(value) {
|
|
845
|
+
const normalized = /^\d{4}\/\d{1,2}\/\d{1,2}$/.test(value) ? value.replaceAll("/", "-") : value;
|
|
846
|
+
const date = Date.parse(`${normalized}${/^\d{4}-\d/.test(normalized) ? "T00:00:00Z" : ""}`);
|
|
847
|
+
if (Number.isNaN(date))
|
|
848
|
+
invalidArgument(`Invalid search date: ${value}`);
|
|
849
|
+
return date;
|
|
850
|
+
}
|
|
851
|
+
function parseDuration(value) {
|
|
852
|
+
const match = value.match(/^(\d+)([dmy])$/i);
|
|
853
|
+
if (!match)
|
|
854
|
+
invalidArgument(`Invalid search duration: ${value}`);
|
|
855
|
+
const units = { d: 864e5, m: 30 * 864e5, y: 365 * 864e5 };
|
|
856
|
+
return Number(match[1]) * units[match[2].toLowerCase()];
|
|
857
|
+
}
|
|
858
|
+
function parseSize(value) {
|
|
859
|
+
const match = value.match(/^(\d+(?:\.\d+)?)([kmg])?$/i);
|
|
860
|
+
if (!match)
|
|
861
|
+
invalidArgument(`Invalid search size: ${value}`);
|
|
862
|
+
const scale = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 };
|
|
863
|
+
return Math.floor(Number(match[1]) * (match[2] ? scale[match[2].toLowerCase()] : 1));
|
|
864
|
+
}
|
|
865
|
+
function countBranches(node) {
|
|
866
|
+
switch (node.type) {
|
|
867
|
+
case "and":
|
|
868
|
+
case "or":
|
|
869
|
+
return node.children.reduce((sum, child) => sum + countBranches(child), 0);
|
|
870
|
+
case "not":
|
|
871
|
+
return countBranches(node.child);
|
|
872
|
+
case "around":
|
|
873
|
+
case "term":
|
|
874
|
+
return 1;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function tokenize(query) {
|
|
878
|
+
const out = [];
|
|
879
|
+
let index = 0;
|
|
880
|
+
while (index < query.length) {
|
|
881
|
+
if (/\s/.test(query[index])) {
|
|
882
|
+
index++;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const char = query[index];
|
|
886
|
+
if ("(){}".includes(char)) {
|
|
887
|
+
out.push({ value: char });
|
|
888
|
+
index++;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
let value = "";
|
|
892
|
+
let quoted = false;
|
|
893
|
+
while (index < query.length && !/\s/.test(query[index]) && !"(){}".includes(query[index])) {
|
|
894
|
+
if (query[index] === '"') {
|
|
895
|
+
quoted = true;
|
|
896
|
+
index++;
|
|
897
|
+
while (index < query.length && query[index] !== '"') {
|
|
898
|
+
if (query[index] === "\\" && index + 1 < query.length)
|
|
899
|
+
index++;
|
|
900
|
+
value += query[index++];
|
|
901
|
+
}
|
|
902
|
+
if (query[index] !== '"')
|
|
903
|
+
invalidArgument("Unclosed search quote");
|
|
904
|
+
index++;
|
|
905
|
+
} else {
|
|
906
|
+
value += query[index++];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (value)
|
|
910
|
+
out.push({ value, quoted });
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
function makeImplicitOr(node) {
|
|
915
|
+
return node.type === "and" ? { type: "or", children: node.children } : node;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ../twin-gmail/dist/src/seed.js
|
|
919
|
+
var email = z.string().trim().email().transform((value) => value.toLowerCase());
|
|
920
|
+
var id = z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/);
|
|
921
|
+
var attachmentSchema = z.object({
|
|
922
|
+
filename: z.string().max(512),
|
|
923
|
+
mimeType: z.string().min(1).max(255).default("application/octet-stream"),
|
|
924
|
+
disposition: z.enum(["attachment", "inline"]).default("attachment"),
|
|
925
|
+
contentId: z.string().max(998).optional(),
|
|
926
|
+
data: z.string().max(5e7)
|
|
927
|
+
}).strict();
|
|
928
|
+
var messageFields = {
|
|
929
|
+
id: id.optional(),
|
|
930
|
+
threadId: id.optional(),
|
|
931
|
+
raw: z.string().max(5e7).optional(),
|
|
932
|
+
from: email.optional(),
|
|
933
|
+
to: z.array(email).max(500).default([]),
|
|
934
|
+
cc: z.array(email).max(500).default([]),
|
|
935
|
+
bcc: z.array(email).max(500).default([]),
|
|
936
|
+
subject: z.string().max(998).default(""),
|
|
937
|
+
text: z.string().max(25e6).default(""),
|
|
938
|
+
html: z.string().max(25e6).default(""),
|
|
939
|
+
date: z.string().datetime({ offset: true }).optional(),
|
|
940
|
+
messageId: z.string().min(3).max(998).optional(),
|
|
941
|
+
inReplyTo: z.string().max(998).optional(),
|
|
942
|
+
references: z.array(z.string().max(998)).max(100).default([]),
|
|
943
|
+
attachments: z.array(attachmentSchema).max(100).default([])
|
|
944
|
+
};
|
|
945
|
+
var messageSchema = z.object({
|
|
946
|
+
...messageFields,
|
|
947
|
+
labels: z.array(z.string().min(1).max(255)).max(100).default([])
|
|
948
|
+
}).strict();
|
|
949
|
+
var draftSchema = z.object(messageFields).strict();
|
|
950
|
+
var labelSchema = z.object({
|
|
951
|
+
id: id.optional(),
|
|
952
|
+
name: z.string().trim().min(1).max(225),
|
|
953
|
+
color: z.object({
|
|
954
|
+
textColor: z.string().max(32).optional(),
|
|
955
|
+
backgroundColor: z.string().max(32).optional()
|
|
956
|
+
}).strict().optional()
|
|
957
|
+
}).strict();
|
|
958
|
+
var filterSchema = z.object({
|
|
959
|
+
id: id.optional(),
|
|
960
|
+
criteria: z.object({
|
|
961
|
+
from: z.string().max(998).optional(),
|
|
962
|
+
to: z.string().max(998).optional(),
|
|
963
|
+
subject: z.string().max(998).optional(),
|
|
964
|
+
query: z.string().max(4096).optional(),
|
|
965
|
+
negatedQuery: z.string().max(4096).optional(),
|
|
966
|
+
hasAttachment: z.boolean().optional(),
|
|
967
|
+
excludeChats: z.boolean().optional(),
|
|
968
|
+
size: z.number().int().nonnegative().optional(),
|
|
969
|
+
sizeComparison: z.enum(["larger", "smaller"]).optional()
|
|
970
|
+
}).strict().default({}),
|
|
971
|
+
action: z.object({
|
|
972
|
+
addLabelIds: z.array(z.string().min(1)).max(100).default([]),
|
|
973
|
+
removeLabelIds: z.array(z.string().min(1)).max(100).default([]),
|
|
974
|
+
forward: email.optional()
|
|
975
|
+
}).strict().default({ addLabelIds: [], removeLabelIds: [] })
|
|
976
|
+
}).strict();
|
|
977
|
+
var sendAsSchema = z.object({
|
|
978
|
+
sendAsEmail: email,
|
|
979
|
+
displayName: z.string().max(256).default(""),
|
|
980
|
+
replyToAddress: email.optional(),
|
|
981
|
+
isPrimary: z.boolean().default(false),
|
|
982
|
+
isDefault: z.boolean().default(false),
|
|
983
|
+
verificationStatus: z.enum(["accepted", "pending"]).default("accepted")
|
|
984
|
+
}).strict();
|
|
985
|
+
var mailboxSchema = z.object({
|
|
986
|
+
email,
|
|
987
|
+
displayName: z.string().max(256).default(""),
|
|
988
|
+
labels: z.array(labelSchema).max(5e3).default([]),
|
|
989
|
+
messages: z.array(messageSchema).max(1e4).default([]),
|
|
990
|
+
drafts: z.array(draftSchema).max(5e3).default([]),
|
|
991
|
+
filters: z.array(filterSchema).max(1e3).default([]),
|
|
992
|
+
forwardingAddresses: z.array(z.object({
|
|
993
|
+
forwardingEmail: email,
|
|
994
|
+
verificationStatus: z.enum(["accepted", "pending"]).default("pending")
|
|
995
|
+
}).strict()).max(1e3).default([]),
|
|
996
|
+
sendAs: z.array(sendAsSchema).max(1e3).default([])
|
|
997
|
+
}).strict();
|
|
998
|
+
var gmailSeedSchema = z.object({
|
|
999
|
+
primaryMailbox: mailboxSchema,
|
|
1000
|
+
mailboxes: z.array(mailboxSchema).max(100).default([]),
|
|
1001
|
+
deliveryMode: z.enum(["sender-only", "seeded-mailboxes"]).default("sender-only"),
|
|
1002
|
+
clock: z.string().datetime({ offset: true }).default("2025-01-01T00:00:00.000Z"),
|
|
1003
|
+
faults: z.array(gmailFaultSchema).max(50).default([])
|
|
1004
|
+
}).strict().superRefine((seed, ctx) => {
|
|
1005
|
+
const emails = [seed.primaryMailbox.email, ...seed.mailboxes.map((mailbox) => mailbox.email)];
|
|
1006
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1007
|
+
for (const mailboxEmail of emails) {
|
|
1008
|
+
if (seen.has(mailboxEmail)) {
|
|
1009
|
+
ctx.addIssue({ code: "custom", message: `Duplicate mailbox: ${mailboxEmail}` });
|
|
1010
|
+
}
|
|
1011
|
+
seen.add(mailboxEmail);
|
|
1012
|
+
}
|
|
1013
|
+
for (const mailbox of [seed.primaryMailbox, ...seed.mailboxes]) {
|
|
1014
|
+
const labelNames = /* @__PURE__ */ new Set();
|
|
1015
|
+
for (const label of mailbox.labels) {
|
|
1016
|
+
const key = label.name.toLowerCase();
|
|
1017
|
+
if (labelNames.has(key)) {
|
|
1018
|
+
ctx.addIssue({ code: "custom", message: `Duplicate label in ${mailbox.email}: ${label.name}` });
|
|
1019
|
+
}
|
|
1020
|
+
labelNames.add(key);
|
|
1021
|
+
}
|
|
1022
|
+
for (const filter of mailbox.filters) {
|
|
1023
|
+
if (filter.action.forward) {
|
|
1024
|
+
ctx.addIssue({
|
|
1025
|
+
code: "custom",
|
|
1026
|
+
message: `Filter forwarding is unsupported: ${mailbox.email}`
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
for (const key of ["query", "negatedQuery"]) {
|
|
1030
|
+
const value = filter.criteria[key];
|
|
1031
|
+
if (!value)
|
|
1032
|
+
continue;
|
|
1033
|
+
try {
|
|
1034
|
+
validateSearchQuery(value);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
ctx.addIssue({
|
|
1037
|
+
code: "custom",
|
|
1038
|
+
message: `Invalid filter ${key} in ${mailbox.email}: ${error.message}`
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
function parseSeed(input) {
|
|
1046
|
+
return gmailSeedSchema.parse(input);
|
|
1047
|
+
}
|
|
1048
|
+
var DEFAULT_GMAIL_AGENT_EMAIL = "pome-agent@pome-twin.test";
|
|
1049
|
+
function agentPathInboxMailbox(email2 = DEFAULT_GMAIL_AGENT_EMAIL) {
|
|
1050
|
+
return {
|
|
1051
|
+
email: email2,
|
|
1052
|
+
displayName: "Pome Agent",
|
|
1053
|
+
labels: [
|
|
1054
|
+
{ id: "Label_follow_up", name: "Follow Up" },
|
|
1055
|
+
{ id: "Label_build", name: "Build" }
|
|
1056
|
+
],
|
|
1057
|
+
messages: [
|
|
1058
|
+
{
|
|
1059
|
+
id: "msg_welcome",
|
|
1060
|
+
threadId: "thread_welcome",
|
|
1061
|
+
from: "welcome@pome-twin.test",
|
|
1062
|
+
to: [email2],
|
|
1063
|
+
subject: "Welcome to your Pome Gmail twin",
|
|
1064
|
+
text: "Your deterministic inbox is ready for agent testing.",
|
|
1065
|
+
html: "<p>Your deterministic inbox is ready for agent testing.</p>",
|
|
1066
|
+
date: "2026-07-18T09:00:00.000Z",
|
|
1067
|
+
messageId: "welcome@pome-twin.test",
|
|
1068
|
+
labels: ["INBOX"]
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
id: "msg_build",
|
|
1072
|
+
threadId: "thread_build",
|
|
1073
|
+
from: "ci@example.com",
|
|
1074
|
+
to: [email2],
|
|
1075
|
+
subject: "Build failed on main",
|
|
1076
|
+
text: "The nightly build failed. See the attached log.",
|
|
1077
|
+
date: "2026-07-19T10:00:00.000Z",
|
|
1078
|
+
messageId: "build-001@example.com",
|
|
1079
|
+
labels: ["INBOX", "UNREAD", "Build"],
|
|
1080
|
+
attachments: [
|
|
1081
|
+
{
|
|
1082
|
+
filename: "build.log",
|
|
1083
|
+
mimeType: "text/plain",
|
|
1084
|
+
data: Buffer.from("BUILD FAILED step=test\n", "utf8").toString("base64")
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
id: "msg_build_reply",
|
|
1090
|
+
threadId: "thread_build",
|
|
1091
|
+
from: email2,
|
|
1092
|
+
to: ["ci@example.com"],
|
|
1093
|
+
subject: "Re: Build failed on main",
|
|
1094
|
+
text: "Looking into the failure now.",
|
|
1095
|
+
date: "2026-07-19T11:00:00.000Z",
|
|
1096
|
+
messageId: "build-reply@pome-twin.test",
|
|
1097
|
+
inReplyTo: "build-001@example.com",
|
|
1098
|
+
references: ["build-001@example.com"],
|
|
1099
|
+
labels: ["SENT"]
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
id: "msg_support",
|
|
1103
|
+
threadId: "thread_support",
|
|
1104
|
+
from: "alice@example.com",
|
|
1105
|
+
to: [email2],
|
|
1106
|
+
subject: "Production export is stuck",
|
|
1107
|
+
text: "Our production export has been stuck for an hour. Can you investigate?",
|
|
1108
|
+
date: "2026-07-19T12:00:00.000Z",
|
|
1109
|
+
messageId: "support-001@example.com",
|
|
1110
|
+
labels: ["INBOX", "UNREAD"]
|
|
1111
|
+
}
|
|
1112
|
+
],
|
|
1113
|
+
drafts: [
|
|
1114
|
+
{
|
|
1115
|
+
id: "draft_ack",
|
|
1116
|
+
threadId: "thread_draft_ack",
|
|
1117
|
+
to: ["bob@example.com"],
|
|
1118
|
+
subject: "Draft acknowledgment",
|
|
1119
|
+
text: "Thanks \u2014 I'll follow up shortly.",
|
|
1120
|
+
date: "2026-07-19T13:00:00.000Z",
|
|
1121
|
+
messageId: "draft-ack@pome-twin.test"
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
filters: [],
|
|
1125
|
+
forwardingAddresses: [],
|
|
1126
|
+
sendAs: []
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function defaultSeedState() {
|
|
1130
|
+
return {
|
|
1131
|
+
primaryMailbox: agentPathInboxMailbox(),
|
|
1132
|
+
mailboxes: [],
|
|
1133
|
+
deliveryMode: "sender-only",
|
|
1134
|
+
clock: "2026-07-20T00:00:00.000Z"
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
export { GMAIL_CHECKS, defaultSeedState, gmailSeedSchema, parseSeed };
|