paperlint 11.1.0 → 11.1.2
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/.github/workflows/dependabot-automerge.yml +20 -6
- package/.github/workflows/release.yml +10 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +83 -7
- package/dist/cli.js.map +1 -1
- package/dist/submission-confirm-hook.d.ts +59 -0
- package/dist/submission-confirm-hook.d.ts.map +1 -0
- package/dist/submission-confirm-hook.js +230 -0
- package/dist/submission-confirm-hook.js.map +1 -0
- package/dist/submission.d.ts +13 -1
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +55 -7
- package/dist/submission.js.map +1 -1
- package/dist/venue-rules.d.ts +33 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +84 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/install.md +4 -4
- package/docs/layout.md +1 -0
- package/docs/submission.md +33 -1
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +4 -0
- package/scripts/refresh-deadlines/main.test.ts +5 -2
- package/scripts/refresh-deadlines/refresh.test.ts +15 -14
- package/skills/submission-portal/SKILL.md +11 -10
- package/skills/submission-portal/SKILL.md.spec.ts +10 -9
- package/src/cli.test.ts +56 -0
- package/src/cli.ts +100 -6
- package/src/cycle-rules.test.ts +14 -6
- package/src/hooks-settings.harness.ts +6 -6
- package/src/presets.test.ts +34 -24
- package/src/submission-confirm-hook.test.ts +399 -0
- package/src/submission-confirm-hook.ts +330 -0
- package/src/submission-fields-run.test.ts +1 -1
- package/src/submission-new.test.ts +29 -2
- package/src/submission-show.test.ts +9 -0
- package/src/submission-update.test.ts +153 -3
- package/src/submission.ts +85 -10
- package/src/venue-rules.test.ts +15 -0
- package/src/venue-rules.ts +126 -10
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `submission-confirm` hook: which commands send a submission (asked), which do not (silent), and
|
|
3
|
+
* what the person is told — including when what it reads is missing, stale or broken.
|
|
4
|
+
*/
|
|
5
|
+
import { expect, test } from "vitest";
|
|
6
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
7
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
8
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
9
|
+
import type { Files } from "./ports/files.ts";
|
|
10
|
+
import { leafCommandsNormalized as shellLeaves } from "vigiles/hook";
|
|
11
|
+
import { submissionSend } from "./cli.ts";
|
|
12
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
13
|
+
import {
|
|
14
|
+
submissionConfirm as confirmWith,
|
|
15
|
+
type ConfirmDeps,
|
|
16
|
+
} from "./submission-confirm-hook.ts";
|
|
17
|
+
import { HEX, PDF, facts, withPresets } from "../test/submission-fixtures.ts";
|
|
18
|
+
|
|
19
|
+
/** What the command sends, read through the real shell parser. */
|
|
20
|
+
const kind = (command: string, hosts: () => readonly string[]) => {
|
|
21
|
+
const reason =
|
|
22
|
+
submissionConfirm(payload(command), memoryFiles({}), {
|
|
23
|
+
portalHosts: hosts,
|
|
24
|
+
})?.hookSpecificOutput.permissionDecisionReason ?? null;
|
|
25
|
+
if (reason === null) return null;
|
|
26
|
+
if (reason.startsWith(HEAD)) return "paperlint";
|
|
27
|
+
return reason.startsWith("This command names") ? "direct" : reason;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The portal hosts the project declares, in these tests: one venue on its own domain. */
|
|
31
|
+
const HOSTS = ["submit.example-venue.org", "conference.org"];
|
|
32
|
+
|
|
33
|
+
/** The hook over `fs` (the shipped presets readable beside it), the project declaring `HOSTS`. */
|
|
34
|
+
const submissionConfirm = (
|
|
35
|
+
stdin: string,
|
|
36
|
+
fs: Files,
|
|
37
|
+
more: Partial<ConfirmDeps> = {},
|
|
38
|
+
) =>
|
|
39
|
+
confirmWith(stdin, {
|
|
40
|
+
files: withPresets(fs),
|
|
41
|
+
venuesDir: presetsDir(),
|
|
42
|
+
portalHosts: () => HOSTS,
|
|
43
|
+
shellLeaves,
|
|
44
|
+
readSend: submissionSend,
|
|
45
|
+
...more,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const payload = (command: string, cwd = "/w"): string =>
|
|
49
|
+
JSON.stringify({ tool_name: "Bash", tool_input: { command }, cwd });
|
|
50
|
+
|
|
51
|
+
test.each([
|
|
52
|
+
"npx paperlint submission update papers/p --save",
|
|
53
|
+
"npx paperlint submission new papers/p --save",
|
|
54
|
+
"npx paperlint submission update --submit --save papers/p",
|
|
55
|
+
"npx paperlint submission update papers/p --save=true",
|
|
56
|
+
"node node_modules/paperlint/bin/paperlint.mjs submission update papers/p --submit",
|
|
57
|
+
"pnpm exec paperlint submission new --save papers/p",
|
|
58
|
+
"HOTCRP_TOKEN_X=abc npx paperlint submission update papers/p --save",
|
|
59
|
+
"npm run build && npx paperlint submission update papers/p --save",
|
|
60
|
+
`npx paperlint "submission" 'update' papers/p "--save"`,
|
|
61
|
+
"npx paperlint --save submission update papers/p",
|
|
62
|
+
`npx paperlint submis"sion" up'date' papers/p --sa"ve"`,
|
|
63
|
+
])("asked: %s", (command) => {
|
|
64
|
+
expect(kind(command, () => [])).toBe("paperlint");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test.each([
|
|
68
|
+
'curl -X POST -H "Authorization: bearer $HOTCRP_TOKEN" https://x.hotcrp.com/api/paper',
|
|
69
|
+
"wget --post-file=paper.pdf https://venue.hotcrp.com/api/upload",
|
|
70
|
+
"http POST https://venue.hotcrp.com/api/paper",
|
|
71
|
+
`node -e "fetch('https://venue.hotcrp.com/api/paper', { method: 'POST' })"`,
|
|
72
|
+
`python3 -c "import requests; requests.post('https://venue.hotcrp.com/api/paper')"`,
|
|
73
|
+
"curl -F file=@paper.pdf https://SUBMIT.example-venue.org/api/upload",
|
|
74
|
+
`node -e "require('https').request('https://venue.hotcrp.com/api/paper', { method: 'POST' })"`,
|
|
75
|
+
"node upload.mjs submit.example-venue.org",
|
|
76
|
+
"curl -X POST CONFERENCE.org/api/paper",
|
|
77
|
+
"echo https://venue.hotcrp.com",
|
|
78
|
+
])("asked, straight at HotCRP: %s", (command) => {
|
|
79
|
+
expect(kind(command, () => HOSTS)).toBe("direct");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test.each([
|
|
83
|
+
"npx paperlint submission update papers/p",
|
|
84
|
+
"npx paperlint submission new papers/p",
|
|
85
|
+
"npx paperlint submission show papers/p",
|
|
86
|
+
"npx paperlint submission topics papers/p --save",
|
|
87
|
+
"grep -rn submission docs/",
|
|
88
|
+
"git log --grep submission",
|
|
89
|
+
"cat docs/submission.md",
|
|
90
|
+
"curl https://example.org/new --save-dir x",
|
|
91
|
+
"grep -rn hotcrp src/",
|
|
92
|
+
// `paperlint new` with a paper named "submission": not a send.
|
|
93
|
+
"npx paperlint new --save submission",
|
|
94
|
+
"cat src/adapters/hotcrp/index.ts",
|
|
95
|
+
"",
|
|
96
|
+
"curl https://example.org/api/paper",
|
|
97
|
+
])("silent: %s", (command) => {
|
|
98
|
+
expect(kind(command, () => HOSTS)).toBe(null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("the hosts are read only for a command that makes a request: anything else pays nothing", () => {
|
|
102
|
+
const never = () => {
|
|
103
|
+
throw new Error("read the hosts for a command with no request in it");
|
|
104
|
+
};
|
|
105
|
+
expect(kind("ls -la", never)).toBe(null);
|
|
106
|
+
// An empty host would be in every command: it matches nothing.
|
|
107
|
+
expect(kind("curl https://example.org/x", () => [""])).toBe(null);
|
|
108
|
+
expect(kind("npx paperlint submission new --save", never)).toBe("paperlint");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("hosts that cannot be read: a request still asks, rather than passing unseen", () => {
|
|
112
|
+
expect(
|
|
113
|
+
submissionConfirm(payload("curl https://example.org/x"), memoryFiles({}), {
|
|
114
|
+
portalHosts: () => {
|
|
115
|
+
throw new Error("broken preset");
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
).toStrictEqual({
|
|
119
|
+
hookSpecificOutput: {
|
|
120
|
+
hookEventName: "PreToolUse",
|
|
121
|
+
permissionDecision: "ask",
|
|
122
|
+
permissionDecisionReason:
|
|
123
|
+
"paperlint could not tell what this command sends (broken preset). Allow only if you have read the dry run's venue check.",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/** The made-up paper: its PDF, and its facts when given. */
|
|
129
|
+
const paper = (factsText: string | null): Files =>
|
|
130
|
+
memoryFiles({
|
|
131
|
+
"/w/papers/p/paper.pdf": PDF,
|
|
132
|
+
...(factsText === null
|
|
133
|
+
? {}
|
|
134
|
+
: { "/w/papers/p/_build/paper.facts.json": factsText }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const reasonOf = (command: string, fs: Files, cwd = "/w"): string =>
|
|
138
|
+
submissionConfirm(payload(command, cwd), fs)?.hookSpecificOutput
|
|
139
|
+
.permissionDecisionReason ?? "(no ask)";
|
|
140
|
+
|
|
141
|
+
const SEND = "npx paperlint submission update papers/p --submit --save";
|
|
142
|
+
const HEAD =
|
|
143
|
+
"This sends the paper to the venue's portal (paperlint submission with --save or --submit).";
|
|
144
|
+
const LAST = "Allow only if you have read the dry run's venue check.";
|
|
145
|
+
|
|
146
|
+
test("the ask: the paper folder, the PDF and its sha256, the body pages the build counted, and the line to read the venue check", () => {
|
|
147
|
+
expect(submissionConfirm(payload(SEND), paper(facts()))).toStrictEqual({
|
|
148
|
+
hookSpecificOutput: {
|
|
149
|
+
hookEventName: "PreToolUse",
|
|
150
|
+
permissionDecision: "ask",
|
|
151
|
+
permissionDecisionReason: [
|
|
152
|
+
HEAD,
|
|
153
|
+
"paper folder: papers/p",
|
|
154
|
+
`PDF: papers/p/paper.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
155
|
+
"body pages: 4 (up to the references on page 5)",
|
|
156
|
+
LAST,
|
|
157
|
+
].join("\n"),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a stale build is said loudly; no facts, broken facts and a missing PDF are said too — and every one still asks", () => {
|
|
163
|
+
expect(reasonOf(SEND, paper(facts({ sha: "b".repeat(64) })))).toBe(
|
|
164
|
+
[
|
|
165
|
+
HEAD,
|
|
166
|
+
"paper folder: papers/p",
|
|
167
|
+
`PDF: papers/p/paper.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
168
|
+
"!! THE LAST BUILD IS NOT OF THIS PDF — the check did not see it. Rebuild (`paperlint build`) and run the dry run again.",
|
|
169
|
+
"body pages: 4 (up to the references on page 5)",
|
|
170
|
+
LAST,
|
|
171
|
+
].join("\n"),
|
|
172
|
+
);
|
|
173
|
+
expect(reasonOf(SEND, paper(null)).split("\n")[3]).toBe(
|
|
174
|
+
"no build facts (_build/paper.facts.json): the paper was not built, so the venue check did not see this PDF",
|
|
175
|
+
);
|
|
176
|
+
expect(reasonOf(SEND, paper("{")).split("\n")[3]).toMatch(
|
|
177
|
+
/^could not read the build facts \(_build\/paper\.facts\.json\): /u,
|
|
178
|
+
);
|
|
179
|
+
expect(
|
|
180
|
+
reasonOf(SEND, paper(JSON.stringify({ schema: 1 }))).split("\n")[3],
|
|
181
|
+
).toBe("could not read the build facts (_build/paper.facts.json): schema 1");
|
|
182
|
+
expect(
|
|
183
|
+
reasonOf(
|
|
184
|
+
"npx paperlint submission update papers/p --pdf gone.pdf --save",
|
|
185
|
+
paper(facts()),
|
|
186
|
+
).split("\n")[2],
|
|
187
|
+
).toBe("PDF: gone.pdf — NOT FOUND");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("the body pages: up to the references when the build found them, else banal's count, else not counted", () => {
|
|
191
|
+
const noHeading = (measured: boolean) =>
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
...JSON.parse(facts({ measured })),
|
|
194
|
+
pages_text: ["Only body."],
|
|
195
|
+
});
|
|
196
|
+
expect(reasonOf(SEND, paper(noHeading(true))).split("\n")[3]).toBe(
|
|
197
|
+
"body pages: 4 (as banal counts them)",
|
|
198
|
+
);
|
|
199
|
+
expect(reasonOf(SEND, paper(noHeading(false))).split("\n")[3]).toBe(
|
|
200
|
+
"body pages: not counted (the build found no references heading and measured no geometry)",
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("the paper and the PDF as the command names them: the current folder by default, --pdf=<file> too", () => {
|
|
205
|
+
const fs = memoryFiles({
|
|
206
|
+
"/w/papers/p/paper.pdf": PDF,
|
|
207
|
+
"/w/papers/p/out.pdf": PDF,
|
|
208
|
+
});
|
|
209
|
+
expect(
|
|
210
|
+
reasonOf("npx paperlint submission update --save", fs, "/w/papers/p")
|
|
211
|
+
.split("\n")
|
|
212
|
+
.slice(1, 3),
|
|
213
|
+
).toStrictEqual([
|
|
214
|
+
"paper folder: p (the current folder)",
|
|
215
|
+
`PDF: paper.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
216
|
+
]);
|
|
217
|
+
expect(
|
|
218
|
+
reasonOf(
|
|
219
|
+
"npx paperlint submission update --abstract a.txt --pdf=papers/p/out.pdf papers/p --save",
|
|
220
|
+
fs,
|
|
221
|
+
)
|
|
222
|
+
.split("\n")
|
|
223
|
+
.slice(1, 3),
|
|
224
|
+
).toStrictEqual([
|
|
225
|
+
"paper folder: papers/p",
|
|
226
|
+
`PDF: papers/p/out.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("the paper's own `pdf` setting is the PDF hashed when the command names no --pdf, as the command sends it", () => {
|
|
231
|
+
const fs = memoryFiles({
|
|
232
|
+
"/w/papers/p/paperlint.json": JSON.stringify({ pdf: "out/main.pdf" }),
|
|
233
|
+
"/w/papers/p/out/main.pdf": PDF,
|
|
234
|
+
});
|
|
235
|
+
expect(reasonOf(SEND, fs).split("\n")[2]).toBe(
|
|
236
|
+
`PDF: papers/p/out/main.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a paperlint.json that does not parse: the PDF is the default paper.pdf, and the ask still goes out", () => {
|
|
241
|
+
const fs = memoryFiles({
|
|
242
|
+
"/w/papers/p/paperlint.json": "{ nope",
|
|
243
|
+
"/w/papers/p/paper.pdf": PDF,
|
|
244
|
+
});
|
|
245
|
+
expect(reasonOf(SEND, fs).split("\n")[2]).toBe(
|
|
246
|
+
`PDF: papers/p/paper.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a --pdf at the end with no value, which the command refuses: unresolved", () => {
|
|
251
|
+
expect(
|
|
252
|
+
reasonOf(
|
|
253
|
+
"npx paperlint submission update papers/p --save --pdf",
|
|
254
|
+
paper(facts()),
|
|
255
|
+
),
|
|
256
|
+
).toBe([HEAD, UNRESOLVED, LAST].join("\n"));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("a command that only mentions `submission` beside a send is no second send", () => {
|
|
260
|
+
expect(
|
|
261
|
+
reasonOf(
|
|
262
|
+
"npx paperlint submission update papers/p --save; echo submission",
|
|
263
|
+
paper(facts()),
|
|
264
|
+
).split("\n")[1],
|
|
265
|
+
).toBe("paper folder: papers/p");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/** What the reason says for a send the parse cannot resolve. */
|
|
269
|
+
const UNRESOLVED =
|
|
270
|
+
"could not resolve which PDF this send uploads — check the dry run";
|
|
271
|
+
|
|
272
|
+
test("the send's words are read by the command's own parser: a value flag before the paper does not shift it; words it would refuse are unresolved", () => {
|
|
273
|
+
expect(
|
|
274
|
+
reasonOf(
|
|
275
|
+
"npx paperlint submission update --max-warnings 0 --config paperlint.json papers/p --save",
|
|
276
|
+
paper(facts()),
|
|
277
|
+
).split("\n")[1],
|
|
278
|
+
).toBe("paper folder: papers/p");
|
|
279
|
+
expect(
|
|
280
|
+
reasonOf(
|
|
281
|
+
"npx paperlint submission update papers/p --nope --save",
|
|
282
|
+
paper(null),
|
|
283
|
+
),
|
|
284
|
+
).toBe([HEAD, UNRESOLVED, LAST].join("\n"));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("anything that may change the directory before the send makes its PDF unresolved, not guessed", () => {
|
|
288
|
+
for (const command of [
|
|
289
|
+
"cd papers/p && npx paperlint submission update --save",
|
|
290
|
+
"pushd papers/p; npx paperlint submission update --save",
|
|
291
|
+
"env -C papers/p npx paperlint submission update --save",
|
|
292
|
+
])
|
|
293
|
+
expect(reasonOf(command, paper(facts())), command).toBe(
|
|
294
|
+
[HEAD, UNRESOLVED, LAST].join("\n"),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a quoted --pdf=<file> with a space is one word: the PDF is that file", () => {
|
|
299
|
+
const fs = memoryFiles({ "/w/final copies/paper.pdf": PDF });
|
|
300
|
+
expect(
|
|
301
|
+
reasonOf(
|
|
302
|
+
`npx paperlint submission update papers/p --pdf="final copies/paper.pdf" --save`,
|
|
303
|
+
fs,
|
|
304
|
+
).split("\n")[2],
|
|
305
|
+
).toBe(`PDF: final copies/paper.pdf, sha256 ${HEX.slice(0, 12)}`);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/** A second paper's PDF, other bytes. */
|
|
309
|
+
const B = new TextEncoder().encode("%PDF b");
|
|
310
|
+
|
|
311
|
+
test("every send in a chain is described, one block each", () => {
|
|
312
|
+
const fs = memoryFiles({
|
|
313
|
+
"/w/papers/a/paper.pdf": PDF,
|
|
314
|
+
"/w/papers/b/paper.pdf": B,
|
|
315
|
+
});
|
|
316
|
+
const reason = reasonOf(
|
|
317
|
+
"npx paperlint submission update papers/a --save && npx paperlint submission new papers/b --save",
|
|
318
|
+
fs,
|
|
319
|
+
).split("\n");
|
|
320
|
+
expect(
|
|
321
|
+
reason.filter((l) => /^(?:send|paper folder|PDF)/u.test(l)),
|
|
322
|
+
).toStrictEqual([
|
|
323
|
+
"send 1 of 2:",
|
|
324
|
+
"paper folder: papers/a",
|
|
325
|
+
`PDF: papers/a/paper.pdf, sha256 ${HEX.slice(0, 12)}`,
|
|
326
|
+
"send 2 of 2:",
|
|
327
|
+
"paper folder: papers/b",
|
|
328
|
+
`PDF: papers/b/paper.pdf, sha256 ${sha256Hex(B).slice(0, 12)}`,
|
|
329
|
+
]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("a send whose words the shell would expand, or one inside a string, is asked about as unresolved", () => {
|
|
333
|
+
expect(
|
|
334
|
+
reasonOf('npx paperlint submission update "$P" --save', paper(null)),
|
|
335
|
+
).toBe([HEAD, UNRESOLVED, LAST].join("\n"));
|
|
336
|
+
expect(
|
|
337
|
+
reasonOf(
|
|
338
|
+
`bash -c "npx paperlint submission update papers/p --save"`,
|
|
339
|
+
paper(null),
|
|
340
|
+
),
|
|
341
|
+
).toBe([HEAD, UNRESOLVED, LAST].join("\n"));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a client run by its path or quoted is a request: the program is read by its basename", () => {
|
|
345
|
+
expect(
|
|
346
|
+
kind("/usr/bin/curl -X POST https://venue.hotcrp.com/api/paper", () => []),
|
|
347
|
+
).toBe("direct");
|
|
348
|
+
expect(
|
|
349
|
+
kind(
|
|
350
|
+
`"/usr/bin/curl" -F f=@p.pdf https://submit.example-venue.org/api`,
|
|
351
|
+
() => HOSTS,
|
|
352
|
+
),
|
|
353
|
+
).toBe("direct");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("straight at HotCRP: asked, naming that paperlint's check is skipped", () => {
|
|
357
|
+
expect(
|
|
358
|
+
reasonOf(
|
|
359
|
+
"curl -F file=@paper.pdf https://venue.hotcrp.com/api/upload",
|
|
360
|
+
paper(null),
|
|
361
|
+
),
|
|
362
|
+
).toBe(
|
|
363
|
+
"This command names a venue's submission portal, so it may send to it outside paperlint and its venue check. Allow only if you have read the dry run's venue check.",
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("a command that sends nothing: no output at all — neither allow nor deny", () => {
|
|
368
|
+
expect(
|
|
369
|
+
submissionConfirm(
|
|
370
|
+
payload("npx paperlint submission update papers/p"),
|
|
371
|
+
paper(null),
|
|
372
|
+
),
|
|
373
|
+
).toBe(null);
|
|
374
|
+
expect(
|
|
375
|
+
submissionConfirm(JSON.stringify({ tool_input: {} }), paper(null)),
|
|
376
|
+
).toBe(null);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("a crash while describing the send still asks; a payload that is not JSON is asked about", () => {
|
|
380
|
+
const broken: Files = {
|
|
381
|
+
isFile: () => false,
|
|
382
|
+
readBytes: (p: AbsolutePath) => {
|
|
383
|
+
throw new Error(`disk on fire at ${p}`);
|
|
384
|
+
},
|
|
385
|
+
writeAtomic: () => undefined,
|
|
386
|
+
};
|
|
387
|
+
expect(reasonOf(SEND, broken)).toBe(
|
|
388
|
+
`${HEAD} paperlint could not say what will be sent: disk on fire at /w/papers/p/paperlint.json. ${LAST}`,
|
|
389
|
+
);
|
|
390
|
+
expect(
|
|
391
|
+
submissionConfirm(`{"tool_input": {"command": "${SEND}"`, paper(null))
|
|
392
|
+
?.hookSpecificOutput.permissionDecision,
|
|
393
|
+
).toBe("ask");
|
|
394
|
+
// A relative or missing cwd: the hook's own working directory.
|
|
395
|
+
expect(
|
|
396
|
+
submissionConfirm(payload(SEND, "relative"), paper(null))
|
|
397
|
+
?.hookSpecificOutput.permissionDecision,
|
|
398
|
+
).toBe("ask");
|
|
399
|
+
});
|