paperlint 6.2.3 → 7.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.
- package/CONTRIBUTING.md +4 -2
- package/README.md +10 -3
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- package/dist/adapters/latex/conditionals.d.ts +21 -0
- package/dist/adapters/latex/conditionals.d.ts.map +1 -0
- package/dist/adapters/latex/conditionals.js +75 -0
- package/dist/adapters/latex/conditionals.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +10 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -0
- package/dist/adapters/latex/headings.js +129 -0
- package/dist/adapters/latex/headings.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +3 -2
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/heading-case.d.ts +100 -0
- package/dist/domain/heading-case.d.ts.map +1 -0
- package/dist/domain/heading-case.js +365 -0
- package/dist/domain/heading-case.js.map +1 -0
- package/dist/domain/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.js.map +1 -0
- package/dist/domain/tex-document.d.ts +32 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +11 -0
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +16 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/facts-file.d.ts +3 -3
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +3 -3
- package/dist/facts-file.js.map +1 -1
- package/dist/folder-venue-rule.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- package/dist/heading-case.d.ts +48 -0
- package/dist/heading-case.d.ts.map +1 -0
- package/dist/heading-case.js +191 -0
- package/dist/heading-case.js.map +1 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- package/dist/preset-card-rule.d.ts +100 -0
- package/dist/preset-card-rule.d.ts.map +1 -0
- package/dist/preset-card-rule.js +221 -0
- package/dist/preset-card-rule.js.map +1 -0
- package/dist/presets.d.ts +43 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +130 -63
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +36 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +30 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -8
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +30 -11
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +64 -53
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-checks.mjs +31 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +8 -2
- package/presets/acm-sigconf.jsonc +5 -0
- package/presets/acm-sigconf.md +43 -0
- package/presets/agenticdev.jsonc +27 -6
- package/presets/agenticdev.md +151 -0
- package/presets/aidc.jsonc +12 -0
- package/presets/aidc.md +75 -0
- package/presets/aisec.jsonc +5 -1
- package/presets/aisec.md +117 -0
- package/presets/ieee-conference.jsonc +5 -0
- package/presets/ieee-conference.md +38 -0
- package/presets/realm.jsonc +7 -2
- package/presets/realm.md +166 -0
- package/presets/tex/agenticdev.tex +1 -1
- package/presets/tex-base.jsonc +6 -0
- package/presets/tex-base.md +30 -0
- package/presets/venue-profile.schema.json +57 -2
- package/skills/find-venue/SKILL.md +6 -5
- package/skills/find-venue/SKILL.md.spec.ts +5 -4
- package/skills/paper-pipeline/SKILL.md +5 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +27 -22
- package/skills/submit-paper/SKILL.md.spec.ts +26 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- package/src/adapters/latex/conditionals.ts +130 -0
- package/src/adapters/latex/headings.ts +163 -0
- package/src/adapters/latex/index.test.ts +65 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +3 -2
- package/src/build.ts +2 -2
- package/src/cli.test.ts +155 -3
- package/src/cli.ts +161 -25
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +121 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +135 -10
- package/src/new-paper.ts +38 -7
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/latex.ts +7 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +212 -37
- package/src/presets.ts +183 -71
- package/src/rule-cases.test.ts +147 -42
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +497 -0
- package/src/submission.ts +311 -0
- package/src/talk-rules.test.ts +6 -3
- package/src/tex-requirements.harness.ts +25 -11
- package/src/tex-requirements.test.ts +127 -1
- package/src/tex-requirements.ts +78 -8
- package/src/tex-venue-rules.test.ts +36 -22
- package/src/tex-venue-rules.ts +14 -26
- package/src/types.ts +13 -0
- package/src/venue-rules.test.ts +7 -6
- package/templates/paper/paper.tex +1 -1
- package/skills/submit-paper/references/venues/agenticdev.md +0 -171
- package/skills/submit-paper/references/venues/aisec.md +0 -105
- package/skills/submit-paper/references/venues/realm.md +0 -155
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint submission` over in-memory files and a recording portal: where the portal and the
|
|
3
|
+
* submission id come from, the refusals that name the file and key to set, the token rule, and what
|
|
4
|
+
* `show` and `update` print and exit with. The HotCRP wire format is tested in the adapter.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
|
+
import { nodeFiles } from "./adapters/node/index.ts";
|
|
10
|
+
import type { Files } from "./ports/files.ts";
|
|
11
|
+
import { ok, err, type Result } from "./domain/result.ts";
|
|
12
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
13
|
+
import type {
|
|
14
|
+
PortalFailure,
|
|
15
|
+
SubmissionChange,
|
|
16
|
+
SubmissionView,
|
|
17
|
+
SupportedPortal,
|
|
18
|
+
UpdateOutcome,
|
|
19
|
+
} from "./domain/submission.ts";
|
|
20
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
21
|
+
import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
22
|
+
import {
|
|
23
|
+
failureText,
|
|
24
|
+
runSubmission,
|
|
25
|
+
submissionTarget,
|
|
26
|
+
tokenFor,
|
|
27
|
+
usageProblem,
|
|
28
|
+
type SubmissionArgs,
|
|
29
|
+
} from "./submission.ts";
|
|
30
|
+
import { venuePreset } from "../test/support.ts";
|
|
31
|
+
|
|
32
|
+
const PAPER = "/w/papers/p";
|
|
33
|
+
const SITE = "http://127.0.0.1:9";
|
|
34
|
+
const PDF = new TextEncoder().encode("%PDF fake");
|
|
35
|
+
const HEX = sha256Hex(PDF);
|
|
36
|
+
|
|
37
|
+
/** The test's files, over the disk for what they do not hold: the shipped presets and their schema. */
|
|
38
|
+
const withPresets = (fs: Files): Files => ({
|
|
39
|
+
...fs,
|
|
40
|
+
isFile: (p) => fs.isFile(p) || nodeFiles.isFile(p),
|
|
41
|
+
readBytes: (p) => fs.readBytes(p) ?? nodeFiles.readBytes(p),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const venue = (portal: unknown): string =>
|
|
45
|
+
JSON.stringify(venuePreset("venue", { extends: "paperlint:aidc", portal }));
|
|
46
|
+
const files = (o: {
|
|
47
|
+
readonly portal?: unknown;
|
|
48
|
+
readonly paper?: Readonly<Record<string, unknown>>;
|
|
49
|
+
readonly extra?: Readonly<Record<string, string | Uint8Array>>;
|
|
50
|
+
}) =>
|
|
51
|
+
memoryFiles({
|
|
52
|
+
"/w/package.json": "{}",
|
|
53
|
+
"/w/venue.jsonc": venue(o.portal ?? { kind: "hotcrp", url: `${SITE}/` }),
|
|
54
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify(
|
|
55
|
+
o.paper ?? { extends: "../../venue.jsonc", submission: { id: 7 } },
|
|
56
|
+
),
|
|
57
|
+
[`${PAPER}/paper.pdf`]: PDF,
|
|
58
|
+
...o.extra,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const VIEW: SubmissionView = {
|
|
62
|
+
id: 7,
|
|
63
|
+
status: "submitted",
|
|
64
|
+
title: "A Fake Title",
|
|
65
|
+
paperType: "Regular",
|
|
66
|
+
topics: ["A", "B"],
|
|
67
|
+
abstract: "one two three\nfour",
|
|
68
|
+
pdf: {
|
|
69
|
+
sha256: HEX,
|
|
70
|
+
hash: `sha2-${HEX}`,
|
|
71
|
+
size: PDF.length,
|
|
72
|
+
mimetype: "application/pdf",
|
|
73
|
+
uploadedAt: 0,
|
|
74
|
+
},
|
|
75
|
+
submittedAt: 60,
|
|
76
|
+
modifiedAt: 120,
|
|
77
|
+
messages: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface Recorded {
|
|
81
|
+
readonly portal: SupportedPortal;
|
|
82
|
+
readonly token: string;
|
|
83
|
+
readonly calls: unknown[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fakePortal = (answers: {
|
|
87
|
+
readonly show?: Result<SubmissionView, PortalFailure>;
|
|
88
|
+
readonly update?: Result<UpdateOutcome, PortalFailure>;
|
|
89
|
+
}) => {
|
|
90
|
+
const seen: Recorded[] = [];
|
|
91
|
+
const portalFor = (
|
|
92
|
+
portal: SupportedPortal,
|
|
93
|
+
token: string,
|
|
94
|
+
): SubmissionPortal => {
|
|
95
|
+
const r: Recorded = { portal, token, calls: [] };
|
|
96
|
+
seen.push(r);
|
|
97
|
+
return {
|
|
98
|
+
show: (id) => {
|
|
99
|
+
r.calls.push(["show", id]);
|
|
100
|
+
return Promise.resolve(answers.show ?? ok(VIEW));
|
|
101
|
+
},
|
|
102
|
+
update: (id, change: SubmissionChange, o) => {
|
|
103
|
+
r.calls.push(["update", id, change, o]);
|
|
104
|
+
return Promise.resolve(
|
|
105
|
+
answers.update ??
|
|
106
|
+
ok({
|
|
107
|
+
httpStatus: 200,
|
|
108
|
+
valid: true,
|
|
109
|
+
changes: ["submission"],
|
|
110
|
+
messages: [],
|
|
111
|
+
dryRun: !o.save,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
return { seen, portalFor };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const ARGS: SubmissionArgs = {
|
|
121
|
+
sub: "show",
|
|
122
|
+
paperDir: "papers/p",
|
|
123
|
+
extra: [],
|
|
124
|
+
pdf: null,
|
|
125
|
+
abstract: null,
|
|
126
|
+
submit: false,
|
|
127
|
+
save: false,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
async function run(
|
|
131
|
+
a: Partial<SubmissionArgs>,
|
|
132
|
+
o: {
|
|
133
|
+
readonly files?: Files;
|
|
134
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
135
|
+
readonly portal?: ReturnType<typeof fakePortal>;
|
|
136
|
+
} = {},
|
|
137
|
+
) {
|
|
138
|
+
const out: string[] = [];
|
|
139
|
+
const errs: string[] = [];
|
|
140
|
+
const portal = o.portal ?? fakePortal({});
|
|
141
|
+
const fs = withPresets(o.files ?? files({}));
|
|
142
|
+
const code = await runSubmission(
|
|
143
|
+
{ ...ARGS, ...a },
|
|
144
|
+
{
|
|
145
|
+
files: fs,
|
|
146
|
+
presets: { files: fs, venuesDir: presetsDir() },
|
|
147
|
+
cwd: "/w",
|
|
148
|
+
env: (n) => (o.env ?? { HOTCRP_TOKEN: "hct_secret" })[n],
|
|
149
|
+
portalFor: portal.portalFor,
|
|
150
|
+
log: (l) => out.push(l),
|
|
151
|
+
err: (l) => errs.push(l),
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
return { code, out, errs, seen: portal.seen };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test("show: the portal from the preset, the id from the paper, the token from HOTCRP_TOKEN", async () => {
|
|
158
|
+
const r = await run({});
|
|
159
|
+
assert.equal(r.code, 0);
|
|
160
|
+
assert.deepEqual(r.errs, []);
|
|
161
|
+
assert.deepEqual(r.seen, [
|
|
162
|
+
{
|
|
163
|
+
portal: { kind: "hotcrp", url: SITE },
|
|
164
|
+
token: "hct_secret",
|
|
165
|
+
calls: [["show", 7]],
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
assert.deepEqual(r.out, [
|
|
169
|
+
`submission 7 on ${SITE} (hotcrp)`,
|
|
170
|
+
"status submitted, submitted 1970-01-01T00:01:00.000Z",
|
|
171
|
+
"title A Fake Title",
|
|
172
|
+
"paper type Regular",
|
|
173
|
+
"topics A; B",
|
|
174
|
+
"abstract 4 words",
|
|
175
|
+
`portal PDF sha2-${HEX}, ${String(PDF.length)} bytes, uploaded 1970-01-01T00:00:00.000Z`,
|
|
176
|
+
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
177
|
+
"match yes — the portal holds this build",
|
|
178
|
+
]);
|
|
179
|
+
// Guards: the token is an input to the adapter and nothing printed carries it.
|
|
180
|
+
assert.doesNotMatch([...r.out, ...r.errs].join("\n"), /hct_secret/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("show: a different PDF on the portal is reported as NO, and still exits 0", async () => {
|
|
184
|
+
const other = {
|
|
185
|
+
...VIEW,
|
|
186
|
+
pdf: VIEW.pdf && { ...VIEW.pdf, sha256: sha256Hex(new Uint8Array([1])) },
|
|
187
|
+
};
|
|
188
|
+
const r = await run({}, { portal: fakePortal({ show: ok(other) }) });
|
|
189
|
+
assert.equal(r.code, 0);
|
|
190
|
+
assert.equal(
|
|
191
|
+
r.out.at(-1),
|
|
192
|
+
"match NO — the portal holds a different PDF than the local one",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("show: no PDF on the portal, a hash that is not sha256, no local PDF, portal messages", async () => {
|
|
197
|
+
const bare: SubmissionView = {
|
|
198
|
+
...VIEW,
|
|
199
|
+
title: null,
|
|
200
|
+
paperType: null,
|
|
201
|
+
topics: [],
|
|
202
|
+
abstract: null,
|
|
203
|
+
pdf: null,
|
|
204
|
+
submittedAt: null,
|
|
205
|
+
messages: [
|
|
206
|
+
{ message: "Not yet submitted", field: "status", status: 1 },
|
|
207
|
+
{ message: "Draft", field: null, status: null },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
const r1 = await run({}, { portal: fakePortal({ show: ok(bare) }) });
|
|
211
|
+
assert.deepEqual(r1.out.slice(1), [
|
|
212
|
+
"status submitted",
|
|
213
|
+
"title —",
|
|
214
|
+
"paper type —",
|
|
215
|
+
"topics —",
|
|
216
|
+
"abstract —",
|
|
217
|
+
"portal PDF none",
|
|
218
|
+
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
219
|
+
"match NO — the portal holds no PDF",
|
|
220
|
+
" portal says: status: Not yet submitted",
|
|
221
|
+
" portal says: Draft",
|
|
222
|
+
]);
|
|
223
|
+
const sha1 = {
|
|
224
|
+
...VIEW,
|
|
225
|
+
pdf: {
|
|
226
|
+
hash: "sha1-abc",
|
|
227
|
+
sha256: null,
|
|
228
|
+
size: null,
|
|
229
|
+
mimetype: null,
|
|
230
|
+
uploadedAt: null,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const r2 = await run({}, { portal: fakePortal({ show: ok(sha1) }) });
|
|
234
|
+
assert.equal(r2.out[6], "portal PDF sha1-abc, ? bytes, uploaded —");
|
|
235
|
+
assert.equal(
|
|
236
|
+
r2.out[8],
|
|
237
|
+
"match unknown — the portal names a sha1 hash, not a sha256",
|
|
238
|
+
);
|
|
239
|
+
const r4 = await run({ pdf: "/elsewhere/x.pdf" });
|
|
240
|
+
assert.equal(r4.out[7], "local PDF /elsewhere/x.pdf (missing)");
|
|
241
|
+
const r3 = await run({ pdf: "missing.pdf" });
|
|
242
|
+
assert.equal(r3.out[7], "local PDF missing.pdf (missing)");
|
|
243
|
+
assert.equal(r3.out[8], "match unknown — no local PDF to compare");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("🔴 the portal's refusal is printed with its own message, exit 1", async () => {
|
|
247
|
+
const refused = err<PortalFailure>({
|
|
248
|
+
kind: "refused",
|
|
249
|
+
httpStatus: 401,
|
|
250
|
+
messages: [{ message: "Missing credentials", field: null, status: 2 }],
|
|
251
|
+
});
|
|
252
|
+
const r = await run({}, { portal: fakePortal({ show: refused }) });
|
|
253
|
+
assert.equal(r.code, 1);
|
|
254
|
+
assert.deepEqual(r.errs, [
|
|
255
|
+
"the portal refused (HTTP 401): Missing credentials",
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("failureText: each failure in words", () => {
|
|
260
|
+
assert.equal(
|
|
261
|
+
failureText({ kind: "refused", httpStatus: 500, messages: [] }),
|
|
262
|
+
"the portal refused (HTTP 500)",
|
|
263
|
+
);
|
|
264
|
+
assert.equal(
|
|
265
|
+
failureText({ kind: "unreachable", detail: "fetch failed" }),
|
|
266
|
+
"the portal cannot be reached: fetch failed",
|
|
267
|
+
);
|
|
268
|
+
assert.equal(
|
|
269
|
+
failureText({
|
|
270
|
+
kind: "malformed",
|
|
271
|
+
httpStatus: 200,
|
|
272
|
+
detail: "the answer is not JSON (text/html)",
|
|
273
|
+
}),
|
|
274
|
+
"the portal answered HTTP 200, but not as its API does: the answer is not JSON (text/html)",
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("update: a dry run by default — the paper's PDF sent, nothing saved, exit 0", async () => {
|
|
279
|
+
const r = await run({ sub: "update" });
|
|
280
|
+
assert.equal(r.code, 0);
|
|
281
|
+
assert.deepEqual(r.seen[0]?.calls, [
|
|
282
|
+
[
|
|
283
|
+
"update",
|
|
284
|
+
7,
|
|
285
|
+
{ pdf: { name: "paper.pdf", bytes: PDF }, abstract: null, submit: false },
|
|
286
|
+
{ save: false },
|
|
287
|
+
],
|
|
288
|
+
]);
|
|
289
|
+
assert.deepEqual(r.out, [
|
|
290
|
+
"dry run — the portal checked the change and kept nothing; --save sends it for real",
|
|
291
|
+
"HTTP 200",
|
|
292
|
+
"valid yes",
|
|
293
|
+
"changes submission",
|
|
294
|
+
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
295
|
+
]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("update --save --submit --abstract --pdf: all of it sent, and said to be saved", async () => {
|
|
299
|
+
const fs = files({
|
|
300
|
+
extra: { "/w/abs.txt": " The abstract.\n", "/w/other.pdf": PDF },
|
|
301
|
+
});
|
|
302
|
+
const r = await run(
|
|
303
|
+
{
|
|
304
|
+
sub: "update",
|
|
305
|
+
save: true,
|
|
306
|
+
submit: true,
|
|
307
|
+
abstract: "abs.txt",
|
|
308
|
+
pdf: "other.pdf",
|
|
309
|
+
},
|
|
310
|
+
{ files: fs },
|
|
311
|
+
);
|
|
312
|
+
assert.equal(r.code, 0);
|
|
313
|
+
assert.deepEqual(r.seen[0]?.calls, [
|
|
314
|
+
[
|
|
315
|
+
"update",
|
|
316
|
+
7,
|
|
317
|
+
{
|
|
318
|
+
pdf: { name: "other.pdf", bytes: PDF },
|
|
319
|
+
abstract: "The abstract.",
|
|
320
|
+
submit: true,
|
|
321
|
+
},
|
|
322
|
+
{ save: true },
|
|
323
|
+
],
|
|
324
|
+
]);
|
|
325
|
+
assert.equal(r.out[0], "saved");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
|
|
329
|
+
const invalid = ok<UpdateOutcome>({
|
|
330
|
+
httpStatus: 200,
|
|
331
|
+
valid: false,
|
|
332
|
+
changes: [],
|
|
333
|
+
messages: [{ message: "Bad PDF", field: "submission", status: 2 }],
|
|
334
|
+
dryRun: true,
|
|
335
|
+
});
|
|
336
|
+
const r1 = await run(
|
|
337
|
+
{ sub: "update" },
|
|
338
|
+
{ portal: fakePortal({ update: invalid }) },
|
|
339
|
+
);
|
|
340
|
+
assert.equal(r1.code, 1);
|
|
341
|
+
assert.deepEqual(r1.out.slice(2), [
|
|
342
|
+
"valid NO",
|
|
343
|
+
"changes —",
|
|
344
|
+
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
345
|
+
" portal says: submission: Bad PDF",
|
|
346
|
+
]);
|
|
347
|
+
const status = ok<UpdateOutcome>({
|
|
348
|
+
httpStatus: 400,
|
|
349
|
+
valid: true,
|
|
350
|
+
changes: [],
|
|
351
|
+
messages: [],
|
|
352
|
+
dryRun: false,
|
|
353
|
+
});
|
|
354
|
+
assert.equal(
|
|
355
|
+
(await run({ sub: "update" }, { portal: fakePortal({ update: status }) }))
|
|
356
|
+
.code,
|
|
357
|
+
1,
|
|
358
|
+
);
|
|
359
|
+
const unreachable = err<PortalFailure>({
|
|
360
|
+
kind: "unreachable",
|
|
361
|
+
detail: "down",
|
|
362
|
+
});
|
|
363
|
+
const r3 = await run(
|
|
364
|
+
{ sub: "update" },
|
|
365
|
+
{ portal: fakePortal({ update: unreachable }) },
|
|
366
|
+
);
|
|
367
|
+
assert.equal(r3.code, 1);
|
|
368
|
+
assert.deepEqual(r3.errs, ["the portal cannot be reached: down"]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("update --save that the portal withheld says so", async () => {
|
|
372
|
+
const withheld = ok<UpdateOutcome>({
|
|
373
|
+
httpStatus: 200,
|
|
374
|
+
valid: true,
|
|
375
|
+
changes: [],
|
|
376
|
+
messages: [],
|
|
377
|
+
dryRun: true,
|
|
378
|
+
});
|
|
379
|
+
const r = await run(
|
|
380
|
+
{ sub: "update", save: true },
|
|
381
|
+
{ portal: fakePortal({ update: withheld }) },
|
|
382
|
+
);
|
|
383
|
+
assert.equal(r.out[0], "SAVE WITHHELD by the portal — nothing changed");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
|
|
387
|
+
const r1 = await run({ sub: "update", pdf: "nope.pdf" });
|
|
388
|
+
assert.equal(r1.code, 2);
|
|
389
|
+
assert.deepEqual(r1.errs, [
|
|
390
|
+
"no PDF at nope.pdf — build the paper, or name one with --pdf",
|
|
391
|
+
]);
|
|
392
|
+
assert.deepEqual(r1.seen[0]?.calls, []);
|
|
393
|
+
const r2 = await run({ sub: "update", abstract: "nope.txt" });
|
|
394
|
+
assert.equal(r2.code, 2);
|
|
395
|
+
assert.deepEqual(r2.errs, ["--abstract: no file at nope.txt"]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("🔴 no token: the variable is named, with where to make one — and nothing is called", async () => {
|
|
399
|
+
const r = await run({}, { env: {} });
|
|
400
|
+
assert.equal(r.code, 2);
|
|
401
|
+
assert.deepEqual(r.seen, []);
|
|
402
|
+
assert.match(
|
|
403
|
+
r.errs[0] ?? "",
|
|
404
|
+
/^HOTCRP_TOKEN is not set — .*Account settings → Developer/,
|
|
405
|
+
);
|
|
406
|
+
assert.deepEqual(
|
|
407
|
+
tokenFor({ kind: "hotcrp", url: SITE }, () => " "),
|
|
408
|
+
{
|
|
409
|
+
ok: false,
|
|
410
|
+
error: r.errs[0],
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("🔴 the file and key to set: no portal in the preset, no submission in the paper, no preset", () => {
|
|
416
|
+
const deps = (fs: Files) => ({
|
|
417
|
+
files: withPresets(fs),
|
|
418
|
+
venuesDir: presetsDir(),
|
|
419
|
+
});
|
|
420
|
+
const noPortal = memoryFiles({
|
|
421
|
+
"/w/package.json": "{}",
|
|
422
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
423
|
+
extends: "paperlint:ieee-conference",
|
|
424
|
+
submission: { id: 7 },
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
const t1 = submissionTarget(PAPER, deps(noPortal));
|
|
428
|
+
assert.match(
|
|
429
|
+
!t1.ok ? t1.error : "",
|
|
430
|
+
/^the venue preset declares no "portal" \(read: .*ieee-conference\.jsonc\) — add "portal": \{ "kind": "hotcrp"/,
|
|
431
|
+
);
|
|
432
|
+
const noId = files({ paper: { extends: "../../venue.jsonc" } });
|
|
433
|
+
assert.deepEqual(submissionTarget(PAPER, deps(noId)), {
|
|
434
|
+
ok: false,
|
|
435
|
+
error: `${PAPER}/paperlint.json: no "submission" — add "submission": { "id": <the submission number on ${SITE}/> }`,
|
|
436
|
+
});
|
|
437
|
+
const noExtends = files({ paper: { submission: { id: 7 } } });
|
|
438
|
+
const t3 = submissionTarget(PAPER, deps(noExtends));
|
|
439
|
+
assert.match(!t3.ok ? t3.error : "", /paperlint\.json: no "extends"/);
|
|
440
|
+
const broken = files({
|
|
441
|
+
paper: { extends: "../../venue.jsonc", submission: { id: "7" } },
|
|
442
|
+
});
|
|
443
|
+
const t4 = submissionTarget(PAPER, deps(broken));
|
|
444
|
+
assert.match(!t4.ok ? t4.error : "", /"submission" must be \{ "id"/);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("🔴 an unknown portal kind is refused: upload by hand", async () => {
|
|
448
|
+
const r = await run(
|
|
449
|
+
{},
|
|
450
|
+
{ files: files({ portal: { kind: "easychair", url: "https://x.test" } }) },
|
|
451
|
+
);
|
|
452
|
+
assert.equal(r.code, 2);
|
|
453
|
+
assert.deepEqual(r.errs, [
|
|
454
|
+
`portal kind "easychair" is not supported; upload by hand`,
|
|
455
|
+
]);
|
|
456
|
+
assert.deepEqual(r.seen, []);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("the shipped aidc preset declares its HotCRP portal", () => {
|
|
460
|
+
const fs = memoryFiles({
|
|
461
|
+
"/w/package.json": "{}",
|
|
462
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
463
|
+
extends: "paperlint:aidc",
|
|
464
|
+
submission: { id: 7 },
|
|
465
|
+
}),
|
|
466
|
+
});
|
|
467
|
+
const t = submissionTarget(PAPER, {
|
|
468
|
+
files: withPresets(fs),
|
|
469
|
+
venuesDir: presetsDir(),
|
|
470
|
+
});
|
|
471
|
+
assert.equal(t.ok && t.value.portal.kind, "hotcrp");
|
|
472
|
+
assert.match(t.ok ? t.value.portal.url : "", /^https:\/\//);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("usage: refused before anything is read, exit 2", async () => {
|
|
476
|
+
const r = await run({ sub: "upload" });
|
|
477
|
+
assert.equal(r.code, 2);
|
|
478
|
+
assert.deepEqual(r.seen, []);
|
|
479
|
+
assert.match(r.errs[0] ?? "", /got `upload`/);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("usage: a subcommand is required, one paper folder, update-only flags refused on show", () => {
|
|
483
|
+
assert.match(
|
|
484
|
+
usageProblem({ ...ARGS, sub: undefined }) ?? "",
|
|
485
|
+
/needs a subcommand: show or update — got ``/,
|
|
486
|
+
);
|
|
487
|
+
assert.match(usageProblem({ ...ARGS, sub: "upload" }) ?? "", /got `upload`/);
|
|
488
|
+
assert.equal(
|
|
489
|
+
usageProblem({ ...ARGS, extra: ["x"] }),
|
|
490
|
+
"`submission show` takes one paper folder — got also `x`",
|
|
491
|
+
);
|
|
492
|
+
assert.equal(
|
|
493
|
+
usageProblem({ ...ARGS, save: true, submit: true, abstract: "a" }),
|
|
494
|
+
"--abstract, --submit, --save: only `submission update` changes anything; `show` reads",
|
|
495
|
+
);
|
|
496
|
+
assert.equal(usageProblem({ ...ARGS, sub: "update", save: true }), null);
|
|
497
|
+
});
|