paperlint 7.1.0 → 7.2.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.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +6 -0
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +40 -5
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cycle-rules.d.ts +24 -0
  20. package/dist/cycle-rules.d.ts.map +1 -0
  21. package/dist/cycle-rules.js +212 -0
  22. package/dist/cycle-rules.js.map +1 -0
  23. package/dist/domain/cycle.d.ts +148 -0
  24. package/dist/domain/cycle.d.ts.map +1 -0
  25. package/dist/domain/cycle.js +281 -0
  26. package/dist/domain/cycle.js.map +1 -0
  27. package/dist/domain/deadline.d.ts +128 -0
  28. package/dist/domain/deadline.d.ts.map +1 -0
  29. package/dist/domain/deadline.js +281 -0
  30. package/dist/domain/deadline.js.map +1 -0
  31. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  32. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  33. package/dist/eslint-rules/cycle-stage.js +119 -0
  34. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  35. package/dist/lib/paper-config.d.ts.map +1 -1
  36. package/dist/lib/paper-config.js +2 -0
  37. package/dist/lib/paper-config.js.map +1 -1
  38. package/dist/paper-settings.d.ts +7 -0
  39. package/dist/paper-settings.d.ts.map +1 -1
  40. package/dist/paper-settings.js +61 -8
  41. package/dist/paper-settings.js.map +1 -1
  42. package/dist/ports/venue-deadlines.d.ts +14 -0
  43. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  44. package/dist/ports/venue-deadlines.js +2 -0
  45. package/dist/ports/venue-deadlines.js.map +1 -0
  46. package/dist/presets.d.ts +12 -0
  47. package/dist/presets.d.ts.map +1 -1
  48. package/dist/presets.js +19 -11
  49. package/dist/presets.js.map +1 -1
  50. package/dist/submission.d.ts +8 -0
  51. package/dist/submission.d.ts.map +1 -1
  52. package/dist/submission.js +41 -10
  53. package/dist/submission.js.map +1 -1
  54. package/dist/tex-requirements.d.ts +3 -0
  55. package/dist/tex-requirements.d.ts.map +1 -1
  56. package/dist/tex-requirements.js +19 -0
  57. package/dist/tex-requirements.js.map +1 -1
  58. package/dist/tex-venue-rules.d.ts +19 -0
  59. package/dist/tex-venue-rules.d.ts.map +1 -1
  60. package/dist/tex-venue-rules.js +69 -0
  61. package/dist/tex-venue-rules.js.map +1 -1
  62. package/dist/venue-rules.d.ts +10 -0
  63. package/dist/venue-rules.d.ts.map +1 -1
  64. package/dist/venue-rules.js +26 -10
  65. package/dist/venue-rules.js.map +1 -1
  66. package/docs/configuration.md +139 -11
  67. package/docs/rules/cycle/evidence.md +87 -0
  68. package/docs/rules/cycle/port-done.md +75 -0
  69. package/docs/rules/cycle/record.md +152 -0
  70. package/docs/rules/cycle/stage.md +85 -0
  71. package/docs/rules.md +48 -43
  72. package/docs/submission.md +5 -0
  73. package/eslint-rules/cycle-stage.test.mjs +131 -0
  74. package/eslint-rules/cycle-stage.ts +141 -0
  75. package/eslint.config.mjs +41 -1
  76. package/lib/paper-config.ts +3 -0
  77. package/package.json +1 -1
  78. package/presets/aidc.jsonc +24 -3
  79. package/presets/aidc.md +6 -3
  80. package/presets/msr.jsonc +25 -1
  81. package/presets/msr.md +5 -1
  82. package/presets/venue-profile.schema.json +39 -0
  83. package/scripts/check.ts +4 -0
  84. package/scripts/refresh-deadlines.test.ts +431 -0
  85. package/scripts/refresh-deadlines.ts +387 -0
  86. package/src/adapters/hotcrp/cassette/README.md +22 -0
  87. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  88. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  89. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  90. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  91. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  92. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  93. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  94. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  95. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  96. package/src/adapters/hotcrp/deadlines.ts +161 -0
  97. package/src/adapters/hotcrp/index.ts +2 -0
  98. package/src/cli.test.ts +9 -0
  99. package/src/cli.ts +43 -4
  100. package/src/cycle-rules.test.ts +278 -0
  101. package/src/cycle-rules.ts +273 -0
  102. package/src/domain/cycle.test.ts +456 -0
  103. package/src/domain/cycle.ts +432 -0
  104. package/src/domain/deadline.test.ts +386 -0
  105. package/src/domain/deadline.ts +472 -0
  106. package/src/new-paper.test.ts +1 -0
  107. package/src/paper-settings.test.ts +167 -0
  108. package/src/paper-settings.ts +85 -11
  109. package/src/ports/venue-deadlines.ts +14 -0
  110. package/src/presets.test.ts +105 -0
  111. package/src/presets.ts +43 -10
  112. package/src/rule-cases.test.ts +144 -0
  113. package/src/submission.test.ts +105 -1
  114. package/src/submission.ts +73 -12
  115. package/src/tex-requirements.ts +31 -0
  116. package/src/tex-venue-rules.test.ts +76 -0
  117. package/src/tex-venue-rules.ts +85 -0
  118. package/src/venue-rules.test.ts +82 -0
  119. package/src/venue-rules.ts +44 -12
