paperlint 10.4.0 → 11.0.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 (232) hide show
  1. package/.github/workflows/deadlines.yml +27 -7
  2. package/README.md +16 -15
  3. package/dist/CLAUDE.md.spec.d.ts.map +1 -1
  4. package/dist/CLAUDE.md.spec.js +6 -3
  5. package/dist/CLAUDE.md.spec.js.map +1 -1
  6. package/dist/adapters/hotcrp/index.d.ts +6 -4
  7. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  8. package/dist/adapters/hotcrp/index.js +6 -4
  9. package/dist/adapters/hotcrp/index.js.map +1 -1
  10. package/dist/adapters/hotcrp/portal.io.d.ts +3 -1
  11. package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -1
  12. package/dist/adapters/hotcrp/portal.io.js +36 -11
  13. package/dist/adapters/hotcrp/portal.io.js.map +1 -1
  14. package/dist/adapters/hotcrp/response.d.ts +6 -1
  15. package/dist/adapters/hotcrp/response.d.ts.map +1 -1
  16. package/dist/adapters/hotcrp/response.js +17 -0
  17. package/dist/adapters/hotcrp/response.js.map +1 -1
  18. package/dist/adapters/hotcrp/site.d.ts +9 -0
  19. package/dist/adapters/hotcrp/site.d.ts.map +1 -0
  20. package/dist/adapters/hotcrp/site.js +22 -0
  21. package/dist/adapters/hotcrp/site.js.map +1 -0
  22. package/dist/adapters/hotcrp/token.d.ts +13 -0
  23. package/dist/adapters/hotcrp/token.d.ts.map +1 -0
  24. package/dist/adapters/hotcrp/token.js +48 -0
  25. package/dist/adapters/hotcrp/token.js.map +1 -0
  26. package/dist/adapters/latex/abstract.d.ts +22 -0
  27. package/dist/adapters/latex/abstract.d.ts.map +1 -0
  28. package/dist/adapters/latex/abstract.js +187 -0
  29. package/dist/adapters/latex/abstract.js.map +1 -0
  30. package/dist/adapters/latex/captions.d.ts +9 -0
  31. package/dist/adapters/latex/captions.d.ts.map +1 -0
  32. package/dist/adapters/latex/captions.js +40 -0
  33. package/dist/adapters/latex/captions.js.map +1 -0
  34. package/dist/adapters/latex/headings.d.ts +6 -0
  35. package/dist/adapters/latex/headings.d.ts.map +1 -1
  36. package/dist/adapters/latex/headings.js +5 -4
  37. package/dist/adapters/latex/headings.js.map +1 -1
  38. package/dist/adapters/latex/index.d.ts +1 -0
  39. package/dist/adapters/latex/index.d.ts.map +1 -1
  40. package/dist/adapters/latex/index.js +3 -0
  41. package/dist/adapters/latex/index.js.map +1 -1
  42. package/dist/adapters/latex/parse.d.ts +15 -0
  43. package/dist/adapters/latex/parse.d.ts.map +1 -1
  44. package/dist/adapters/latex/parse.js +64 -1
  45. package/dist/adapters/latex/parse.js.map +1 -1
  46. package/dist/adapters/node/list-entries.io.d.ts.map +1 -1
  47. package/dist/adapters/node/list-entries.io.js +3 -1
  48. package/dist/adapters/node/list-entries.io.js.map +1 -1
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +20 -8
  51. package/dist/cli.js.map +1 -1
  52. package/dist/cycle-rules.d.ts.map +1 -1
  53. package/dist/cycle-rules.js +3 -4
  54. package/dist/cycle-rules.js.map +1 -1
  55. package/dist/domain/deadline.d.ts +8 -1
  56. package/dist/domain/deadline.d.ts.map +1 -1
  57. package/dist/domain/deadline.js +26 -12
  58. package/dist/domain/deadline.js.map +1 -1
  59. package/dist/domain/decide-first.d.ts +11 -1
  60. package/dist/domain/decide-first.d.ts.map +1 -1
  61. package/dist/domain/decide-first.js +6 -0
  62. package/dist/domain/decide-first.js.map +1 -1
  63. package/dist/domain/index-columns.d.ts.map +1 -1
  64. package/dist/domain/index-columns.js +2 -0
  65. package/dist/domain/index-columns.js.map +1 -1
  66. package/dist/domain/portal-token.d.ts +44 -0
  67. package/dist/domain/portal-token.d.ts.map +1 -0
  68. package/dist/domain/portal-token.js +32 -0
  69. package/dist/domain/portal-token.js.map +1 -0
  70. package/dist/domain/study-registry.d.ts +65 -8
  71. package/dist/domain/study-registry.d.ts.map +1 -1
  72. package/dist/domain/study-registry.js +63 -19
  73. package/dist/domain/study-registry.js.map +1 -1
  74. package/dist/domain/submission.d.ts +30 -0
  75. package/dist/domain/submission.d.ts.map +1 -1
  76. package/dist/domain/submission.js +68 -0
  77. package/dist/domain/submission.js.map +1 -1
  78. package/dist/lib/paper-config.d.ts +1 -1
  79. package/dist/lib/paper-config.d.ts.map +1 -1
  80. package/dist/lib/paper-config.js +6 -4
  81. package/dist/lib/paper-config.js.map +1 -1
  82. package/dist/outputs-before-decision-rule.d.ts +1 -1
  83. package/dist/outputs-before-decision-rule.d.ts.map +1 -1
  84. package/dist/outputs-before-decision-rule.js +21 -17
  85. package/dist/outputs-before-decision-rule.js.map +1 -1
  86. package/dist/paper-settings.d.ts +3 -1
  87. package/dist/paper-settings.d.ts.map +1 -1
  88. package/dist/paper-settings.js +11 -5
  89. package/dist/paper-settings.js.map +1 -1
  90. package/dist/ports/latex.d.ts +23 -0
  91. package/dist/ports/latex.d.ts.map +1 -1
  92. package/dist/ports/list-entries.d.ts +7 -4
  93. package/dist/ports/list-entries.d.ts.map +1 -1
  94. package/dist/ports/study-deps.d.ts +14 -0
  95. package/dist/ports/study-deps.d.ts.map +1 -0
  96. package/dist/ports/study-deps.js +2 -0
  97. package/dist/ports/study-deps.js.map +1 -0
  98. package/dist/ports/study-folder.d.ts +43 -0
  99. package/dist/ports/study-folder.d.ts.map +1 -0
  100. package/dist/ports/study-folder.js +2 -0
  101. package/dist/ports/study-folder.js.map +1 -0
  102. package/dist/ports/submission-portal.d.ts +39 -2
  103. package/dist/ports/submission-portal.d.ts.map +1 -1
  104. package/dist/ports/venue-deadlines.d.ts +1 -1
  105. package/dist/presets.d.ts +1 -2
  106. package/dist/presets.d.ts.map +1 -1
  107. package/dist/presets.js +1 -2
  108. package/dist/presets.js.map +1 -1
  109. package/dist/studies.d.ts.map +1 -1
  110. package/dist/studies.js +10 -7
  111. package/dist/studies.js.map +1 -1
  112. package/dist/study-folder.d.ts +14 -0
  113. package/dist/study-folder.d.ts.map +1 -0
  114. package/dist/study-folder.js +123 -0
  115. package/dist/study-folder.js.map +1 -0
  116. package/dist/study-gate.d.ts +1 -1
  117. package/dist/study-gate.d.ts.map +1 -1
  118. package/dist/study-gate.js +65 -28
  119. package/dist/study-gate.js.map +1 -1
  120. package/dist/study-index-input.d.ts +1 -1
  121. package/dist/study-index-input.d.ts.map +1 -1
  122. package/dist/study-index-input.js +8 -3
  123. package/dist/study-index-input.js.map +1 -1
  124. package/dist/study-modes.d.ts +38 -27
  125. package/dist/study-modes.d.ts.map +1 -1
  126. package/dist/study-modes.js +42 -53
  127. package/dist/study-modes.js.map +1 -1
  128. package/dist/study-registry-rule.d.ts +2 -6
  129. package/dist/study-registry-rule.d.ts.map +1 -1
  130. package/dist/study-registry-rule.js +32 -41
  131. package/dist/study-registry-rule.js.map +1 -1
  132. package/dist/submission.d.ts +49 -11
  133. package/dist/submission.d.ts.map +1 -1
  134. package/dist/submission.js +332 -44
  135. package/dist/submission.js.map +1 -1
  136. package/docs/configuration.md +2 -1
  137. package/docs/design/study-modes.md +46 -28
  138. package/docs/rules/cycle/record.md +9 -6
  139. package/docs/rules/repro/outputs-before-decision.md +3 -0
  140. package/docs/rules/repro/study-registered.md +3 -2
  141. package/docs/rules/tex/heading-case.md +1 -1
  142. package/docs/submission.md +47 -48
  143. package/eslint.config.mjs +39 -24
  144. package/lib/paper-config.ts +7 -4
  145. package/package.json +6 -1
  146. package/presets/msr.jsonc +9 -7
  147. package/presets/msr.md +14 -10
  148. package/scripts/check.ts +1 -1
  149. package/scripts/refresh-deadlines/compare.test.ts +75 -0
  150. package/scripts/refresh-deadlines/compare.ts +101 -0
  151. package/scripts/refresh-deadlines/main.test.ts +140 -0
  152. package/scripts/refresh-deadlines/main.ts +56 -0
  153. package/scripts/refresh-deadlines/refresh.test.ts +287 -0
  154. package/scripts/refresh-deadlines/refresh.ts +183 -0
  155. package/skills/grade-paper-writing/SKILL.md +4 -2
  156. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -1
  157. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +16 -3
  158. package/skills/grade-paper-writing/prose-lint.mjs +127 -95
  159. package/skills/grade-paper-writing/prose-lint.test.ts +119 -10
  160. package/skills/submission-portal/SKILL.md +60 -44
  161. package/skills/submission-portal/SKILL.md.spec.ts +62 -43
  162. package/skills/submission-portal/submission-portal.test.ts +4 -73
  163. package/src/CLAUDE.md +7 -4
  164. package/src/CLAUDE.md.spec.ts +6 -3
  165. package/src/adapters/hotcrp/cassette/README.md +4 -4
  166. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +9 -9
  167. package/src/adapters/hotcrp/deadlines.io.test.ts +2 -2
  168. package/src/adapters/hotcrp/deadlines.test.ts +3 -3
  169. package/src/adapters/hotcrp/index.ts +15 -3
  170. package/src/adapters/hotcrp/portal.io.ts +61 -17
  171. package/src/adapters/hotcrp/portal.test.ts +105 -1
  172. package/src/adapters/hotcrp/response.ts +21 -0
  173. package/src/adapters/hotcrp/site.ts +34 -0
  174. package/src/adapters/hotcrp/token.test.ts +69 -0
  175. package/src/adapters/hotcrp/token.ts +51 -0
  176. package/src/adapters/latex/abstract.test.ts +280 -0
  177. package/src/adapters/latex/abstract.ts +272 -0
  178. package/src/adapters/latex/captions.test.ts +57 -0
  179. package/src/adapters/latex/captions.ts +56 -0
  180. package/src/adapters/latex/headings.ts +5 -4
  181. package/src/adapters/latex/index.test.ts +31 -1
  182. package/src/adapters/latex/index.ts +3 -0
  183. package/src/adapters/latex/parse.ts +71 -3
  184. package/src/adapters/node/list-entries.io.ts +2 -1
  185. package/src/adapters/node/list-entries.test.ts +18 -0
  186. package/src/cli.test.ts +20 -8
  187. package/src/cli.ts +22 -13
  188. package/src/cycle-rules.test.ts +6 -6
  189. package/src/cycle-rules.ts +7 -4
  190. package/src/domain/deadline.test.ts +31 -2
  191. package/src/domain/deadline.ts +42 -16
  192. package/src/domain/decide-first.test.ts +33 -0
  193. package/src/domain/decide-first.ts +16 -1
  194. package/src/domain/index-columns.test.ts +49 -4
  195. package/src/domain/index-columns.ts +2 -0
  196. package/src/domain/portal-token.test.ts +55 -0
  197. package/src/domain/portal-token.ts +63 -0
  198. package/src/domain/study-measurements.test.ts +7 -3
  199. package/src/domain/study-registry.test.ts +34 -6
  200. package/src/domain/study-registry.ts +123 -31
  201. package/src/domain/submission.test.ts +88 -0
  202. package/src/domain/submission.ts +99 -0
  203. package/src/new-paper.test.ts +1 -0
  204. package/src/outputs-before-decision-rule.test.ts +47 -2
  205. package/src/outputs-before-decision-rule.ts +31 -22
  206. package/src/paper-settings.test.ts +20 -0
  207. package/src/paper-settings.ts +17 -5
  208. package/src/ports/latex.ts +25 -0
  209. package/src/ports/list-entries.ts +7 -4
  210. package/src/ports/study-deps.ts +14 -0
  211. package/src/ports/study-folder.ts +38 -0
  212. package/src/ports/submission-portal.ts +43 -1
  213. package/src/ports/venue-deadlines.ts +1 -1
  214. package/src/presets.test.ts +8 -5
  215. package/src/presets.ts +1 -2
  216. package/src/studies-index-modes.test.ts +2 -2
  217. package/src/studies.ts +19 -10
  218. package/src/study-folder.test.ts +237 -0
  219. package/src/study-folder.ts +176 -0
  220. package/src/study-gate.test.ts +158 -7
  221. package/src/study-gate.ts +127 -54
  222. package/src/study-index-input.ts +12 -4
  223. package/src/study-modes.ts +67 -80
  224. package/src/study-registry-rule.test.ts +87 -0
  225. package/src/study-registry-rule.ts +58 -66
  226. package/src/submission-new.test.ts +569 -0
  227. package/src/submission-show.test.ts +210 -0
  228. package/src/submission-update.test.ts +160 -0
  229. package/src/submission.test.ts +140 -518
  230. package/src/submission.ts +494 -61
  231. package/scripts/refresh-deadlines.test.ts +0 -431
  232. package/scripts/refresh-deadlines.ts +0 -387
