paperlint 3.1.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/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.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- 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/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 +3 -3
- 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/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- 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/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 +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- 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 +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -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.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/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.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- 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 +27 -7
- 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 +3 -3
- 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/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -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/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- 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 -65
- package/src/structure.mutations.mjs +0 -83
package/src/init.test.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `paperlint init
|
|
3
|
-
*
|
|
2
|
+
* `paperlint init`: how the papers directory is decided and declared, the refusals over a
|
|
3
|
+
* `paperlint.json` it cannot use, and each section of its report, asserted whole.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The first block is the one this file was written for — a project that ALREADY declares its
|
|
6
|
+
* papers directory: the declaration is the answer, and init reports it, not a directory it
|
|
7
|
+
* discovered elsewhere and then did not use. Seen migrating a consumer project: init printed
|
|
8
|
+
* "papers directory ✓ eslint-rules/fixtures — 4 candidates", then kept the declared papersDir.
|
|
9
|
+
* The line described a decision that was not made.
|
|
7
10
|
*/
|
|
8
11
|
import {
|
|
9
12
|
existsSync,
|
|
@@ -17,7 +20,21 @@ import {
|
|
|
17
20
|
import { tmpdir } from "node:os";
|
|
18
21
|
import { join } from "node:path";
|
|
19
22
|
import { afterEach, describe, expect, it } from "vitest";
|
|
20
|
-
import {
|
|
23
|
+
import { FRESH_CLONE_NOTE } from "./hooks-settings.ts";
|
|
24
|
+
import {
|
|
25
|
+
UNPINNED_REF,
|
|
26
|
+
WORKFLOW_PATH,
|
|
27
|
+
choosePapers,
|
|
28
|
+
declarePapers,
|
|
29
|
+
init,
|
|
30
|
+
offerHooks,
|
|
31
|
+
offerTexLive,
|
|
32
|
+
reportHooks,
|
|
33
|
+
reportSkillLinks,
|
|
34
|
+
reportTexLive,
|
|
35
|
+
reportWorkflow,
|
|
36
|
+
} from "./init.ts";
|
|
37
|
+
import { runNode } from "../test/support.mjs";
|
|
21
38
|
|
|
22
39
|
const dirs: string[] = [];
|
|
23
40
|
afterEach(() => {
|
|
@@ -204,3 +221,289 @@ describe("paperlint init — TeX Live is offered with its cost, never installed
|
|
|
204
221
|
expect(r.text).toMatch(/✓ paperlint's TeX Live is installed/);
|
|
205
222
|
});
|
|
206
223
|
});
|
|
224
|
+
|
|
225
|
+
/** A throwaway project from `{ path: contents }`. */
|
|
226
|
+
function tree(files: Record<string, string>): string {
|
|
227
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
|
|
228
|
+
dirs.push(root);
|
|
229
|
+
for (const [p, text] of Object.entries(files)) {
|
|
230
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
231
|
+
writeFileSync(join(root, p), text);
|
|
232
|
+
}
|
|
233
|
+
return root;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** init with everything outside the papers step stubbed out; returns the code and both streams. */
|
|
237
|
+
async function initRun(
|
|
238
|
+
root: string,
|
|
239
|
+
opts: Parameters<typeof init>[1] = {},
|
|
240
|
+
): Promise<{ code: number; out: string[]; err: string[] }> {
|
|
241
|
+
const out: string[] = [];
|
|
242
|
+
const err: string[] = [];
|
|
243
|
+
const code = await init(root, {
|
|
244
|
+
cwd: root,
|
|
245
|
+
log: (s: string) => out.push(s),
|
|
246
|
+
err: (s: string) => err.push(s),
|
|
247
|
+
interactive: false,
|
|
248
|
+
hooks: false,
|
|
249
|
+
run: (() => ({ status: 0 })) as never,
|
|
250
|
+
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
251
|
+
...opts,
|
|
252
|
+
});
|
|
253
|
+
return { code, out, err };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
describe("paperlint init — a paperlint.json it cannot use stops it before anything is written", () => {
|
|
257
|
+
it("unparsable: exit 2, the reason, and nothing written", async () => {
|
|
258
|
+
const root = tree({ "paperlint.json": "{ nope" });
|
|
259
|
+
const r = await initRun(root);
|
|
260
|
+
expect(r.code).toBe(2);
|
|
261
|
+
expect(r.err[0]).toMatch(/^ {2}✗ paperlint\.json is not valid JSON: /);
|
|
262
|
+
expect(r.err.slice(1)).toEqual([
|
|
263
|
+
" nothing was written. The hooks read their papers directory from this file and",
|
|
264
|
+
" refuse every Bash command while it cannot be parsed — fix the JSON first.",
|
|
265
|
+
]);
|
|
266
|
+
expect(readFileSync(join(root, "paperlint.json"), "utf8")).toBe("{ nope");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("a papersDir that is not a string: exit 2, named", async () => {
|
|
270
|
+
const root = tree({ "paperlint.json": '{ "papersDir": 7 }' });
|
|
271
|
+
const r = await initRun(root);
|
|
272
|
+
expect([r.code, r.err]).toEqual([
|
|
273
|
+
2,
|
|
274
|
+
[
|
|
275
|
+
" ✗ paperlint.json declares papersDir = 7 — it must be a directory path (a string)",
|
|
276
|
+
],
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("a file without a trailing newline keeps having none", async () => {
|
|
281
|
+
const root = tree({
|
|
282
|
+
"paperlint.json": '{"x":1}',
|
|
283
|
+
"drafts/a/paper.md": "# a\n",
|
|
284
|
+
});
|
|
285
|
+
const decl = await declarePapers(root, () =>
|
|
286
|
+
choosePapers(root, { interactive: false }),
|
|
287
|
+
);
|
|
288
|
+
expect(decl.status).toBe("written");
|
|
289
|
+
expect(readFileSync(join(root, "paperlint.json"), "utf8")).toBe(
|
|
290
|
+
'{\n "x": 1,\n "papersDir": "drafts"\n}',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("paperlint init — how several candidate directories were decided, said", () => {
|
|
296
|
+
const several = () =>
|
|
297
|
+
tree({ "drafts/a/paper.md": "# a\n", "writing/b/paper.md": "# b\n" });
|
|
298
|
+
const section = (out: string[]) =>
|
|
299
|
+
out.slice(out.indexOf("papers directory") + 1, out.indexOf("settings") - 1);
|
|
300
|
+
|
|
301
|
+
it("picked by a human", async () => {
|
|
302
|
+
const r = await initRun(several(), {
|
|
303
|
+
interactive: true,
|
|
304
|
+
ask: async () => "2",
|
|
305
|
+
});
|
|
306
|
+
expect(section(r.out)).toEqual([
|
|
307
|
+
" ✓ writing — you picked it out of 2 candidates",
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("no terminal: the first, and the others named", async () => {
|
|
312
|
+
const r = await initRun(several());
|
|
313
|
+
expect(section(r.out)).toEqual([
|
|
314
|
+
" ✓ drafts — 2 candidates, stdin is not a terminal so nothing was asked",
|
|
315
|
+
' the others: writing — set "papersDir" in paperlint.json if this is the wrong one',
|
|
316
|
+
]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("an answer that picks nothing: the first, and said so", async () => {
|
|
320
|
+
const r = await initRun(several(), {
|
|
321
|
+
interactive: true,
|
|
322
|
+
ask: async () => "nine",
|
|
323
|
+
});
|
|
324
|
+
expect(section(r.out)[0]).toBe(
|
|
325
|
+
" ✓ drafts — 2 candidates, no answer was given, so the first one was taken",
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const here = (p: string) => `<${p}>`;
|
|
331
|
+
|
|
332
|
+
describe("paperlint init — the workflow section, whole", () => {
|
|
333
|
+
it("the workflow: written unpinned, kept, declined, not asked", () => {
|
|
334
|
+
const ctx = { papersDir: "papers", why: "stdin is not a terminal" };
|
|
335
|
+
expect(reportWorkflow("written", ctx)).toEqual([
|
|
336
|
+
` ✓ wrote ${WORKFLOW_PATH} — pin ${UNPINNED_REF} before pushing it`,
|
|
337
|
+
]);
|
|
338
|
+
expect(reportWorkflow("written", { ...ctx, version: "1.2.3" })).toEqual([
|
|
339
|
+
` ✓ wrote ${WORKFLOW_PATH}, pinned to v1.2.3`,
|
|
340
|
+
]);
|
|
341
|
+
expect(reportWorkflow("kept", ctx)).toEqual([
|
|
342
|
+
` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
|
|
343
|
+
]);
|
|
344
|
+
const step = [
|
|
345
|
+
" to run the same checks in CI, add this step to a workflow:",
|
|
346
|
+
` - uses: zernie/paperlint@${UNPINNED_REF}`,
|
|
347
|
+
" with:",
|
|
348
|
+
" paths: papers",
|
|
349
|
+
];
|
|
350
|
+
expect(reportWorkflow("declined", ctx)).toEqual([
|
|
351
|
+
" · declined — nothing written",
|
|
352
|
+
...step,
|
|
353
|
+
]);
|
|
354
|
+
expect(reportWorkflow("not-asked", ctx)).toEqual([
|
|
355
|
+
" · stdin is not a terminal, so nothing was asked. Default taken: NO file written.",
|
|
356
|
+
...step,
|
|
357
|
+
]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("paperlint init — the hooks section, whole", () => {
|
|
362
|
+
it("the hooks: every outcome", () => {
|
|
363
|
+
const how = "HOW";
|
|
364
|
+
const r = (o: Parameters<typeof reportHooks>[0]) =>
|
|
365
|
+
reportHooks(o, { how, here });
|
|
366
|
+
expect(r({ status: "skipped" })).toEqual([
|
|
367
|
+
" · --no-hooks — nothing written",
|
|
368
|
+
]);
|
|
369
|
+
expect(r({ status: "declined" })).toEqual([
|
|
370
|
+
" · declined — nothing written. `npx paperlint init` again wires them later",
|
|
371
|
+
]);
|
|
372
|
+
expect(r({ status: "failed", reason: "no vigiles" })).toEqual([
|
|
373
|
+
" ✗ not wired — no vigiles",
|
|
374
|
+
" the hooks need vigiles to run at all; reinstall this package, then `npx paperlint init`",
|
|
375
|
+
]);
|
|
376
|
+
expect(r({ status: "unparsable", path: "s.json", reason: "bad" })).toEqual([
|
|
377
|
+
" ✗ <s.json> does not parse — nothing written: bad",
|
|
378
|
+
]);
|
|
379
|
+
expect(r({ status: "present", path: "s.json", names: ["a"] })).toEqual([
|
|
380
|
+
" ✓ already wired in <s.json> — nothing changed",
|
|
381
|
+
" HOW",
|
|
382
|
+
` ${FRESH_CLONE_NOTE}`,
|
|
383
|
+
]);
|
|
384
|
+
expect(
|
|
385
|
+
r({
|
|
386
|
+
status: "foreign",
|
|
387
|
+
path: "s.json",
|
|
388
|
+
names: ["a", "b"],
|
|
389
|
+
found: [{ name: "a", command: "node x.mjs" }],
|
|
390
|
+
missing: ["b"],
|
|
391
|
+
}),
|
|
392
|
+
).toEqual([
|
|
393
|
+
" ✓ already wired under another spelling in <s.json> — nothing written, so nothing runs twice:",
|
|
394
|
+
" a: node x.mjs",
|
|
395
|
+
" ⚠ and NOT wired in any form: b — add them in that same form",
|
|
396
|
+
" to switch to the form init writes, delete those commands and run `npx paperlint init` again",
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("paperlint init — the skills section, whole", () => {
|
|
402
|
+
it("the skills: nothing linked, and a mix with a skipped entry of unknown reason and a missing one", () => {
|
|
403
|
+
expect(reportSkillLinks({ ok: false, error: "no package" }, here)).toEqual([
|
|
404
|
+
"",
|
|
405
|
+
"skills (Claude Code finds project skills in .claude/skills/, not in node_modules)",
|
|
406
|
+
" ⚠ nothing linked — no package",
|
|
407
|
+
" install the package into this project (`npm i -D …`), then `npx paperlint init` again",
|
|
408
|
+
]);
|
|
409
|
+
expect(
|
|
410
|
+
reportSkillLinks(
|
|
411
|
+
{
|
|
412
|
+
ok: true,
|
|
413
|
+
home: "h",
|
|
414
|
+
example: null,
|
|
415
|
+
links: [
|
|
416
|
+
{ name: "a", status: "created" },
|
|
417
|
+
{ name: "b", status: "foreign" },
|
|
418
|
+
{ name: "c", status: "missing" },
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
here,
|
|
422
|
+
).slice(2),
|
|
423
|
+
).toEqual([
|
|
424
|
+
" ⚠ 3 shipped: 1 linked now, 0 already linked, 1 skipped, 1 NOT linked",
|
|
425
|
+
" left untouched — the name is taken by something paperlint did not make:",
|
|
426
|
+
" b — occupied",
|
|
427
|
+
" those skills are NOT available in Claude Code until the entry is moved or removed",
|
|
428
|
+
]);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("paperlint init — TeX Live and hooks failures, whole", () => {
|
|
433
|
+
it("TeX Live: a failed install says to run it again", async () => {
|
|
434
|
+
const t = await offerTexLive(
|
|
435
|
+
{ installed: () => false, install: () => 1 },
|
|
436
|
+
{ interactive: true, ask: async () => "yes" },
|
|
437
|
+
);
|
|
438
|
+
expect([t, reportTexLive(t, "")]).toEqual([
|
|
439
|
+
"failed",
|
|
440
|
+
[
|
|
441
|
+
" ✗ the install failed — see above. Run it again:",
|
|
442
|
+
" npx paperlint toolchain # ~270 MB, ~3 min, once",
|
|
443
|
+
],
|
|
444
|
+
]);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("hooks: a merge that throws is a failed step, not a crash", async () => {
|
|
448
|
+
const root = tree({});
|
|
449
|
+
const outcome = await offerHooks(root, {
|
|
450
|
+
hooks: true,
|
|
451
|
+
interactive: false,
|
|
452
|
+
merge: () => {
|
|
453
|
+
throw new Error("merge exploded");
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
expect(outcome).toEqual({ status: "failed", reason: "merge exploded" });
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("paperlint init — a first paper", () => {
|
|
461
|
+
it("Enter at the name prompt skips it: nothing is created", async () => {
|
|
462
|
+
const made: string[] = [];
|
|
463
|
+
const r = await initRun(tree({}), {
|
|
464
|
+
interactive: true,
|
|
465
|
+
ask: async () => "",
|
|
466
|
+
createPaper: async (_root, name) => (made.push(name), 0),
|
|
467
|
+
});
|
|
468
|
+
expect(made).toEqual([]);
|
|
469
|
+
expect(r.out[r.out.indexOf("first paper") + 1]).toBe(
|
|
470
|
+
" · none yet. `npx paperlint new <name>` or `--paper <name>` creates one",
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("paperlint init — paths are shown relative to where it was run", () => {
|
|
476
|
+
it("a path that IS the working directory is shown whole, not as an empty string", async () => {
|
|
477
|
+
const root = tree({ "papers/a/paper.md": "# a\n" });
|
|
478
|
+
const home = join(root, ".claude", "skills");
|
|
479
|
+
mkdirSync(home, { recursive: true });
|
|
480
|
+
const r = await initRun(root, {
|
|
481
|
+
cwd: home,
|
|
482
|
+
link: () => ({
|
|
483
|
+
ok: true,
|
|
484
|
+
home,
|
|
485
|
+
example: "../../node_modules/paperlint/skills/x",
|
|
486
|
+
links: [{ name: "x", status: "present" }],
|
|
487
|
+
}),
|
|
488
|
+
});
|
|
489
|
+
expect(r.out).toContain(
|
|
490
|
+
` ${join(home, "<name>")} → ../../node_modules/paperlint/skills/<name>`,
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("askOnTerminal", () => {
|
|
496
|
+
it("reads one line from stdin, and the question goes to stdout", () => {
|
|
497
|
+
const probe = join(tree({}), "probe.mjs");
|
|
498
|
+
writeFileSync(
|
|
499
|
+
probe,
|
|
500
|
+
`const { askOnTerminal } = await import(${JSON.stringify(join(import.meta.dirname, "init.ts"))});\n` +
|
|
501
|
+
`console.log(JSON.stringify(await askOnTerminal("name? ")));\n`,
|
|
502
|
+
);
|
|
503
|
+
expect(runNode(probe, [], { input: "paper-one\n" })).toEqual({
|
|
504
|
+
status: 0,
|
|
505
|
+
stdout: 'name? "paper-one"\n',
|
|
506
|
+
stderr: "",
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
package/src/init.ts
CHANGED
|
@@ -83,7 +83,7 @@ export type PapersHow =
|
|
|
83
83
|
* declaration had already been written. So the install both succeeded and looked like a crash.
|
|
84
84
|
* An unanswered question is an answer: take the default and say so.
|
|
85
85
|
*/
|
|
86
|
-
async function askOrDefault(
|
|
86
|
+
export async function askOrDefault(
|
|
87
87
|
ask: (q: string) => Promise<string>,
|
|
88
88
|
question: string,
|
|
89
89
|
): Promise<string | null> {
|
|
@@ -857,8 +857,8 @@ export async function init(
|
|
|
857
857
|
// 🔴 THE NAMES AND THE COUNT HERE, THE CONSEQUENCES — IN THE doctor REPORT BELOW, AND THIS IS
|
|
858
858
|
// NOT ABOUT SAVING LINES. The first version printed here the same "✗ program — what goes silent
|
|
859
859
|
// without it" table that doctor prints twenty lines later. Not only did the reader see it
|
|
860
|
-
// twice — the harness assert could not tell one from the other and
|
|
861
|
-
// remedy OUT of init green. One fact is printed by one author.
|
|
860
|
+
// twice — the harness assert could not tell one from the other and would have let a defect
|
|
861
|
+
// cut the remedy OUT of init green. One fact is printed by one author.
|
|
862
862
|
log(
|
|
863
863
|
` ✗ ${String(missing.length)} of ${String(PROGRAMS.length)} missing: ` +
|
|
864
864
|
missing.map((p) => p.bin).join(", "),
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* memory: the two error spellings `-file-line-error` produces, the missing-package form, and a
|
|
6
6
|
* marker that TeX broke at column 79.
|
|
7
7
|
*/
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
8
|
import { dirname, join } from "node:path";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { createChecker } from "../lib/check.mjs";
|
|
11
11
|
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const {
|
|
@@ -20,11 +20,7 @@ const {
|
|
|
20
20
|
MAX_PRINT_LINE,
|
|
21
21
|
} = await import(join(HERE, "latex-log.ts"));
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const check = (label, cond) => {
|
|
25
|
-
assert.ok(cond, label);
|
|
26
|
-
n++;
|
|
27
|
-
};
|
|
23
|
+
const check = createChecker();
|
|
28
24
|
|
|
29
25
|
// ── the 79-column wrap ──────────────────────────────────────────────────────────────────
|
|
30
26
|
// A package warning long enough that "Rerun to get" crosses column 79. A real acmart log shows the
|
|
@@ -221,6 +217,35 @@ check(
|
|
|
221
217
|
.citations.length === 1,
|
|
222
218
|
);
|
|
223
219
|
|
|
220
|
+
check(
|
|
221
|
+
"an \\@input aux that is not there (the default reader finds nothing) is skipped",
|
|
222
|
+
JSON.stringify(auxBib("\\@input{gone.aux}\n\\citation{x}\n").citations) ===
|
|
223
|
+
JSON.stringify(["x"]),
|
|
224
|
+
);
|
|
225
|
+
check(
|
|
226
|
+
"a log whose LAST line is exactly 79 bytes keeps it (nothing follows to join it to)",
|
|
227
|
+
JSON.stringify(unwrapLog(`a\n${"x".repeat(79)}`)) ===
|
|
228
|
+
JSON.stringify(["a", "x".repeat(79)]),
|
|
229
|
+
);
|
|
230
|
+
check(
|
|
231
|
+
"an error path must hold a dot or a slash; a bare word before the line number is not one",
|
|
232
|
+
isErrorLine("paper.tex:4: Undefined control sequence.") &&
|
|
233
|
+
isErrorLine("chapters/intro:9: x") &&
|
|
234
|
+
!isErrorLine("paper:4: x") &&
|
|
235
|
+
!isErrorLine("two words.tex:4: x"),
|
|
236
|
+
);
|
|
237
|
+
{
|
|
238
|
+
const flood = [
|
|
239
|
+
"./paper.tex:1: Undefined control sequence.",
|
|
240
|
+
...Array.from({ length: 20 }, (_, i) => `noise ${String(i)}`),
|
|
241
|
+
"l.1 \\foo",
|
|
242
|
+
];
|
|
243
|
+
check(
|
|
244
|
+
"an excerpt stops at MAX_EXCERPT lines even before the l.NNN line arrives",
|
|
245
|
+
errorExcerpt(flood).length === 12 && errorExcerpt(flood)[11] === "noise 10",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
console.log(
|
|
225
|
-
`✓ ${String(
|
|
250
|
+
`✓ ${String(check.count)} assertions passed — latex-log: markers across the 79-column wrap, both error spellings, aux`,
|
|
226
251
|
);
|
package/src/latex-log.ts
CHANGED
|
@@ -39,13 +39,6 @@ export type LogMarker =
|
|
|
39
39
|
| "undefined-references"
|
|
40
40
|
| "undefined-citations";
|
|
41
41
|
|
|
42
|
-
/** Markers that mean "the output of this pass is not final yet". */
|
|
43
|
-
export const RERUN_MARKERS: readonly LogMarker[] = [
|
|
44
|
-
"rerun-requested",
|
|
45
|
-
"labels-changed",
|
|
46
|
-
"rerunfilecheck",
|
|
47
|
-
];
|
|
48
|
-
|
|
49
42
|
/**
|
|
50
43
|
* Rejoin the lines TeX broke at column 79. A line of exactly 79 bytes continues on the next one.
|
|
51
44
|
*
|
|
@@ -100,9 +93,8 @@ export function logMarkers(lines: readonly string[]): LogMarker[] {
|
|
|
100
93
|
* contain a dot or a slash, so an ordinary warning ending in a number and a colon cannot pass.
|
|
101
94
|
*/
|
|
102
95
|
export function isErrorLine(line: string): boolean {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return m !== null && /[./]/.test(m[1] ?? "");
|
|
96
|
+
// `<path>:<line>: `, where the path (no space, no colon) holds at least one dot or slash.
|
|
97
|
+
return line.startsWith("!") || /^[^\s:]*[./][^\s:]*:\d+: /.test(line);
|
|
106
98
|
}
|
|
107
99
|
|
|
108
100
|
/** The `l.NNN …` line TeX prints to show where in the source it stopped. */
|
|
@@ -121,12 +113,13 @@ export function errorExcerpt(lines: readonly string[]): string[] {
|
|
|
121
113
|
const start = lines.findIndex(isErrorLine);
|
|
122
114
|
if (start < 0) return [];
|
|
123
115
|
const out: string[] = [];
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
const rest = lines.slice(start);
|
|
117
|
+
for (const [i, line] of rest.entries()) {
|
|
118
|
+
if (out.length >= MAX_EXCERPT) break;
|
|
126
119
|
if (line.trim() === "") continue;
|
|
127
120
|
out.push(line);
|
|
128
121
|
if (isContextLine(line)) {
|
|
129
|
-
const next =
|
|
122
|
+
const next = rest[i + 1];
|
|
130
123
|
if (next !== undefined && next.trim() !== "") out.push(next);
|
|
131
124
|
break;
|
|
132
125
|
}
|
|
@@ -8,20 +8,16 @@
|
|
|
8
8
|
* The decision is pure, so none of this needs TeX. The real-pdflatex half lives in
|
|
9
9
|
* `test/e2e/build.mjs`.
|
|
10
10
|
*/
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
11
|
import { dirname, join } from "node:path";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { createChecker } from "../lib/check.mjs";
|
|
14
14
|
|
|
15
15
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const { nextStep, summarize, changedFiles, MAX_PASSES } = await import(
|
|
17
17
|
join(HERE, "latex-loop.ts")
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const check = (label, cond) => {
|
|
22
|
-
assert.ok(cond, label);
|
|
23
|
-
n++;
|
|
24
|
-
};
|
|
20
|
+
const check = createChecker();
|
|
25
21
|
|
|
26
22
|
// ── builders ────────────────────────────────────────────────────────────────────────────
|
|
27
23
|
const H = (aux, toc = null, out = null, bbl = null) => ({ aux, toc, out, bbl });
|
|
@@ -444,6 +440,11 @@ check(
|
|
|
444
440
|
changedFiles(H(null), H("a1")).includes("aux"),
|
|
445
441
|
);
|
|
446
442
|
|
|
443
|
+
check(
|
|
444
|
+
"a history of only a bibtex run that left every file as it was: nothing unsettled",
|
|
445
|
+
JSON.stringify(summarize([bibtex({ after: H("a1") })]).unsettled) === "[]",
|
|
446
|
+
);
|
|
447
|
+
|
|
447
448
|
console.log(
|
|
448
|
-
`✓ ${String(
|
|
449
|
+
`✓ ${String(check.count)} assertions passed — latex-loop: nextStep over State, summarize over histories, and the two composed, including the ${MAX_PASSES}-pass cap`,
|
|
449
450
|
);
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
* consumer.mjs — the same constant the linker and the install e2e read.
|
|
14
14
|
*
|
|
15
15
|
* Run: node src/link-skills.harness.mjs
|
|
16
|
-
* Killed by: src/link-skills.mutations.mjs
|
|
17
16
|
*/
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
17
|
import {
|
|
20
18
|
existsSync,
|
|
21
19
|
mkdirSync,
|
|
@@ -30,6 +28,7 @@ import {
|
|
|
30
28
|
import { tmpdir } from "node:os";
|
|
31
29
|
import { dirname, isAbsolute, join } from "node:path";
|
|
32
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { createChecker } from "../lib/check.mjs";
|
|
33
32
|
|
|
34
33
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
34
|
const { linkSkills, locatePackage, shippedSkills } = await import(
|
|
@@ -39,11 +38,7 @@ const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } = await import(
|
|
|
39
38
|
join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
|
|
40
39
|
);
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
const check = (label, cond) => {
|
|
44
|
-
n++;
|
|
45
|
-
assert.ok(cond, label);
|
|
46
|
-
};
|
|
41
|
+
const check = createChecker();
|
|
47
42
|
|
|
48
43
|
const SKILLS = ["alpha", "beta", "gamma"];
|
|
49
44
|
const work = realpathSync(
|
|
@@ -234,5 +229,5 @@ try {
|
|
|
234
229
|
}
|
|
235
230
|
|
|
236
231
|
console.log(
|
|
237
|
-
`✓ ${
|
|
232
|
+
`✓ ${check.count} assertions passed — link-skills: the skills are where Claude Code looks`,
|
|
238
233
|
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* link-skills.ts edges the harness does not reach: a resolver error without a code, a package that
|
|
3
|
+
* resolved outside every `node_modules` up the tree, a skills directory that is missing or holds a
|
|
4
|
+
* dangling link, a skills home that cannot be made, an entry that is a plain file, and — through
|
|
5
|
+
* the injected fs — an entry that cannot be resolved and a link that cannot be created.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import {
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
symlinkSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { test } from "vitest";
|
|
17
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
18
|
+
import { linkSkills, locatePackage, type LinkFs } from "./link-skills.ts";
|
|
19
|
+
|
|
20
|
+
const root = useTempDir("paperlint-link-skills-");
|
|
21
|
+
const realFs: LinkFs = { lstatSync, realpathSync, readlinkSync, symlinkSync };
|
|
22
|
+
|
|
23
|
+
/** A package directory shipping skills `names` under `skills/`. */
|
|
24
|
+
function pkg(name: string, names: string[]): string {
|
|
25
|
+
const dir = join(root, name, "pkg");
|
|
26
|
+
mkdirSync(join(dir, "skills"), { recursive: true });
|
|
27
|
+
writeTree(
|
|
28
|
+
dir,
|
|
29
|
+
Object.fromEntries(names.map((n) => [`skills/${n}/SKILL.md`, "# s\n"])),
|
|
30
|
+
);
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
const at = (dir: string) => () => ({ dir, spelled: dir });
|
|
34
|
+
|
|
35
|
+
test("locatePackage: a resolver error without a code; a package found outside every node_modules", () => {
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
locatePackage(root, {
|
|
38
|
+
resolveManifest: () => {
|
|
39
|
+
throw new Error("odd\nsecond line");
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
{ error: "error: odd" },
|
|
43
|
+
);
|
|
44
|
+
const elsewhere = pkg("elsewhere", []);
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
locatePackage(root, {
|
|
47
|
+
resolveManifest: () => join(elsewhere, "package.json"),
|
|
48
|
+
}),
|
|
49
|
+
{ dir: elsewhere, spelled: elsewhere },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a package without a skills directory, or one holding a dangling link, is not linked", () => {
|
|
54
|
+
const bare = join(root, "bare");
|
|
55
|
+
mkdirSync(bare, { recursive: true });
|
|
56
|
+
const missing = linkSkills(join(root, "p1"), { locate: at(bare) });
|
|
57
|
+
assert.deepEqual(missing, {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `the package's skills directory is missing: ${join(bare, "skills")}`,
|
|
60
|
+
});
|
|
61
|
+
const dangling = pkg("dangling", ["a"]);
|
|
62
|
+
symlinkSync(join(root, "gone"), join(dangling, "skills", "b"));
|
|
63
|
+
const r = linkSkills(join(root, "p2"), { locate: at(dangling) });
|
|
64
|
+
assert.equal(r.ok, false);
|
|
65
|
+
assert.match(!r.ok ? r.error : "", /^1 skill link\(s\) in .* lead nowhere:/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a skills home that cannot be made is named", () => {
|
|
69
|
+
const project = writeTree(join(root, "p3"), {
|
|
70
|
+
".claude": "a file, not a directory",
|
|
71
|
+
});
|
|
72
|
+
const r = linkSkills(project, { locate: at(pkg("home", ["a"])) });
|
|
73
|
+
assert.equal(r.ok, false);
|
|
74
|
+
assert.match(!r.ok ? r.error : "", /^cannot create .*\.claude\/skills: /);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("entries that are someone else's: a plain file, one that cannot be resolved, a link that cannot be made", () => {
|
|
78
|
+
const shipped = pkg("entries", ["file", "unresolved", "refused"]);
|
|
79
|
+
const project = writeTree(join(root, "p4"), { ".claude/skills/file": "x" });
|
|
80
|
+
mkdirSync(join(project, ".claude", "skills", "unresolved"));
|
|
81
|
+
const fs: LinkFs = {
|
|
82
|
+
...realFs,
|
|
83
|
+
realpathSync: (p) => {
|
|
84
|
+
if (p.endsWith("unresolved")) throw new Error("ELOOP");
|
|
85
|
+
return realpathSync(p);
|
|
86
|
+
},
|
|
87
|
+
symlinkSync: () => {
|
|
88
|
+
throw new Error("EEXIST: raced");
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const r = linkSkills(project, { locate: at(shipped), fs });
|
|
92
|
+
assert.deepEqual(r.ok ? r.links : [], [
|
|
93
|
+
{ name: "file", status: "foreign", reason: "a file" },
|
|
94
|
+
{
|
|
95
|
+
name: "refused",
|
|
96
|
+
status: "foreign",
|
|
97
|
+
reason: "could not create the link: EEXIST: raced",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "unresolved",
|
|
101
|
+
status: "foreign",
|
|
102
|
+
reason: "an entry that cannot be resolved",
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
});
|