paperlint 3.0.0 → 3.1.1
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/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli.ts paths the harness (src/cli.harness.mjs) does not reach: argument parsing of the flags
|
|
3
|
+
* `build`/`toolchain` take, the root settings' refusals, `build` and `new` refusing before any
|
|
4
|
+
* work, the hook runner when its runtime is broken, and venue choice by path and by prefix. Each
|
|
5
|
+
* case runs `run()` in-process with its own `cwd` and compares the whole code and output.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { test } from "vitest";
|
|
12
|
+
import { runNode, useTempDir, writeTree } from "../test/support.mjs";
|
|
13
|
+
import {
|
|
14
|
+
chooseVenue,
|
|
15
|
+
initTexLive,
|
|
16
|
+
ownVersion,
|
|
17
|
+
paperRuleBlocks,
|
|
18
|
+
parseArgs,
|
|
19
|
+
parseSettings,
|
|
20
|
+
run,
|
|
21
|
+
runHook,
|
|
22
|
+
silentOptionalRules,
|
|
23
|
+
toolchainTex,
|
|
24
|
+
} from "./cli.ts";
|
|
25
|
+
|
|
26
|
+
const root = useTempDir("cli-test-");
|
|
27
|
+
|
|
28
|
+
/** `run(argv)` from `cwd`, the output as one string per stream. */
|
|
29
|
+
async function cli(argv: string[], cwd: string) {
|
|
30
|
+
const out: string[] = [];
|
|
31
|
+
const err: string[] = [];
|
|
32
|
+
const code = await run(argv, {
|
|
33
|
+
log: (...a: unknown[]) => out.push(a.join(" ")),
|
|
34
|
+
err: (...a: unknown[]) => err.push(a.join(" ")),
|
|
35
|
+
cwd,
|
|
36
|
+
});
|
|
37
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("parseArgs: the boolean flags of build and toolchain, and a value flag with no value", () => {
|
|
41
|
+
const base = parseArgs([]);
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
[
|
|
44
|
+
parseArgs(["build", "--all", "--dry-run", "papers/p"]),
|
|
45
|
+
parseArgs(["toolchain", "--check"]),
|
|
46
|
+
parseArgs(["new", "x", "--format"]),
|
|
47
|
+
parseArgs(["new", "x", "--venue="]),
|
|
48
|
+
parseArgs(["new", "x", "--kind", "short", "--max-warnings=3"]),
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
{ ...base, cmd: "build", all: true, dryRun: true, paths: ["papers/p"] },
|
|
52
|
+
{ ...base, cmd: "toolchain", check: true },
|
|
53
|
+
{ ...base, cmd: "new", paths: ["x"], missingValue: "--format" },
|
|
54
|
+
{ ...base, cmd: "new", paths: ["x"], missingValue: "--venue" },
|
|
55
|
+
{ ...base, cmd: "new", paths: ["x"], kind: "short", maxWarnings: 3 },
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("parseSettings refuses a non-object, names one unknown key or several, and a mistyped default", () => {
|
|
61
|
+
const error = (json: unknown) => {
|
|
62
|
+
const r = parseSettings(json, "paperlint.json", "/r");
|
|
63
|
+
return r.ok ? null : r.error.split(" — ")[0];
|
|
64
|
+
};
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
[
|
|
67
|
+
error([]),
|
|
68
|
+
error("x"),
|
|
69
|
+
error({ typo: 1 }),
|
|
70
|
+
error({ a: 1, b: 2 }),
|
|
71
|
+
error({ kind: 3 }),
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"paperlint.json: must be a JSON object",
|
|
75
|
+
"paperlint.json: must be a JSON object",
|
|
76
|
+
'paperlint.json: unknown key "typo"',
|
|
77
|
+
'paperlint.json: unknown keys "a", "b"',
|
|
78
|
+
'paperlint.json: "kind" must be a non-empty string, got 3',
|
|
79
|
+
],
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("build refuses with no target, and --all over no papers", async () => {
|
|
84
|
+
const dir = join(root, "build-empty");
|
|
85
|
+
writeTree(dir, { "package.json": "{}" });
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
[await cli(["build"], dir), await cli(["build", "--all"], dir)],
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
code: 2,
|
|
91
|
+
out: "",
|
|
92
|
+
err:
|
|
93
|
+
"`build` needs a target: `paperlint build papers/my-paper` or `paperlint build --all`.\n" +
|
|
94
|
+
'There is deliberately no "build everything" default: a build is expensive and has side\n' +
|
|
95
|
+
"effects, so the target is named — as with make, docker and latexmk.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
code: 1,
|
|
99
|
+
out: "",
|
|
100
|
+
err: `--all: no papers found under ${join(dir, "papers")}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("new refuses an unknown format, a broken config, and a config that names no papers directory", async () => {
|
|
107
|
+
const broken = join(root, "new-broken");
|
|
108
|
+
writeTree(broken, { "paperlint.json": "{ not json" });
|
|
109
|
+
const none = join(root, "new-none");
|
|
110
|
+
writeTree(none, { "paperlint.json": JSON.stringify({ papersDir: [] }) });
|
|
111
|
+
const results = [
|
|
112
|
+
await cli(["new", "p", "--format", "docx"], root),
|
|
113
|
+
await cli(["new", "p", "--format", "md"], broken),
|
|
114
|
+
await cli(["new", "p", "--format", "md"], none),
|
|
115
|
+
];
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
results.map((r) => ({ code: r.code, err: r.err.split(":")[0] })),
|
|
118
|
+
[
|
|
119
|
+
{ code: 2, err: "--format must be one of tex, md — got `docx`" },
|
|
120
|
+
{ code: 2, err: join(broken, "paperlint.json is not valid JSON") },
|
|
121
|
+
{
|
|
122
|
+
code: 2,
|
|
123
|
+
err: '"papersDir" names no directory, so there is nowhere to put `p`.',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("new with several papers directories says which one it used", async () => {
|
|
130
|
+
const dir = join(root, "new-several");
|
|
131
|
+
writeTree(dir, {
|
|
132
|
+
"paperlint.json": JSON.stringify({ papersDir: ["a", "b"] }),
|
|
133
|
+
});
|
|
134
|
+
const r = await cli(["new", "p", "--format", "md", "--yes"], dir);
|
|
135
|
+
assert.equal(
|
|
136
|
+
r.out.split("\n")[0],
|
|
137
|
+
"several papers directories are declared — using the first: a",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("runHook: a runtime with no cli.js beside it, and a hook killed by a signal, both let the tool call through", () => {
|
|
142
|
+
const messages: string[] = [];
|
|
143
|
+
const noCli = runHook("paper-edit-guard", {
|
|
144
|
+
err: (m: string) => messages.push(m.split("\n")[0] ?? ""),
|
|
145
|
+
resolve: () => join(root, "nowhere", "index.js"),
|
|
146
|
+
});
|
|
147
|
+
const killed = runHook("paper-edit-guard", {
|
|
148
|
+
err: (m: string) => messages.push(m),
|
|
149
|
+
run: (() => ({ status: null, signal: "SIGKILL" })) as never,
|
|
150
|
+
});
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
{ noCli, killed, messages: messages.map((m) => m.split(" from ")[0]) },
|
|
153
|
+
{
|
|
154
|
+
noCli: 0,
|
|
155
|
+
killed: 0,
|
|
156
|
+
messages: ["paperlint: the hook runtime (vigiles) is not resolvable"],
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("chooseVenue: a preset by path (inside and outside the paper), by prefixed name, and a broken one", async () => {
|
|
162
|
+
const dir = join(root, "venues");
|
|
163
|
+
writeTree(dir, {
|
|
164
|
+
"papers/p/mine.jsonc": JSON.stringify({ extends: "paperlint:agenticdev" }),
|
|
165
|
+
"shared/ours.jsonc": JSON.stringify({ extends: "paperlint:agenticdev" }),
|
|
166
|
+
"shared/broken.jsonc": "{ nope",
|
|
167
|
+
});
|
|
168
|
+
const paperDir = join(dir, "papers", "p");
|
|
169
|
+
const choose = (venue: string) =>
|
|
170
|
+
chooseVenue(
|
|
171
|
+
{ venue, kind: null },
|
|
172
|
+
{ paperDir, cwd: dir, interactive: false, ask: async () => "" },
|
|
173
|
+
);
|
|
174
|
+
const results = await Promise.all([
|
|
175
|
+
choose("./papers/p/mine.jsonc"),
|
|
176
|
+
choose("./shared/ours.jsonc"),
|
|
177
|
+
choose("paperlint:agenticdev"),
|
|
178
|
+
choose("./shared/broken.jsonc"),
|
|
179
|
+
]);
|
|
180
|
+
assert.deepEqual(
|
|
181
|
+
results.map((r) => (r.ok ? r.value?.extends : r.error.split(":")[0])),
|
|
182
|
+
[
|
|
183
|
+
"./mine.jsonc",
|
|
184
|
+
"../../shared/ours.jsonc",
|
|
185
|
+
"paperlint:agenticdev",
|
|
186
|
+
"--venue ./shared/broken.jsonc",
|
|
187
|
+
],
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("doctor reads a broken config as 'the CLI would lint nothing' instead of stopping", async () => {
|
|
192
|
+
const dir = join(root, "doctor-broken");
|
|
193
|
+
writeTree(dir, { "paperlint.json": "{ not json" });
|
|
194
|
+
const r = await cli(["doctor"], dir);
|
|
195
|
+
const lines = r.out.split("\n");
|
|
196
|
+
const at = lines.indexOf("papers directory");
|
|
197
|
+
assert.deepEqual(
|
|
198
|
+
{ code: r.code, papers: lines.slice(at, at + 3) },
|
|
199
|
+
{
|
|
200
|
+
code: 2,
|
|
201
|
+
papers: [
|
|
202
|
+
"papers directory",
|
|
203
|
+
" the CLI will lint (nothing — no declaration found)",
|
|
204
|
+
" the hooks will guard (nothing — the guard refuses and says why on first use)",
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/** Run with no TeX Live anywhere: PATH holds node alone, the cache is an empty directory. */
|
|
211
|
+
async function withoutTex<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
212
|
+
const saved = { ...process.env };
|
|
213
|
+
Object.assign(process.env, {
|
|
214
|
+
PATH: dirname(process.execPath),
|
|
215
|
+
PAPERLINT_TEXLIVE_DIR: join(dir, "no-texlive"),
|
|
216
|
+
PAPERLINT_BANAL_DIR: join(dir, "no-banal"),
|
|
217
|
+
HOME: dir,
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
return await fn();
|
|
221
|
+
} finally {
|
|
222
|
+
process.env = saved;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const noTexDir = join(root, "build-notex");
|
|
227
|
+
writeTree(noTexDir, {
|
|
228
|
+
"package.json": "{}",
|
|
229
|
+
"papers/md/paper.md": "# P\n\nx\n",
|
|
230
|
+
"papers/tex/paper.tex":
|
|
231
|
+
"\\documentclass{article}\\begin{document}x\\end{document}\n",
|
|
232
|
+
});
|
|
233
|
+
const venues = join(
|
|
234
|
+
import.meta.dirname,
|
|
235
|
+
"..",
|
|
236
|
+
"skills",
|
|
237
|
+
"submit-paper",
|
|
238
|
+
"references",
|
|
239
|
+
"venues",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
test("build: a markdown-only paper is refused", async () => {
|
|
243
|
+
const md = await withoutTex(noTexDir, () =>
|
|
244
|
+
cli(["build", "papers/md"], noTexDir),
|
|
245
|
+
);
|
|
246
|
+
assert.deepEqual(md, {
|
|
247
|
+
code: 1,
|
|
248
|
+
out: [
|
|
249
|
+
"papers/md",
|
|
250
|
+
` inputs: TEXINPUTS += ${venues}`,
|
|
251
|
+
" compile: refused — no paper.tex; paperlint compiles LaTeX, and this paper has none",
|
|
252
|
+
" measure: skipped — nothing is compiled",
|
|
253
|
+
" references: skipped — nothing is compiled",
|
|
254
|
+
" ✗ nothing to compile: no paper.tex",
|
|
255
|
+
].join("\n"),
|
|
256
|
+
err:
|
|
257
|
+
"\nNo paper.tex in: papers/md.\n" +
|
|
258
|
+
'This is NOT "nothing to build" — paperlint compiles LaTeX, and these papers have no LaTeX source.\n' +
|
|
259
|
+
"Write the paper in paper.tex; `paperlint new <name>` creates one.",
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("build: with no TeX Live a dry run says where a real run stops, and a real run stops", async () => {
|
|
264
|
+
const [dry, real, toolchain] = await withoutTex(noTexDir, async () => [
|
|
265
|
+
await cli(["build", "papers/tex", "--dry-run"], noTexDir),
|
|
266
|
+
await cli(["build", "papers/tex"], noTexDir),
|
|
267
|
+
await cli(["toolchain", "--check"], noTexDir),
|
|
268
|
+
]);
|
|
269
|
+
const missing =
|
|
270
|
+
"missing: 19 package(s): amsfonts, amsmath, bibtex, booktabs, caption, cm-super, enumitem, geometry, graphics, hyperref, latex, microtype, natbib, pgf, seqsplit, tools, url, xcolor, xurl";
|
|
271
|
+
assert.deepEqual(
|
|
272
|
+
{ dry, real, toolchain: { code: toolchain.code } },
|
|
273
|
+
{
|
|
274
|
+
dry: {
|
|
275
|
+
code: 0,
|
|
276
|
+
out: [
|
|
277
|
+
`engine: none — a real run would stop here: paperlint build: no TeX Live with every package these papers need — run \`npx paperlint toolchain\` (${missing})`,
|
|
278
|
+
"papers/tex",
|
|
279
|
+
` inputs: TEXINPUTS += ${venues}`,
|
|
280
|
+
" compile: paper.tex (\\documentclass{article})",
|
|
281
|
+
" measure: pdf.js → _build/paper.facts.json (facts for the lint rules; nothing is judged here)",
|
|
282
|
+
" references: online: citations exist, titles and authors match → _build/references.json (never fails the build)",
|
|
283
|
+
" – not run (--dry-run)",
|
|
284
|
+
].join("\n"),
|
|
285
|
+
err: "",
|
|
286
|
+
},
|
|
287
|
+
real: {
|
|
288
|
+
code: 1,
|
|
289
|
+
out: "",
|
|
290
|
+
err: `✗ paperlint build: no TeX Live with every package these papers need — run \`npx paperlint toolchain\` (${missing})`,
|
|
291
|
+
},
|
|
292
|
+
toolchain: { code: 1 },
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const BIN = join(import.meta.dirname, "..", "bin", "paperlint.mjs");
|
|
298
|
+
|
|
299
|
+
test("no command prints the usage and exits 2; --kind with no value is refused", async () => {
|
|
300
|
+
const r = await cli([], root);
|
|
301
|
+
assert.equal(r.code, 2);
|
|
302
|
+
assert.match(r.out, /^paperlint — /);
|
|
303
|
+
assert.equal(parseArgs(["new", "x", "--kind"]).missingValue, "--kind");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("src/cli.ts run as the program itself answers like the bin", () => {
|
|
307
|
+
const r = runNode(join(import.meta.dirname, "cli.ts"), ["--help"]);
|
|
308
|
+
assert.equal(r.status, 0);
|
|
309
|
+
assert.match(r.stdout, /^paperlint — /);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("build and toolchain over a broken or empty declaration", async () => {
|
|
313
|
+
const broken = writeTree(join(root, "build-broken"), {
|
|
314
|
+
"paperlint.json": "{ nope",
|
|
315
|
+
});
|
|
316
|
+
const empty = writeTree(join(root, "build-none"), {
|
|
317
|
+
"paperlint.json": JSON.stringify({ papersDir: [] }),
|
|
318
|
+
});
|
|
319
|
+
const b = await cli(["build", "--all"], broken);
|
|
320
|
+
const e = await cli(["build", "--all"], empty);
|
|
321
|
+
assert.deepEqual(
|
|
322
|
+
[b.code, e],
|
|
323
|
+
[
|
|
324
|
+
2,
|
|
325
|
+
{
|
|
326
|
+
code: 1,
|
|
327
|
+
out: "config: paperlint.json",
|
|
328
|
+
err: "--all: no papers found under (nothing declared)",
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
);
|
|
332
|
+
// A config that does not parse contributes no paper to the toolchain: the shipped presets only.
|
|
333
|
+
assert.deepEqual(toolchainTex(broken), toolchainTex(empty));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("build --dry-run on a paper with a venue plans for the preset's packages", async () => {
|
|
337
|
+
const dir = writeTree(join(root, "build-venue"), {
|
|
338
|
+
"package.json": "{}",
|
|
339
|
+
"papers/v/paper.tex":
|
|
340
|
+
"\\documentclass{acmart}\\begin{document}x\\end{document}\n",
|
|
341
|
+
"papers/v/paperlint.json": JSON.stringify({ extends: "paperlint:aisec" }),
|
|
342
|
+
});
|
|
343
|
+
const r = await withoutTex(dir, () =>
|
|
344
|
+
cli(["build", "papers/v", "--dry-run"], dir),
|
|
345
|
+
);
|
|
346
|
+
assert.equal(r.code, 0);
|
|
347
|
+
assert.match(r.out, /^engine: none — a real run would stop here: .*acmart/);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("a paper's paperlint.json that does not parse stops the lint with its path", () => {
|
|
351
|
+
const dir = writeTree(join(root, "rules-broken"), {
|
|
352
|
+
"p/paper.md": "# P\n",
|
|
353
|
+
"p/paperlint.json": "{ nope",
|
|
354
|
+
});
|
|
355
|
+
const r = paperRuleBlocks([join(dir, "p")]);
|
|
356
|
+
assert.equal(r.ok, false);
|
|
357
|
+
assert.match(!r.ok ? r.error : "", /^.*rules-broken\/p\/paperlint\.json: /);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("silentOptionalRules: a config with no rules turns nothing on", async () => {
|
|
361
|
+
assert.deepEqual(await silentOptionalRules({} as never, [], {} as never), []);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("init with no path and --format md sets up the current directory", async () => {
|
|
365
|
+
const dir = writeTree(join(root, "init-here"), { "package.json": "{}" });
|
|
366
|
+
const r = await cli(
|
|
367
|
+
["init", "--yes", "--no-hooks", "--paper", "first", "--format", "md"],
|
|
368
|
+
dir,
|
|
369
|
+
);
|
|
370
|
+
assert.equal(
|
|
371
|
+
existsSync(join(dir, "papers", "first", "paper.md")),
|
|
372
|
+
true,
|
|
373
|
+
r.out + r.err,
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("init and doctor with a declaration that names no directory: nothing to compare", async () => {
|
|
378
|
+
const dir = writeTree(join(root, "init-empty"), {
|
|
379
|
+
"package.json": "{}",
|
|
380
|
+
"paperlint.json": JSON.stringify({ papersDir: [] }),
|
|
381
|
+
});
|
|
382
|
+
const d = await cli(["doctor"], dir);
|
|
383
|
+
assert.match(
|
|
384
|
+
d.out,
|
|
385
|
+
/ {2}the CLI will lint {4}\(nothing — no declaration found\)/,
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("lint: an empty papers directory that IS the working directory is named '.'", async () => {
|
|
390
|
+
const dir = writeTree(join(root, "lint-dot"), {
|
|
391
|
+
"paperlint.json": JSON.stringify({ papersDir: "." }),
|
|
392
|
+
});
|
|
393
|
+
const r = await cli(["lint"], dir);
|
|
394
|
+
assert.equal(r.code, 2);
|
|
395
|
+
assert.match(r.err, /^no papers in \./);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("lint: a named directory with nothing paperlint lints is named in full when it is the working directory", async () => {
|
|
399
|
+
const dir = writeTree(join(root, "lint-nothing"), { "notes.txt": "x" });
|
|
400
|
+
const r = await cli(["lint", "."], dir);
|
|
401
|
+
assert.deepEqual(r.code, 1);
|
|
402
|
+
assert.match(r.err, new RegExp(`^nothing was linted under ${dir} — `));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("new from inside the paper it names adds the missing files and names the paper in full", async () => {
|
|
406
|
+
const dir = writeTree(join(root, "new-inside"), {
|
|
407
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers" }),
|
|
408
|
+
"papers/p/paper.md": "# P\n",
|
|
409
|
+
});
|
|
410
|
+
const r = await cli(
|
|
411
|
+
["new", "p", "--format", "md", "--yes"],
|
|
412
|
+
join(dir, "papers", "p"),
|
|
413
|
+
);
|
|
414
|
+
assert.match(
|
|
415
|
+
r.out,
|
|
416
|
+
new RegExp(
|
|
417
|
+
`already there, only missing files added: ${join(dir, "papers", "p")}`,
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("new with several directories, run from the first, names it in full", async () => {
|
|
423
|
+
const dir = writeTree(join(root, "new-several-inside"), {
|
|
424
|
+
"paperlint.json": JSON.stringify({ papersDir: ["a", "b"] }),
|
|
425
|
+
"a/.keep": "",
|
|
426
|
+
});
|
|
427
|
+
const r = await cli(["new", "p", "--format", "md", "--yes"], join(dir, "a"));
|
|
428
|
+
assert.equal(
|
|
429
|
+
r.out.split("\n")[0],
|
|
430
|
+
`several papers directories are declared — using the first: ${join(dir, "a")}`,
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("lint: an ESLint failure that is not an empty set is not swallowed", async () => {
|
|
435
|
+
const dir = writeTree(join(root, "lint-bad-option"), {
|
|
436
|
+
"paperlint.json": JSON.stringify({
|
|
437
|
+
rules: { "paper/stages": ["error", { x: 1 }] },
|
|
438
|
+
}),
|
|
439
|
+
"papers/p/paper.md": "# P\n",
|
|
440
|
+
"papers/p/PIPELINE-STATUS.md": "---\nstages: []\n---\n# S\n",
|
|
441
|
+
});
|
|
442
|
+
await assert.rejects(cli(["lint"], dir), {
|
|
443
|
+
message:
|
|
444
|
+
/Key "paper\/stages":\n\tValue \[\{"x":1\}\] should NOT have more than 0 items/,
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("lint: a path that shares nothing with the project but the filesystem root is still linted", async () => {
|
|
449
|
+
// The temp project lives under the OS temp directory and the fixture in this repository: the
|
|
450
|
+
// only directory holding both is `/`, and ESLint runs from there.
|
|
451
|
+
const paper = join(
|
|
452
|
+
import.meta.dirname,
|
|
453
|
+
"..",
|
|
454
|
+
"fixtures",
|
|
455
|
+
"paper-stages",
|
|
456
|
+
"ok",
|
|
457
|
+
);
|
|
458
|
+
const r = await cli(["lint", paper], root);
|
|
459
|
+
assert.equal(r.code, 1);
|
|
460
|
+
assert.match(
|
|
461
|
+
r.out,
|
|
462
|
+
/stage «submitted» \(2026-07-22\) carries no frozen source/,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* `new` at a terminal: stdin and stdout claim to be TTYs (a preload), and each prompt is answered
|
|
468
|
+
* when it appears — a pipe written all at once is swallowed by the first question.
|
|
469
|
+
*/
|
|
470
|
+
function newAtTerminal(cwd: string, answers: Record<string, string>) {
|
|
471
|
+
const tty = join(root, "tty.mjs");
|
|
472
|
+
writeTree(root, {
|
|
473
|
+
"tty.mjs":
|
|
474
|
+
'Object.defineProperty(process.stdin, "isTTY", { value: true });\n' +
|
|
475
|
+
'Object.defineProperty(process.stdout, "isTTY", { value: true });\n',
|
|
476
|
+
});
|
|
477
|
+
return new Promise<{ status: number | null; out: string }>((done) => {
|
|
478
|
+
const child = spawn(
|
|
479
|
+
process.execPath,
|
|
480
|
+
["--import", tty, BIN, "new", "first"],
|
|
481
|
+
{
|
|
482
|
+
cwd,
|
|
483
|
+
env: { ...process.env, CI: "" },
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
let out = "";
|
|
487
|
+
const asked = new Set<string>();
|
|
488
|
+
child.stdout.on("data", (d: Buffer) => {
|
|
489
|
+
out += String(d);
|
|
490
|
+
for (const [prompt, answer] of Object.entries(answers)) {
|
|
491
|
+
if (asked.has(prompt) || !out.includes(prompt)) continue;
|
|
492
|
+
asked.add(prompt);
|
|
493
|
+
child.stdin.write(answer);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
child.on("close", (status) => done({ status, out }));
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
test("new at a terminal asks for the format and the venue; the answers pick them", async () => {
|
|
501
|
+
const md = writeTree(join(root, "new-tty-md"), { "package.json": "{}" });
|
|
502
|
+
const r = await newAtTerminal(md, { "format:": "md\n", "venue:": "none\n" });
|
|
503
|
+
assert.equal(r.status, 0, r.out);
|
|
504
|
+
assert.equal(existsSync(join(md, "papers", "first", "paper.md")), true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("chooseVenue: a question that fails (the stream ended) takes the default, no venue", async () => {
|
|
508
|
+
const r = await chooseVenue(parseArgs(["new", "x"]), {
|
|
509
|
+
paperDir: join(root, "x"),
|
|
510
|
+
cwd: root,
|
|
511
|
+
interactive: true,
|
|
512
|
+
ask: () => Promise.reject(new Error("Aborted with Ctrl+D")),
|
|
513
|
+
});
|
|
514
|
+
assert.deepEqual(r, { ok: true, value: null });
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("ownVersion: the manifest's version; an unreadable manifest or a non-string version is undefined", () => {
|
|
518
|
+
assert.deepEqual(
|
|
519
|
+
[
|
|
520
|
+
ownVersion(() => '{"version": "1.2.3"}'),
|
|
521
|
+
ownVersion(() => "{ nope"),
|
|
522
|
+
ownVersion(() => '{"version": 3}'),
|
|
523
|
+
ownVersion(() => {
|
|
524
|
+
throw new Error("ENOENT");
|
|
525
|
+
}),
|
|
526
|
+
typeof ownVersion(),
|
|
527
|
+
],
|
|
528
|
+
["1.2.3", undefined, undefined, undefined, "string"],
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("init's TeX Live install runs the toolchain over the project's papers, never a check", () => {
|
|
533
|
+
const dir = writeTree(join(root, "init-tex"), { "package.json": "{}" });
|
|
534
|
+
const calls: unknown[] = [];
|
|
535
|
+
const fake = ((o: unknown) => (calls.push(o), 0)) as never;
|
|
536
|
+
const log = () => {};
|
|
537
|
+
const here = initTexLive(
|
|
538
|
+
parseArgs(["init"]),
|
|
539
|
+
{ log, err: log, cwd: dir },
|
|
540
|
+
fake,
|
|
541
|
+
);
|
|
542
|
+
const there = initTexLive(
|
|
543
|
+
parseArgs(["init", "sub"]),
|
|
544
|
+
{ log, err: log, cwd: dir },
|
|
545
|
+
fake,
|
|
546
|
+
);
|
|
547
|
+
assert.deepEqual([here.install(), there.install()], [0, 0]);
|
|
548
|
+
assert.deepEqual(
|
|
549
|
+
calls.map((c) => {
|
|
550
|
+
const o = c as { check: boolean; tex: unknown; banal: unknown };
|
|
551
|
+
return [o.check, o.tex, typeof o.banal];
|
|
552
|
+
}),
|
|
553
|
+
[
|
|
554
|
+
[false, toolchainTex(dir), "object"],
|
|
555
|
+
[false, toolchainTex(join(dir, "sub")), "object"],
|
|
556
|
+
],
|
|
557
|
+
);
|
|
558
|
+
assert.equal(typeof here.installed(), "boolean");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("lint with its LaTeX language missing from the install still lints the markdown papers", () => {
|
|
562
|
+
const dir = writeTree(join(root, "lint-no-tex-language"), {
|
|
563
|
+
"papers/p/paper.md": "# P\n\nSome prose.\n",
|
|
564
|
+
"no-latex.mjs":
|
|
565
|
+
"import { register } from 'node:module';\n" +
|
|
566
|
+
"register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s.endsWith('latex-language.mjs')) throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
|
|
567
|
+
});
|
|
568
|
+
const without = runNode(BIN, ["lint", "papers"], {
|
|
569
|
+
cwd: dir,
|
|
570
|
+
nodeArgs: ["--import", join(dir, "no-latex.mjs")],
|
|
571
|
+
});
|
|
572
|
+
const whole = runNode(BIN, ["lint", "papers"], { cwd: dir });
|
|
573
|
+
// A markdown paper needs no LaTeX language: the same report, the same code.
|
|
574
|
+
assert.deepEqual(without, whole);
|
|
575
|
+
assert.match(whole.stdout, /missing `PIPELINE-STATUS\.md`/);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("`paperlint hook` with no name: exit 2, and the usage names an example", async () => {
|
|
579
|
+
assert.deepEqual(await cli(["hook"], root), {
|
|
580
|
+
code: 2,
|
|
581
|
+
out: "",
|
|
582
|
+
err: "`hook` needs a name, e.g. `paperlint hook paper-edit-guard`",
|
|
583
|
+
});
|
|
584
|
+
});
|