@@ -0,0 +1,210 @@
1
+ /** `paperlint submission show`: what the portal holds, whether it is the local build, the deadlines in force. */
2
+ import { expect, test } from "vitest";
3
+ import { memoryFiles } from "./adapters/memory/index.ts";
4
+ import { err, ok } from "./domain/result.ts";
5
+ import { sha256Hex } from "./domain/sha256.ts";
6
+ import {
7
+ type PortalFailure,
8
+ type SubmissionView,
9
+ } from "./domain/submission.ts";
10
+ import { presetsDir } from "./package-dirs.ts";
11
+ import { deadlineLine, submissionTarget } from "./submission.ts";
12
+ import {
13
+ PAPER,
14
+ SITE,
15
+ PDF,
16
+ HEX,
17
+ withPresets,
18
+ VIEW,
19
+ fakePortal,
20
+ run,
21
+ STEPS,
22
+ } from "../test/submission-fixtures.ts";
23
+
24
+ test("show: the portal from the preset, the id from the paper, the token from the site's variable", async () => {
25
+ const r = await run({});
26
+ expect(r.code).toBe(0);
27
+ expect(r.errs).toStrictEqual([]);
28
+ expect(r.seen).toStrictEqual([
29
+ {
30
+ portal: { kind: "hotcrp", url: SITE },
31
+ token: "hct_secret",
32
+ calls: [["show", 7]],
33
+ },
34
+ ]);
35
+ expect(r.out).toStrictEqual([
36
+ `submission 7 on ${SITE} (hotcrp)`,
37
+ "status submitted, submitted 1970-01-01T00:01:00.000Z",
38
+ "title A Fake Title",
39
+ "paper type Regular",
40
+ "topics A; B",
41
+ "abstract 4 words",
42
+ `portal PDF sha2-${HEX}, ${String(PDF.length)} bytes, uploaded 1970-01-01T00:00:00.000Z`,
43
+ `local PDF papers/p/paper.pdf sha2-${HEX}`,
44
+ "match yes — the portal holds this build",
45
+ ]);
46
+ // Guards: the token is an input to the adapter and nothing printed carries it.
47
+ expect([...r.out, ...r.errs].join("\n")).not.toMatch(/hct_secret/);
48
+ });
49
+
50
+ test("show: a different PDF on the portal is reported as NO, and still exits 0", async () => {
51
+ const other = {
52
+ ...VIEW,
53
+ pdf: VIEW.pdf && { ...VIEW.pdf, sha256: sha256Hex(new Uint8Array([1])) },
54
+ };
55
+ const r = await run({}, { portal: fakePortal({ show: ok(other) }) });
56
+ expect(r.code).toBe(0);
57
+ expect(r.out.at(-1)).toBe(
58
+ "match NO — the portal holds a different PDF than the local one",
59
+ );
60
+ });
61
+
62
+ test("show: no PDF on the portal, a hash that is not sha256, no local PDF, portal messages", async () => {
63
+ const bare: SubmissionView = {
64
+ ...VIEW,
65
+ title: null,
66
+ paperType: null,
67
+ topics: [],
68
+ abstract: null,
69
+ pdf: null,
70
+ submittedAt: null,
71
+ messages: [
72
+ { message: "Not yet submitted", field: "status", status: 1 },
73
+ { message: "Draft", field: null, status: null },
74
+ ],
75
+ };
76
+ const r1 = await run({}, { portal: fakePortal({ show: ok(bare) }) });
77
+ expect(r1.out.slice(1)).toStrictEqual([
78
+ "status submitted",
79
+ "title —",
80
+ "paper type —",
81
+ "topics —",
82
+ "abstract —",
83
+ "portal PDF none",
84
+ `local PDF papers/p/paper.pdf sha2-${HEX}`,
85
+ "match NO — the portal holds no PDF",
86
+ " portal says: status: Not yet submitted",
87
+ " portal says: Draft",
88
+ ]);
89
+ const sha1 = {
90
+ ...VIEW,
91
+ pdf: {
92
+ hash: "sha1-abc",
93
+ sha256: null,
94
+ size: null,
95
+ mimetype: null,
96
+ uploadedAt: null,
97
+ },
98
+ };
99
+ const r2 = await run({}, { portal: fakePortal({ show: ok(sha1) }) });
100
+ expect(r2.out[6]).toBe("portal PDF sha1-abc, ? bytes, uploaded —");
101
+ expect(r2.out[8]).toBe(
102
+ "match unknown — the portal names a sha1 hash, not a sha256",
103
+ );
104
+ const r4 = await run({ pdf: "/elsewhere/x.pdf" });
105
+ expect(r4.out[7]).toBe("local PDF /elsewhere/x.pdf (missing)");
106
+ const r3 = await run({ pdf: "missing.pdf" });
107
+ expect(r3.out[7]).toBe("local PDF missing.pdf (missing)");
108
+ expect(r3.out[8]).toBe("match unknown — no local PDF to compare");
109
+ });
110
+
111
+ test("the portal's refusal is printed with its own message, exit 1", async () => {
112
+ const refused = err<PortalFailure>({
113
+ kind: "refused",
114
+ httpStatus: 401,
115
+ messages: [{ message: "Missing credentials", field: null, status: 2 }],
116
+ });
117
+ const r = await run({}, { portal: fakePortal({ show: refused }) });
118
+ expect(r.code).toBe(1);
119
+ expect(r.errs).toStrictEqual([
120
+ "the portal refused (HTTP 401): Missing credentials",
121
+ "HOTCRP_TOKEN_127_0_0_1_9 was used; it may belong to another hotcrp site (tokens are per site), or be revoked or expired",
122
+ ...STEPS,
123
+ ]);
124
+ });
125
+
126
+ test("show sends nothing, so it asks for no check", async () => {
127
+ const r = await run({});
128
+ expect(r.out.some((l) => l.includes("call for papers"))).toBe(false);
129
+ });
130
+
131
+ test("show prints the current attempt's deadlines in force: the preset's portal readings, the paper's call readings and override over them", () => {
132
+ const fs = memoryFiles({
133
+ "/w/package.json": "{}",
134
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
135
+ cycles: [
136
+ {
137
+ id: "msr-2027",
138
+ venue: { kind: "preset", extends: "paperlint:msr" },
139
+ kind: "technical",
140
+ opened: "2026-10-05",
141
+ submission: { id: 7 },
142
+ deadlines: [
143
+ {
144
+ what: "registration",
145
+ observed: [
146
+ {
147
+ at: "2026-10-20 AoE",
148
+ source: "call",
149
+ url: "https://2027.msrconf.org/cfp",
150
+ read: "2026-10-05",
151
+ },
152
+ ],
153
+ },
154
+ {
155
+ what: "submission",
156
+ observed: [
157
+ {
158
+ at: "2026-10-23 AoE",
159
+ source: "call",
160
+ url: "https://2027.msrconf.org/cfp",
161
+ read: "2026-10-05",
162
+ },
163
+ ],
164
+ },
165
+ {
166
+ what: "resubmission",
167
+ override: {
168
+ at: "2026-10-27T04:00:00Z",
169
+ reason: "the chairs extended updates by mail",
170
+ evidence: "mail/extension.eml",
171
+ },
172
+ },
173
+ ],
174
+ },
175
+ ],
176
+ }),
177
+ });
178
+ const t = submissionTarget(PAPER, {
179
+ files: withPresets(fs),
180
+ venuesDir: presetsDir(),
181
+ });
182
+ expect(t.ok ? t.value.deadlines.map(deadlineLine) : t).toStrictEqual([
183
+ "deadline registration 2026-10-21T11:59:59Z, the call's reading on 2026-10-05 from https://2027.msrconf.org/cfp",
184
+ "deadline submission 2026-10-21T11:59:59Z, the portal's reading on 2026-10-19 from https://msr2027.hotcrp.com/deadlines — venues move dates: re-check there before relying on it; also 2026-10-24T11:59:59Z, the call's reading on 2026-10-05 from https://2027.msrconf.org/cfp",
185
+ "deadline resubmission 2026-10-27T04:00:00Z — override: the chairs extended updates by mail (mail/extension.eml); also 2026-10-24T11:59:59Z, the portal's reading on 2026-10-19 from https://msr2027.hotcrp.com/deadlines — venues move dates: re-check there before relying on it",
186
+ ]);
187
+ });
188
+
189
+ test("show, in the flat form: the deadlines in force are the preset's readings alone", () => {
190
+ const flat = memoryFiles({
191
+ "/w/package.json": "{}",
192
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
193
+ extends: "paperlint:msr",
194
+ kind: "technical",
195
+ submission: { id: 7 },
196
+ }),
197
+ });
198
+ const f = submissionTarget(PAPER, {
199
+ files: withPresets(flat),
200
+ venuesDir: presetsDir(),
201
+ });
202
+ expect(f.ok ? f.value.deadlines.map((d) => [d.what, d.at]) : f).toStrictEqual(
203
+ [
204
+ ["submission", "2026-10-21T11:59:59Z"],
205
+ ["resubmission", "2026-10-24T11:59:59Z"],
206
+ ],
207
+ );
208
+ });
209
+
210
+ // ── new ───────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,160 @@
1
+ /** `paperlint submission update`: a dry run by default, what is sent, the portal's verdict and the exit. */
2
+ import { expect, test } from "vitest";
3
+ import { memoryFiles } from "./adapters/memory/index.ts";
4
+ import { err, ok } from "./domain/result.ts";
5
+ import { type PortalFailure, type UpdateOutcome } from "./domain/submission.ts";
6
+ import {
7
+ PAPER,
8
+ SITE,
9
+ PDF,
10
+ HEX,
11
+ files,
12
+ fakePortal,
13
+ CHECK,
14
+ run,
15
+ } from "../test/submission-fixtures.ts";
16
+
17
+ test("update: a dry run by default — the paper's PDF sent, nothing saved, exit 0", async () => {
18
+ const r = await run({ sub: "update" });
19
+ expect(r.code).toBe(0);
20
+ expect(r.seen[0]?.calls).toStrictEqual([
21
+ [
22
+ "update",
23
+ 7,
24
+ { pdf: { name: "paper.pdf", bytes: PDF }, abstract: null, submit: false },
25
+ { save: false },
26
+ ],
27
+ ]);
28
+ expect(r.out).toStrictEqual([
29
+ CHECK("papers/p/paper.pdf", "the call for papers, https://example.org/cfp"),
30
+ "dry run — the portal checked the change and kept nothing; --save sends it for real",
31
+ "HTTP 200",
32
+ "valid yes",
33
+ "changes submission",
34
+ `local PDF papers/p/paper.pdf sha2-${HEX}`,
35
+ ]);
36
+ });
37
+
38
+ test("update --save --submit --abstract --pdf: all of it sent, and said to be saved", async () => {
39
+ const fs = files({
40
+ extra: { "/w/abs.txt": " The abstract.\n", "/w/other.pdf": PDF },
41
+ });
42
+ const r = await run(
43
+ {
44
+ sub: "update",
45
+ save: true,
46
+ submit: true,
47
+ abstract: "abs.txt",
48
+ pdf: "other.pdf",
49
+ },
50
+ { files: fs },
51
+ );
52
+ expect(r.code).toBe(0);
53
+ expect(r.seen[0]?.calls).toStrictEqual([
54
+ [
55
+ "update",
56
+ 7,
57
+ {
58
+ pdf: { name: "other.pdf", bytes: PDF },
59
+ abstract: "The abstract.",
60
+ submit: true,
61
+ },
62
+ { save: true },
63
+ ],
64
+ ]);
65
+ // Guards: the PDF that is sent is the one named, and --save is told to check it too.
66
+ expect(r.out.slice(0, 2)).toStrictEqual([
67
+ CHECK("other.pdf", "the call for papers, https://example.org/cfp"),
68
+ "saved",
69
+ ]);
70
+ });
71
+
72
+ test("update: the check line names the venue's call from the preset's `url`; a preset that names no venue gets the generic line", async () => {
73
+ const family = memoryFiles({
74
+ "/w/package.json": "{}",
75
+ "/w/house.jsonc": JSON.stringify({
76
+ type: "family",
77
+ extends: "paperlint:ieee-conference",
78
+ portal: { kind: "hotcrp", url: `${SITE}/` },
79
+ }),
80
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
81
+ extends: "../../house.jsonc",
82
+ submission: { id: 7 },
83
+ }),
84
+ [`${PAPER}/paper.pdf`]: PDF,
85
+ });
86
+ const r = await run({ sub: "update" }, { files: family });
87
+ expect(r.code).toBe(0);
88
+ expect(r.out[0]).toBe(
89
+ CHECK("papers/p/paper.pdf", "the venue's call for papers"),
90
+ );
91
+ });
92
+
93
+ test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
94
+ const invalid = ok<UpdateOutcome>({
95
+ httpStatus: 200,
96
+ valid: false,
97
+ changes: [],
98
+ messages: [{ message: "Bad PDF", field: "submission", status: 2 }],
99
+ dryRun: true,
100
+ });
101
+ const r1 = await run(
102
+ { sub: "update" },
103
+ { portal: fakePortal({ update: invalid }) },
104
+ );
105
+ expect(r1.code).toBe(1);
106
+ expect(r1.out.slice(3)).toStrictEqual([
107
+ "valid NO",
108
+ "changes —",
109
+ `local PDF papers/p/paper.pdf sha2-${HEX}`,
110
+ " portal says: submission: Bad PDF",
111
+ ]);
112
+ const status = ok<UpdateOutcome>({
113
+ httpStatus: 400,
114
+ valid: true,
115
+ changes: [],
116
+ messages: [],
117
+ dryRun: false,
118
+ });
119
+ expect(
120
+ (await run({ sub: "update" }, { portal: fakePortal({ update: status }) }))
121
+ .code,
122
+ ).toBe(1);
123
+ const unreachable = err<PortalFailure>({
124
+ kind: "unreachable",
125
+ detail: "down",
126
+ });
127
+ const r3 = await run(
128
+ { sub: "update" },
129
+ { portal: fakePortal({ update: unreachable }) },
130
+ );
131
+ expect(r3.code).toBe(1);
132
+ expect(r3.errs).toStrictEqual(["the portal cannot be reached: down"]);
133
+ });
134
+
135
+ test("update --save that the portal withheld says so", async () => {
136
+ const withheld = ok<UpdateOutcome>({
137
+ httpStatus: 200,
138
+ valid: true,
139
+ changes: [],
140
+ messages: [],
141
+ dryRun: true,
142
+ });
143
+ const r = await run(
144
+ { sub: "update", save: true },
145
+ { portal: fakePortal({ update: withheld }) },
146
+ );
147
+ expect(r.out[1]).toBe("SAVE WITHHELD by the portal — nothing changed");
148
+ });
149
+
150
+ test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
151
+ const r1 = await run({ sub: "update", pdf: "nope.pdf" });
152
+ expect(r1.code).toBe(2);
153
+ expect(r1.errs).toStrictEqual([
154
+ "no PDF at nope.pdf — build the paper, or name one with --pdf",
155
+ ]);
156
+ expect(r1.seen[0]?.calls).toStrictEqual([]);
157
+ const r2 = await run({ sub: "update", abstract: "nope.txt" });
158
+ expect(r2.code).toBe(2);
159
+ expect(r2.errs).toStrictEqual(["--abstract: no file at nope.txt"]);
160
+ });