@@ -0,0 +1,431 @@
1
+ /**
2
+ * The scheduled refresh of the shipped presets' deadlines, on the HotCRP pages recorded in
3
+ * `src/adapters/hotcrp/cassette/` — never the network. Both halves: an unchanged portal leaves the
4
+ * preset's bytes alone (no pull request every day), a moved deadline rewrites that reading and its
5
+ * `read` day only; a portal that cannot be read is REPORTED and does not fail the run.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import {
11
+ mkdtempSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import {
17
+ createServer,
18
+ type IncomingMessage,
19
+ type Server,
20
+ type ServerResponse,
21
+ } from "node:http";
22
+ import { tmpdir } from "node:os";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
26
+ import { parseDeadlinesPage } from "../src/adapters/hotcrp/index.ts";
27
+ import { err } from "../src/domain/result.ts";
28
+ import { parsePreset } from "../src/tex-requirements.ts";
29
+ import {
30
+ argsOf,
31
+ main,
32
+ refreshPreset,
33
+ refreshReadings,
34
+ withDeadlines,
35
+ type Outcome,
36
+ } from "./refresh-deadlines.ts";
37
+
38
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
39
+ const PRESETS = join(ROOT, "presets");
40
+ const CASSETTE = join(ROOT, "src", "adapters", "hotcrp", "cassette");
41
+ const MSR = join(PRESETS, "msr.jsonc");
42
+ const msrText = readFileSync(MSR, "utf8");
43
+
44
+ const recorded =
45
+ (site: string, status = 200) =>
46
+ async () =>
47
+ Promise.resolve(
48
+ parseDeadlinesPage(
49
+ status,
50
+ readFileSync(join(CASSETTE, `deadlines-${site}.html`), "utf8"),
51
+ ),
52
+ );
53
+
54
+ const readings = (text: string, file = MSR) =>
55
+ parsePreset(text, file).deadlines ?? [];
56
+
57
+ /** The outcome, known to be of `kind` — or the test fails naming what it was. */
58
+ function as<K extends Outcome["kind"]>(
59
+ o: Outcome,
60
+ kind: K,
61
+ ): Extract<Outcome, { kind: K }> {
62
+ const isKind = (x: Outcome): x is Extract<Outcome, { kind: K }> =>
63
+ x.kind === kind;
64
+ if (!isKind(o)) throw new Error(`expected ${kind}, got ${JSON.stringify(o)}`);
65
+ return o;
66
+ }
67
+
68
+ /** Each site's recorded page, by the URL the preset names. */
69
+ const bySite = (url: string) =>
70
+ url.includes("acsac") ? recorded("aidc26")() : recorded("msr2027")();
71
+
72
+ describe("refreshPreset on the recorded MSR 2027 page", () => {
73
+ test("the portal says what the preset says: unchanged — not one byte rewritten, read days kept", async () => {
74
+ const o = await refreshPreset({
75
+ file: MSR,
76
+ text: msrText,
77
+ read: recorded("msr2027"),
78
+ today: "2026-10-06",
79
+ });
80
+ expect(o).toEqual({
81
+ kind: "unchanged",
82
+ url: "https://msr2027.hotcrp.com/deadlines",
83
+ notes: [],
84
+ });
85
+ });
86
+
87
+ test("the portal moved the submission: that reading changes, with today's read day; the resubmission keeps its own", async () => {
88
+ const earlier = msrText.replace(
89
+ '"at": "2026-10-20T04:00:00Z"',
90
+ '"at": "2026-10-19T04:00:00Z"',
91
+ );
92
+ const o = as(
93
+ await refreshPreset({
94
+ file: MSR,
95
+ text: earlier,
96
+ read: recorded("msr2027"),
97
+ today: "2026-10-06",
98
+ }),
99
+ "changed",
100
+ );
101
+ expect(o.changes).toEqual([
102
+ "submission: 2026-10-19T04:00:00Z → 2026-10-20T04:00:00Z",
103
+ ]);
104
+ expect(readings(o.text).map((r) => [r.what, r.at, r.read])).toEqual([
105
+ ["submission", "2026-10-20T04:00:00Z", "2026-10-06"],
106
+ ["resubmission", "2026-10-23T04:00:00Z", "2026-10-05"],
107
+ ]);
108
+ // Only the deadlines block differs: the comments and every other key are the file's own.
109
+ expect(o.text).toBe(
110
+ msrText.replace(
111
+ '"read": "2026-10-05",\n },\n {\n "what": "resubmission"',
112
+ '"read": "2026-10-06",\n },\n {\n "what": "resubmission"',
113
+ ),
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("refreshPreset — a preset with no readings yet, and the ones it skips", () => {
119
+ test("a preset with a portal and no `deadlines`: the block is inserted after `portal`, and parses", async () => {
120
+ const start = msrText.indexOf(" // Read from the portal");
121
+ const end = msrText.indexOf(" ],\n", start) + " ],\n".length;
122
+ const bare = msrText.slice(0, start) + msrText.slice(end);
123
+ const o = as(
124
+ await refreshPreset({
125
+ file: MSR,
126
+ text: bare,
127
+ read: recorded("msr2027"),
128
+ today: "2026-10-05",
129
+ }),
130
+ "changed",
131
+ );
132
+ expect(o.changes).toEqual([
133
+ "submission: added 2026-10-20T04:00:00Z",
134
+ "resubmission: added 2026-10-23T04:00:00Z",
135
+ ]);
136
+ expect(readings(o.text)).toEqual(readings(msrText));
137
+ });
138
+
139
+ test("a preset without a portal, or with one paperlint has no adapter for: nothing to read", async () => {
140
+ const aisec = join(PRESETS, "aisec.jsonc");
141
+ const none = await refreshPreset({
142
+ file: aisec,
143
+ text: readFileSync(aisec, "utf8"),
144
+ read: recorded("msr2027"),
145
+ today: "2026-10-05",
146
+ });
147
+ expect(none).toEqual({ kind: "skipped", why: "no portal" });
148
+ const easychair = await refreshPreset({
149
+ file: MSR,
150
+ text: msrText.replace('"kind": "hotcrp"', '"kind": "easychair"'),
151
+ read: recorded("msr2027"),
152
+ today: "2026-10-05",
153
+ });
154
+ expect(easychair).toEqual({
155
+ kind: "skipped",
156
+ why: 'portal kind "easychair" is not supported; upload by hand',
157
+ });
158
+ });
159
+ });
160
+
161
+ describe("refreshPreset — a portal that cannot be read is reported, never guessed", () => {
162
+ test.each<[string, () => Promise<Outcome>, RegExp]>([
163
+ [
164
+ "a site that answers 404",
165
+ () =>
166
+ refreshPreset({
167
+ file: MSR,
168
+ text: msrText,
169
+ read: recorded("nosuch", 404),
170
+ today: "2026-10-05",
171
+ }),
172
+ /^HTTP 404: No such conference$/,
173
+ ],
174
+ [
175
+ "a site that does not answer",
176
+ () =>
177
+ refreshPreset({
178
+ file: MSR,
179
+ text: msrText,
180
+ read: () =>
181
+ Promise.resolve(
182
+ err({ kind: "unreachable", detail: "connect ECONNREFUSED" }),
183
+ ),
184
+ today: "2026-10-05",
185
+ }),
186
+ /^unreachable: connect ECONNREFUSED$/,
187
+ ],
188
+ [
189
+ "a page that is not a HotCRP deadlines page",
190
+ () =>
191
+ refreshPreset({
192
+ file: MSR,
193
+ text: msrText,
194
+ read: () =>
195
+ Promise.resolve(
196
+ err({ kind: "malformed", httpStatus: 200, detail: "no init" }),
197
+ ),
198
+ today: "2026-10-05",
199
+ }),
200
+ /^not the page expected \(HTTP 200\): no init$/,
201
+ ],
202
+ ])("%s", async (_what, run, why) => {
203
+ const o = as(await run(), "unreadable");
204
+ expect(o.url).toBe("https://msr2027.hotcrp.com/deadlines");
205
+ expect(o.why).toMatch(why);
206
+ });
207
+ });
208
+
209
+ const reading = (
210
+ what: "submission" | "resubmission" | "camera-ready",
211
+ at: string,
212
+ source: "portal" | "call" = "portal",
213
+ ) => ({
214
+ what,
215
+ at,
216
+ source,
217
+ url: "https://x.example/deadlines",
218
+ read: "2026-10-01",
219
+ });
220
+ const page = (
221
+ deadlines: readonly {
222
+ what: "submission" | "camera-ready" | null;
223
+ at: string;
224
+ }[],
225
+ ) => ({
226
+ now: 1,
227
+ open: true,
228
+ blind: true,
229
+ deadlines: deadlines.map((d) => ({ ...d, label: String(d.what) })),
230
+ });
231
+
232
+ describe("refreshReadings — what the page says, over what the preset said", () => {
233
+ test("a reading the portal no longer lists is kept and noted; a call's reading is never touched", () => {
234
+ const r = refreshReadings(
235
+ [
236
+ reading("submission", "2026-10-20T04:00:00Z"),
237
+ reading("submission", "2026-10-21T11:59:59Z", "call"),
238
+ reading("camera-ready", "2026-12-01T00:00:00Z"),
239
+ ],
240
+ page([{ what: "submission", at: "2026-10-20T04:00:00Z" }]),
241
+ "https://x.example/deadlines",
242
+ "2026-10-06",
243
+ );
244
+ expect(r.changes).toEqual([]);
245
+ expect(r.notes).toEqual([
246
+ "camera-ready: no longer on the page — kept as read 2026-10-01",
247
+ ]);
248
+ expect(r.readings).toEqual([
249
+ reading("submission", "2026-10-20T04:00:00Z"),
250
+ reading("submission", "2026-10-21T11:59:59Z", "call"),
251
+ reading("camera-ready", "2026-12-01T00:00:00Z"),
252
+ ]);
253
+ });
254
+
255
+ test("a kind the page lists twice binds at the earlier instant; a label paperlint names no kind for is left out", () => {
256
+ const r = refreshReadings(
257
+ [],
258
+ page([
259
+ { what: "submission", at: "2026-10-22T04:00:00Z" },
260
+ { what: "submission", at: "2026-10-20T04:00:00Z" },
261
+ { what: null, at: "2026-09-01T00:00:00Z" },
262
+ ]),
263
+ "https://x.example/deadlines",
264
+ "2026-10-06",
265
+ );
266
+ expect(r.readings).toEqual([
267
+ {
268
+ what: "submission",
269
+ at: "2026-10-20T04:00:00Z",
270
+ source: "portal",
271
+ url: "https://x.example/deadlines",
272
+ read: "2026-10-06",
273
+ },
274
+ ]);
275
+ expect(r.notes).toEqual([
276
+ "submission: the page lists 2026-10-20T04:00:00Z, 2026-10-22T04:00:00Z — the earliest is recorded",
277
+ ]);
278
+ });
279
+ });
280
+
281
+ describe("withDeadlines — located on the JSONC's syntax tree, not by text", () => {
282
+ test.each([
283
+ ["an object with no portal", "{}"],
284
+ ["a list", "[]"],
285
+ ["nothing", ""],
286
+ ])(
287
+ "%s has no `portal` to put `deadlines` after: refused by name",
288
+ async (_what, text) => {
289
+ await expect(withDeadlines(text, "x.jsonc", [])).rejects.toThrow(
290
+ 'x.jsonc: no "portal" to put "deadlines" after',
291
+ );
292
+ },
293
+ );
294
+
295
+ test("writing back the readings the file holds gives the file's own bytes", async () => {
296
+ expect(await withDeadlines(msrText, MSR, readings(msrText))).toBe(msrText);
297
+ });
298
+ });
299
+
300
+ // ── main, and the script as the workflow runs it ─────────────────────────────────────
301
+
302
+ const MSR_PAGE = readFileSync(join(CASSETTE, "deadlines-msr2027.html"), "utf8");
303
+ const server = createServer((req: IncomingMessage, res: ServerResponse) => {
304
+ const ok = req.url === "/msr/deadlines";
305
+ res.writeHead(ok ? 200 : 404, { "content-type": "text/html" });
306
+ res.end(ok ? MSR_PAGE : '<h1 id="h-title">No such conference</h1>');
307
+ });
308
+ const portOf = (s: Server): number => {
309
+ const a = s.address();
310
+ assert.ok(a !== null && typeof a === "object", "the server is not listening");
311
+ return a.port;
312
+ };
313
+ let base = "";
314
+ beforeAll(async () => {
315
+ await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
316
+ base = `http://127.0.0.1:${String(portOf(server))}`;
317
+ });
318
+ afterAll(
319
+ () =>
320
+ new Promise<void>((r) => {
321
+ server.close(() => {
322
+ r();
323
+ });
324
+ }),
325
+ );
326
+
327
+ /** A presets directory of the test's own: MSR pointing at the local server one day early, and a site that is gone. */
328
+ function presetsDirWith(): string {
329
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "deadlines-")));
330
+ const atServer = (path: string) =>
331
+ msrText.split("https://msr2027.hotcrp.com").join(`${base}/${path}`);
332
+ writeFileSync(
333
+ join(dir, "msr.jsonc"),
334
+ atServer("msr").replace(
335
+ '"at": "2026-10-20T04:00:00Z"',
336
+ '"at": "2026-10-19T04:00:00Z"',
337
+ ),
338
+ );
339
+ writeFileSync(join(dir, "gone.jsonc"), atServer("gone"));
340
+ writeFileSync(
341
+ join(dir, "aisec.jsonc"),
342
+ readFileSync(join(PRESETS, "aisec.jsonc")),
343
+ );
344
+ writeFileSync(join(dir, "notes.md"), "not a preset\n");
345
+ return dir;
346
+ }
347
+
348
+ describe("the script, as .github/workflows/deadlines.yml runs it", () => {
349
+ // A real `node` process: Node strips the types of the script and everything it imports (TypeScript's
350
+ // JSON parser, Prettier) cold — about a second alone, several on a loaded machine, past vitest's 5 s.
351
+ test(
352
+ "rewrites the moved deadline, reports the site it could not read, writes the summary — and exits 0",
353
+ {
354
+ timeout: 60_000,
355
+ },
356
+ async () => {
357
+ const dir = presetsDirWith();
358
+ const summary = join(dir, "summary.md");
359
+ // Asynchronously: the local server answers from THIS process, which a synchronous spawn blocks.
360
+ const r = await promisify(execFile)(process.execPath, [
361
+ join(ROOT, "scripts", "refresh-deadlines.ts"),
362
+ "--presets",
363
+ dir,
364
+ "--summary",
365
+ summary,
366
+ ]); // rejects on a non-zero exit, with the output
367
+ expect(
368
+ readings(readFileSync(join(dir, "msr.jsonc"), "utf8"))[0]?.at,
369
+ ).toBe("2026-10-20T04:00:00Z");
370
+ expect(r.stdout).toMatch(
371
+ /^::warning title=deadlines::gone\.jsonc: could not read http:\/\/127\.0\.0\.1:\d+\/gone\/deadlines — HTTP 404: No such conference$/m,
372
+ );
373
+ const md = readFileSync(summary, "utf8");
374
+ expect(md).toMatch(
375
+ /^- `msr\.jsonc` \(http:\/\/127\.0\.0\.1:\d+\/msr\/deadlines\)\n {2}- submission: 2026-10-19T04:00:00Z → 2026-10-20T04:00:00Z$/m,
376
+ );
377
+ expect(md).toMatch(/could not read/);
378
+ },
379
+ );
380
+ });
381
+
382
+ describe("main — what it prints and returns", () => {
383
+ test("nothing changed: one line, nothing written, exit 0", async () => {
384
+ const lines: string[] = [];
385
+ const written: string[] = [];
386
+ const code = await main({
387
+ presetsDir: PRESETS,
388
+ read: bySite,
389
+ today: "2026-10-06",
390
+ log: (l) => lines.push(l),
391
+ write: (f) => written.push(f),
392
+ summary: null,
393
+ });
394
+ expect(code).toBe(0);
395
+ expect(written).toEqual([]);
396
+ expect(lines.at(-1)).toMatch(
397
+ /^✓ no deadline changed: \d+ preset\(s\) with a portal read, \d+ skipped$/,
398
+ );
399
+ });
400
+
401
+ test("a preset that does not parse is a defect of the repository: exit 1, naming it", async () => {
402
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "deadlines-")));
403
+ writeFileSync(join(dir, "bad.jsonc"), "{ nope");
404
+ const lines: string[] = [];
405
+ const code = await main({
406
+ presetsDir: dir,
407
+ read: bySite,
408
+ today: "2026-10-06",
409
+ log: (l) => lines.push(l),
410
+ write: () => undefined,
411
+ summary: null,
412
+ });
413
+ expect(code).toBe(1);
414
+ expect(lines.join("\n")).toMatch(
415
+ /^::error title=deadlines::bad\.jsonc does not parse: /m,
416
+ );
417
+ });
418
+ });
419
+
420
+ describe("argsOf", () => {
421
+ test("defaults to the repository's presets and no summary; a flag without a value is no value", () => {
422
+ expect(argsOf([])).toEqual({
423
+ presets: join(ROOT, "presets"),
424
+ summary: null,
425
+ });
426
+ expect(argsOf(["--presets", "/p", "--summary"])).toEqual({
427
+ presets: "/p",
428
+ summary: null,
429
+ });
430
+ });
431
+ });