@joshuafolkken/kit 1.1010.0 → 1.1035.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +1 -5
- package/.claude/skills/dependency-update/SKILL.md +6 -3
- package/.claude/skills/epic-commands/SKILL.md +46 -17
- package/.claude/skills/workflow-commands/SKILL.md +8 -23
- package/.claude/skills/workflow-commands/backlogrun-child.md +4 -2
- package/.claude/skills/workflow-commands/backlogrun.md +3 -12
- package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/fullrun.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/into-target.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +42 -43
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.codex/hooks.json +1 -5
- package/.github/workflows/sonar-qube.yml +1 -1
- package/AGENTS.md +4 -25
- package/CLAUDE.md +24 -38
- package/GEMINI.md +4 -24
- package/README.md +22 -56
- package/dist/CLAUDE.md +24 -38
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-WMIR5SNX.js → chunk-D67KVGGY.js} +3 -3
- package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-EIFCAJRB.js} +70 -27
- package/dist/hooks/{chunk-KPZN42RT.js → chunk-GJT4BNVE.js} +3 -3
- package/dist/hooks/{chunk-PAXMLGSS.js → chunk-GLBDBJOK.js} +44 -24
- package/dist/hooks/{chunk-GBJUERYK.js → chunk-HOYJU5RM.js} +1 -1
- package/dist/hooks/{chunk-6PSTVOLH.js → chunk-HU66PWWL.js} +33 -57
- package/dist/hooks/{chunk-A2H5I2VD.js → chunk-KG6PZD72.js} +2 -2
- package/dist/hooks/{chunk-AWTFKKFW.js → chunk-XPLP27WA.js} +3 -8
- package/dist/hooks/{chunk-4GDR2YTV.js → chunk-Y27M6A7X.js} +4 -4
- package/dist/hooks/{chunk-56LGMJJV.js → chunk-ZXNE6NY3.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +11 -15
- package/dist/josh.js +38 -19
- package/dist/version/index.js +93 -41
- package/eslint/base.js +2 -11
- package/package.json +31 -5
- package/prompts/coding-standards.md +15 -290
- package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
- package/prompts/collaboration-workflow/epic-audit.md +2 -53
- package/prompts/collaboration-workflow/epic-bundle.md +2 -106
- package/prompts/collaboration-workflow/issue-template.md +4 -4
- package/prompts/collaboration-workflow/operating-rules.md +18 -75
- package/prompts/collaboration-workflow/principles.md +74 -0
- package/prompts/collaboration-workflow/residency.md +54 -14
- package/prompts/collaboration-workflow/rule-delivery.md +1 -11
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow.md +22 -28
- package/scripts/adopt/adopt-logic.ts +2 -2
- package/scripts/adopt/adopt-toolkits.ts +1 -1
- package/scripts/adopt/adopt.ts +2 -2
- package/scripts/agent/agent-event.ts +2 -9
- package/scripts/agent/agent-exit-record.ts +2 -9
- package/scripts/backlog/backlog-budget-cli.ts +2 -6
- package/scripts/backlog/backlog-drive-cli.ts +20 -10
- package/scripts/backlog/backlog-drive.ts +50 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -5
- package/scripts/backlog/backlog-plan.ts +1 -1
- package/scripts/build/build-bin.ts +2 -1
- package/scripts/build/build-config-merge.ts +2 -1
- package/scripts/build/build-hooks.ts +24 -4
- package/scripts/build/build-managed-marker.ts +2 -1
- package/scripts/build/build-self-sync-guard.ts +2 -1
- package/scripts/build/build-version.ts +2 -1
- package/scripts/cases/cases-logic.ts +2 -6
- package/scripts/ci/pr-classification.ts +13 -11
- package/scripts/cost-runtime/cost-transcript.ts +4 -11
- package/scripts/doctor/doctor-consumer.ts +5 -12
- package/scripts/doctor/doctor-ports.ts +2 -2
- package/scripts/document/document-byte-budget.ts +8 -16
- package/scripts/document/document-section.ts +2 -6
- package/scripts/document/entry-read-budget.ts +1 -1
- package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
- package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
- package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
- package/scripts/epic/epic-audit-cli.ts +4 -4
- package/scripts/epic/epic-audit-orphans.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +1 -1
- package/scripts/epic/epic-audit.ts +1 -1
- package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
- package/scripts/epic/epic-bundle-cli.ts +1 -1
- package/scripts/epic/epic-bundle.ts +2 -2
- package/scripts/epic/epic-candidate-confirm.ts +2 -1
- package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
- package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
- package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
- package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
- package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
- package/scripts/epic/epic-cross-repo.ts +21 -38
- package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
- package/scripts/epic/epic-fetch.ts +3 -3
- package/scripts/epic/epic-graph.ts +3 -3
- package/scripts/epic/epic-index.ts +2 -2
- package/scripts/epic/epic-issue.ts +1 -1
- package/scripts/epic/epic-next.ts +3 -3
- package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
- package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
- package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
- package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
- package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
- package/scripts/epic/epic-relation-recheck.ts +4 -3
- package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
- package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
- package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
- package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
- package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
- package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
- package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
- package/{scripts-ai → scripts/epic}/epic.ts +17 -16
- package/scripts/gh/fix-gh-packages.ts +2 -1
- package/scripts/gh/gh-spawn.ts +12 -10
- package/scripts/git/constants.ts +1 -1
- package/scripts/git/git-branch.ts +10 -1
- package/scripts/git/git-command.ts +1 -1
- package/scripts/git/git-error.ts +2 -5
- package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
- package/scripts/git/git-followup-flush.ts +74 -48
- package/scripts/git/git-followup-issue-close.ts +2 -2
- package/scripts/git/git-followup-label.ts +1 -1
- package/scripts/git/git-followup-pending.ts +1 -1
- package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
- package/scripts/git/git-gh-exec.ts +85 -26
- package/scripts/git/git-gh-issue-read.ts +2 -2
- package/scripts/git/git-gh-issue-rest.ts +2 -2
- package/scripts/git/git-gh-issue-write.ts +6 -6
- package/scripts/git/git-gh-pr-read.ts +0 -21
- package/scripts/git/git-gh-pr.ts +2 -23
- package/scripts/git/git-pr-followup-wrapup.ts +2 -6
- package/scripts/git/git-preflight.ts +3 -6
- package/scripts/git/git-staging.ts +21 -9
- package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
- package/scripts/git/github-issue-url.ts +1 -1
- package/scripts/git/issue-labels.ts +1 -1
- package/scripts/git/issue-reference.ts +19 -0
- package/scripts/git/main-merge.ts +2 -1
- package/scripts/git/main-sync.ts +3 -2
- package/scripts/git/schemas.ts +1 -1
- package/scripts/git/stash-pop-args.ts +7 -13
- package/scripts/git/stash-pop-cli.ts +2 -1
- package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
- package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
- package/scripts/hooks/format-rewrite-notice.ts +2 -8
- package/scripts/init/init-ai-copy.ts +2 -5
- package/scripts/init/init.ts +8 -4
- package/scripts/init/start-steps.ts +2 -1
- package/scripts/init/start.ts +2 -1
- package/scripts/issue/issue-comment-cli.ts +2 -1
- package/scripts/issue/issue-file-cli.ts +2 -1
- package/scripts/issue/issue-fold-cli.ts +9 -16
- package/scripts/issue/issue-scout-cli.ts +8 -13
- package/scripts/issue/measure-rerun-cli.ts +7 -6
- package/scripts/josh/josh-commands-ai.ts +2 -2
- package/scripts/josh/josh-commands-backlog.ts +1 -1
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +4 -4
- package/scripts/lane/lane-cli.ts +2 -1
- package/scripts/lane/lane-open.ts +3 -2
- package/scripts/lane/lane-registry.ts +3 -6
- package/scripts/lib/cli-flags.ts +20 -2
- package/scripts/lib/error-message.ts +9 -0
- package/scripts/lib/read-file.ts +26 -2
- package/scripts/observations/observation-ledger-home.ts +98 -26
- package/scripts/observations/observation-ledger-migrate.ts +26 -21
- package/scripts/observations/observation-ledger-prepare.ts +10 -20
- package/scripts/observations/observation-ledger.ts +58 -25
- package/scripts/observations/observations-flush-cli.ts +6 -23
- package/scripts/observations/observations-flush-landing.ts +2 -1
- package/scripts/observations/observations-flush.ts +29 -20
- package/scripts/package/package-scout-cli.ts +8 -16
- package/scripts/ports/port-command.ts +2 -1
- package/scripts/propagate/propagate-publish.ts +6 -7
- package/scripts/propagate/propagate-steps.ts +2 -1
- package/scripts/propagate/propagate.ts +3 -2
- package/scripts/refactor/import-graph.ts +2 -6
- package/scripts/registry-migration/migrate.ts +3 -2
- package/scripts/registry-migration/user-npmrc.ts +2 -2
- package/scripts/release/release-cli.ts +2 -1
- package/scripts/repo/repo-setting.ts +10 -8
- package/scripts/review/review-finding-ledger.ts +3 -3
- package/scripts/review/review-findings-cli.ts +4 -11
- package/scripts/review/review-record-cli.ts +14 -11
- package/scripts/review/review-record.ts +6 -16
- package/scripts/rules/decision-oracle.ts +2 -2
- package/scripts/rules/rule-body-guard.ts +2 -2
- package/scripts/rules/rule-value-cache.ts +99 -0
- package/scripts/rules/rule-value-cli.ts +10 -9
- package/scripts/rules/rule-value.ts +1 -1
- package/scripts/run/run-carry-args.ts +2 -6
- package/scripts/run/run-carry-cli.ts +5 -7
- package/scripts/run/run-cut-args.ts +7 -13
- package/scripts/run/run-ending-cli.ts +4 -7
- package/scripts/run/run-event-stream.ts +2 -5
- package/scripts/run/run-liveness-cli.ts +4 -7
- package/scripts/run/run-progress-args.ts +2 -6
- package/scripts/run/run-progress-cli.ts +2 -1
- package/scripts/run/run-tail-cli.ts +3 -3
- package/scripts/run/run-tidy-stashes.ts +7 -14
- package/scripts/run/run-wake-cli.ts +2 -6
- package/scripts/run/run-wake-driver.ts +15 -2
- package/scripts/run/run-wake-session.ts +14 -4
- package/scripts/safe-chain/project-config.ts +3 -6
- package/scripts/security/security-audit-provision.ts +6 -7
- package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
- package/scripts/sync/directory-copy-guard.ts +2 -1
- package/scripts/sync/sync-ai-files.ts +3 -3
- package/scripts/sync/template-source-logic.ts +3 -4
- package/scripts/test/e2e-retry-check.ts +3 -6
- package/scripts/ui/ui-routes-cli.ts +3 -6
- package/scripts/version/bump-version.ts +2 -1
- package/scripts/version/version-remote.ts +18 -27
- package/scripts/version/version-targets.ts +3 -3
- package/.claude/skills/workflow-commands/rule-residency.md +0 -267
- package/evals/scenarios/consult-not-execute.json +0 -20
- package/evals/scenarios/no-implicit-workflow.json +0 -26
- package/evals/scenarios/overrides-both-locations.json +0 -29
- package/evals/scenarios/upstream-interrupt.json +0 -23
- package/evals/scenarios/workflow-skill-loaded.json +0 -16
- package/prompts/agent-lint-protocol.json +0 -79
- package/prompts/agent-rules.md +0 -63
- package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
- package/prompts/collaboration-workflow/distributed-docs.md +0 -7
- package/prompts/collaboration-workflow/durable-rules.md +0 -24
- package/prompts/collaboration-workflow/latest-first.md +0 -14
- package/prompts/collaboration-workflow/no-clones.md +0 -11
- package/prompts/collaboration-workflow/simplicity-first.md +0 -9
- package/prompts/collaboration-workflow/single-source-rules.md +0 -13
- package/scripts/build/shim-shell.ts +0 -25
- package/scripts/claude/skill-meta.ts +0 -37
- package/scripts/document/document-scan.ts +0 -108
- package/scripts/document/generate-catalog.ts +0 -69
- package/scripts/document/retired-phrases.ts +0 -143
- package/scripts/document/review-agent-type.ts +0 -46
- package/scripts/document/single-source.ts +0 -73
- package/scripts/git/git-fixture-workspace.ts +0 -88
- package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
- package/scripts/hooks/prompt-hooks.ts +0 -38
- package/scripts/init/hook-command-bootstrap.ts +0 -83
- package/scripts/issue/issue-logic.ts +0 -50
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
- package/scripts/josh/josh-script-reader.ts +0 -11
- package/scripts/package/production-dependency-graph.ts +0 -68
- package/scripts/retrospective/retrospective-cli.ts +0 -97
- package/scripts/retrospective/retrospective.ts +0 -127
- package/scripts/rules/delivered-rules-harness.ts +0 -72
- package/scripts/rules/top-level-scripts-guard.ts +0 -47
- package/scripts/run/run-carry-flush.ts +0 -58
- package/scripts/run/run-transcript.golden.txt +0 -57
- package/scripts/run/run-wake-work.ts +0 -108
- package/scripts/sync/dependabot-pin-logic.ts +0 -135
- package/scripts/test/classify-isolation.ts +0 -149
- package/scripts/test/http-fault-injection.ts +0 -79
- package/scripts/test/josh-harness-environment.ts +0 -260
- package/scripts/test/josh-harness.ts +0 -96
- package/scripts/test/pilot-files.ts +0 -452
- package/scripts/test/pilot-measure.ts +0 -78
- package/scripts/test/test-network-guard.ts +0 -453
- package/scripts/test/test-repository-guard.ts +0 -317
- package/scripts/test/test-state-guard.ts +0 -85
- package/scripts/test/test-stdout-guard.ts +0 -54
- package/scripts/test/test-telegram-guard.ts +0 -90
- package/scripts/test/unit-guard-environment.ts +0 -12
- package/scripts/test/unit-projects.ts +0 -139
- package/scripts/test/vitest-include-globs.ts +0 -12
- package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
- package/scripts-ai/completion-gate.ts +0 -13
- package/scripts-ai/environment-loader.ts +0 -9
- /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
- /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
- /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
accessSync,
|
|
3
|
-
chmodSync,
|
|
4
|
-
constants,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from 'node:fs'
|
|
10
|
-
import path from 'node:path'
|
|
11
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
-
import { shim_shell } from '#scripts/build/shim-shell'
|
|
13
|
-
import { platform_temporary } from '#scripts/josh/platform-temporary'
|
|
14
|
-
import { test_repository_guard, type WriteGuard } from './test-repository-guard'
|
|
15
|
-
import { GUARD_LOG_KEY } from './unit-guard-environment'
|
|
16
|
-
|
|
17
|
-
// The unit suite must not reach the network, and until joshuafolkken/kit#1353 nothing said so out
|
|
18
|
-
// loud. One test in `epic-bundle-cli.test.ts` mocked the listing read but not the per-issue relation
|
|
19
|
-
// read, so `fetch_backlog` called through to a live `gh api` against real issues — 768ms against its
|
|
20
|
-
// 26 siblings' 0–1ms, and a CI failure at the 10-second test timeout that had nothing to do with
|
|
21
|
-
// the change under test (PR #1351). Three more in `doctor-report.test.ts` drove `doctor.main()`
|
|
22
|
-
// without stubbing the Dependabot read and spawned `gh api …/automated-security-fixes` for real.
|
|
23
|
-
//
|
|
24
|
-
// Fixing those four is the repair; this file is what keeps it repaired. A one-off audit answers for
|
|
25
|
-
// the suite as it stands today and for nothing after it — and the failure mode is invisible by
|
|
26
|
-
// construction, because a test that calls through still *passes*, just slowly and against whatever
|
|
27
|
-
// GitHub happens to answer.
|
|
28
|
-
//
|
|
29
|
-
// **The guard is a `PATH` shim rather than a module mock.** `gh` is spawned from at least four
|
|
30
|
-
// places (`git-gh-exec.ts`, `gh-spawn.ts`, `git-gh-check.ts`, `repo-setting.ts`) through both
|
|
31
|
-
// `execa` and `execaSync`, and a test may also spawn a CLI subprocess that spawns `gh` in turn —
|
|
32
|
-
// which is exactly how the `doctor` cases reached the network. Intercepting the binary catches every
|
|
33
|
-
// one of those shapes, including the ones nobody thought of; mocking a module catches only the
|
|
34
|
-
// import graph the mock was written against.
|
|
35
|
-
//
|
|
36
|
-
// It is deliberately not `gh-subcommand-guard.ts`. That one reads source text and proves a call was
|
|
37
|
-
// *written*; this one observes the run and proves a call was *made*.
|
|
38
|
-
//
|
|
39
|
-
// **`git` is guarded too, and differently** (joshuafolkken/kit#1515). Guarding `gh` alone left the
|
|
40
|
-
// other way out of the machine wide open: `git-pr-followup.test.ts` and its stages suite drove
|
|
41
|
-
// `notify_completion`, whose release count fetches `origin/<default>` for real — 4.1s per test on the
|
|
42
|
-
// machine it was found on, fifteen tests in one file, against a 10s test timeout. The two files were
|
|
43
|
-
// half the unit suite's wall clock and failed the pre-push gate non-deterministically, on an idle
|
|
44
|
-
// machine as readily as under parallel load: 63.7s of wall clock against 14.9 CPU-seconds, three
|
|
45
|
-
// quarters of it spent waiting rather than computing. That is the same defect this file's header
|
|
46
|
-
// describes for `gh`, wearing a different binary's name, and the same one-off-audit argument applies
|
|
47
|
-
// — so the mechanism is reused rather than a second one written.
|
|
48
|
-
//
|
|
49
|
-
// **The `git` shim blocks by subcommand and passes everything else through**, which is the one way it
|
|
50
|
-
// differs from the `gh` shim. `gh` has no business in the unit suite at all, so its shim blocks
|
|
51
|
-
// outright; `git` is how half these tests read the repository they are about — `status`, `log`,
|
|
52
|
-
// `rev-parse`, the worktree machinery the lane suites drive — and a shim that refused those would
|
|
53
|
-
// stop the suite rather than the network. Only the subcommands that open a connection are refused,
|
|
54
|
-
// and the rest `exec` the real binary at the absolute path resolved before `PATH` was touched, so a
|
|
55
|
-
// subprocess that spawns `git` in turn is still behind the shim.
|
|
56
|
-
//
|
|
57
|
-
// **The `git` shim also refuses a write into the repository the suite is running in**
|
|
58
|
-
// (joshuafolkken/kit#1530). That is a second dimension rather than a second guard: the arming, the
|
|
59
|
-
// `PATH` shim, the shared record and the teardown that throws are all this file's, and only the `sh`
|
|
60
|
-
// that decides comes from `test-repository-guard.ts`, which is where the defect, the two findings
|
|
61
|
-
// and what is deliberately left unguarded are written down. Sharing the record is what lets one run
|
|
62
|
-
// report both kinds — `describe_violations` puts each under its own heading rather than filing a
|
|
63
|
-
// stray commit under "mock the read".
|
|
64
|
-
//
|
|
65
|
-
// **Every failure mode here has to be loud, because the guard's own defects fail open.** A shim that
|
|
66
|
-
// cannot run, a log that cannot be read and a clean run all look alike from the outside — so the
|
|
67
|
-
// path is quoted rather than interpolated, and an unreadable record throws instead of reading as
|
|
68
|
-
// "no violations".
|
|
69
|
-
//
|
|
70
|
-
// **On Windows it guards nothing, knowingly.** PATH resolution there selects `gh.exe` / `gh.cmd` and
|
|
71
|
-
// never a `#!/bin/sh` file named `gh`, so the suite would reach the network with the guard reporting
|
|
72
|
-
// clean. CI and every current checkout are POSIX; a `.cmd` companion is what to add if that changes.
|
|
73
|
-
|
|
74
|
-
const PROJECT_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
75
|
-
// Keyed by the process that armed it. `josh gate`'s unit step and a `josh test:related` run can be
|
|
76
|
-
// in flight in the same checkout at once, and one shared log means the second run truncates the
|
|
77
|
-
// first one's records and then inherits whatever the other recorded.
|
|
78
|
-
//
|
|
79
|
-
// The prefix is named on its own because two readers need it without rebuilding the whole path: a
|
|
80
|
-
// test recognizing an armed directory on `PATH` — the pid in it is `globalSetup`'s, and a worker
|
|
81
|
-
// asking has a different one — and `resolve_binary`, which must not find a shim where it is looking
|
|
82
|
-
// for the real binary.
|
|
83
|
-
const GUARD_PREFIX = 'unit-network-guard-'
|
|
84
|
-
const GUARD_DIRECTORY = path.join(
|
|
85
|
-
PROJECT_ROOT,
|
|
86
|
-
'node_modules',
|
|
87
|
-
'.cache',
|
|
88
|
-
`${GUARD_PREFIX}${String(process.pid)}`,
|
|
89
|
-
)
|
|
90
|
-
// Where the run's host-shared records land instead of the real temp root (joshuafolkken/kit#2494,
|
|
91
|
-
// `platform-temporary.ts`). **Beside the real root rather than inside `GUARD_DIRECTORY`**, because
|
|
92
|
-
// some of those records are Unix sockets, and a socket path under a checkout's `node_modules/.cache`
|
|
93
|
-
// runs past the 104-byte limit macOS puts on one. The platform root is resolved directly, not read
|
|
94
|
-
// from `PLATFORM_TEMP_ROOT`, so a suite nested inside another's test does not nest its root.
|
|
95
|
-
const TEMPORARY_ROOT = path.join(
|
|
96
|
-
platform_temporary.resolve_temporary_root(process.platform),
|
|
97
|
-
`${GUARD_PREFIX}${String(process.pid)}`,
|
|
98
|
-
)
|
|
99
|
-
// One record for both shims. Each line starts with the binary's own name, so a mixed run still says
|
|
100
|
-
// which call was which, and one log means `disarm` has one place to look.
|
|
101
|
-
const LOG_NAME = 'network-calls.log'
|
|
102
|
-
// The binaries being stood in for. Single-sourced because each is two things at once: the file name
|
|
103
|
-
// that has to shadow the real one on `PATH`, and the word each recorded line starts with.
|
|
104
|
-
const SHIM_NAME = 'gh'
|
|
105
|
-
const GIT_SHIM_NAME = 'git'
|
|
106
|
-
// Owner-writable, everyone-executable: the shim is spawned by the test workers, never edited.
|
|
107
|
-
const SHIM_MODE = 0o755
|
|
108
|
-
|
|
109
|
-
// The `git` subcommands that open a connection. Everything absent from this list is local and is
|
|
110
|
-
// handed to the real binary untouched — see the header on why this shim blocks by subcommand where
|
|
111
|
-
// the `gh` one blocks outright.
|
|
112
|
-
//
|
|
113
|
-
// **`remote`, `submodule` and `archive` are refused whole, though each has local spellings.**
|
|
114
|
-
// `remote update` and `remote prune` reach the network while `remote -v` does not; `submodule update`
|
|
115
|
-
// fetches while `submodule status` does not; `archive --remote=` does while a plain `archive` does
|
|
116
|
-
// not. Splitting on the flag would put that judgement inside a `sh` `case`, and getting it wrong there
|
|
117
|
-
// fails **open** — the call goes out and the log stays empty, which is the one failure mode this file
|
|
118
|
-
// exists to make impossible. Refusing the whole subcommand fails closed instead, and costs nothing
|
|
119
|
-
// real: the unit suite has no business shelling out to any of them, and none of the repository's
|
|
120
|
-
// tests does.
|
|
121
|
-
const GIT_NETWORK_SUBCOMMANDS: ReadonlyArray<string> = [
|
|
122
|
-
'archive',
|
|
123
|
-
'clone',
|
|
124
|
-
'fetch',
|
|
125
|
-
'ls-remote',
|
|
126
|
-
'pull',
|
|
127
|
-
'push',
|
|
128
|
-
'remote',
|
|
129
|
-
'send-email',
|
|
130
|
-
'submodule',
|
|
131
|
-
]
|
|
132
|
-
// Git's global options that take their value as a *separate* word. Without them `git -C <dir> fetch`
|
|
133
|
-
// reads `<dir>` as the subcommand, finds it on no list, and lets the fetch through — which is the
|
|
134
|
-
// precise shape of failure this guard exists to notice. The `--option=value` spellings need no entry:
|
|
135
|
-
// they are one word, and the `-*` arm already skips them.
|
|
136
|
-
const GIT_VALUE_OPTIONS: ReadonlyArray<string> = [
|
|
137
|
-
'-C',
|
|
138
|
-
'-c',
|
|
139
|
-
'--git-dir',
|
|
140
|
-
'--namespace',
|
|
141
|
-
'--super-prefix',
|
|
142
|
-
'--work-tree',
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
const BLOCKED_MESSAGE = 'josh: the unit suite must not reach the network — mock the read instead'
|
|
146
|
-
const VIOLATION_HEADING =
|
|
147
|
-
'The unit suite made a live network call. Mock the read in the test that made it:'
|
|
148
|
-
const UNREADABLE_LOG_HEADING =
|
|
149
|
-
'The unit-suite network guard could not read its own record, so the run proves nothing:'
|
|
150
|
-
|
|
151
|
-
// The three lines a block writes, shared by both shims so a `git` violation is recorded, explained
|
|
152
|
-
// and failed exactly as a `gh` one is. `$*` rather than `$@` because the record is a line of prose
|
|
153
|
-
// for a person to read, not something re-executed.
|
|
154
|
-
function blocking_lines(log_file: string, name: string): Array<string> {
|
|
155
|
-
return [
|
|
156
|
-
shim_shell.record_line(log_file, `${name} $*`),
|
|
157
|
-
`echo ${shim_shell.quoted(BLOCKED_MESSAGE)} >&2`,
|
|
158
|
-
'exit 1',
|
|
159
|
-
]
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// The shim records the invocation and fails, so a caller that ignores the exit code still leaves a
|
|
163
|
-
// trace. `exit 1` rather than `exit 0`: a spawn that succeeds with empty output is indistinguishable
|
|
164
|
-
// from a repository that answers nothing, and the reads here degrade that into "unreadable" — which
|
|
165
|
-
// is what let the original defect pass as a green test for as long as it did.
|
|
166
|
-
function shim_script(log_file: string): string {
|
|
167
|
-
return ['#!/bin/sh', ...blocking_lines(log_file, SHIM_NAME), ''].join('\n')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// The `git` shim: find the subcommand, refuse it if it opens a connection, otherwise `exec` the real
|
|
171
|
-
// binary. **The scan is what makes it correct rather than the list** — git accepts its own global
|
|
172
|
-
// options before the subcommand, so the first word is not necessarily the subcommand, and a shim that
|
|
173
|
-
// read `$1` would pass `git -C <dir> fetch` straight through. `skip` consumes the value of an option
|
|
174
|
-
// that takes one; `-*` skips the rest; the first bare word is the subcommand, and reaching it without
|
|
175
|
-
// a match ends the scan.
|
|
176
|
-
//
|
|
177
|
-
// `exec` rather than a call, so the real `git`'s exit code and signals are the shim's own — a caller
|
|
178
|
-
// checking either must not be able to tell the shim is there.
|
|
179
|
-
// The writing arm, from joshuafolkken/kit#1530. Unlike the network arm it does not refuse on sight:
|
|
180
|
-
// the `sh` it splices in decides, and `break` ends the scan when it lets the command through — the
|
|
181
|
-
// subcommand has been found either way.
|
|
182
|
-
function guarded_arm(
|
|
183
|
-
subcommands: ReadonlyArray<string>,
|
|
184
|
-
body: ReadonlyArray<string>,
|
|
185
|
-
): Array<string> {
|
|
186
|
-
return [
|
|
187
|
-
`\t\t${subcommands.join('|')})`,
|
|
188
|
-
...body.map((line) => `\t\t\t${line}`),
|
|
189
|
-
'\t\t\tbreak',
|
|
190
|
-
'\t\t\t;;',
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// The two arms from joshuafolkken/kit#1530. Unlike the network arm neither refuses on sight — the
|
|
195
|
-
// `sh` spliced in decides — and each ends in `break`, because the subcommand has been found whether
|
|
196
|
-
// or not it was let through.
|
|
197
|
-
function git_write_arms(guard: WriteGuard): Array<string> {
|
|
198
|
-
return [
|
|
199
|
-
...guarded_arm(
|
|
200
|
-
test_repository_guard.GIT_WRITING_SUBCOMMANDS,
|
|
201
|
-
test_repository_guard.write_guard_lines(guard),
|
|
202
|
-
),
|
|
203
|
-
...guarded_arm(
|
|
204
|
-
test_repository_guard.GIT_AMBIGUOUS_SUBCOMMANDS,
|
|
205
|
-
test_repository_guard.environment_guard_lines(guard.log_file),
|
|
206
|
-
),
|
|
207
|
-
]
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// The scan itself: `skip` consumes the value of an option that takes one, `guard_seen` counts the
|
|
211
|
-
// words git's own global options took, and the first bare word ends it.
|
|
212
|
-
function git_scan_lines(guard: WriteGuard): Array<string> {
|
|
213
|
-
return [
|
|
214
|
-
'for arg in "$@"; do',
|
|
215
|
-
'\tif [ "$skip" = 1 ]; then skip=0; guard_seen=$((guard_seen + 1)); continue; fi',
|
|
216
|
-
'\tcase "$arg" in',
|
|
217
|
-
`\t\t${GIT_VALUE_OPTIONS.join('|')}) skip=1 ;;`,
|
|
218
|
-
`\t\t${GIT_NETWORK_SUBCOMMANDS.join('|')})`,
|
|
219
|
-
...blocking_lines(guard.log_file, GIT_SHIM_NAME).map((line) => `\t\t\t${line}`),
|
|
220
|
-
'\t\t\t;;',
|
|
221
|
-
...git_write_arms(guard),
|
|
222
|
-
'\t\t-*) ;;',
|
|
223
|
-
'\t\t*) break ;;',
|
|
224
|
-
'\tesac',
|
|
225
|
-
'\tguard_seen=$((guard_seen + 1))',
|
|
226
|
-
'done',
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function git_shim_script(
|
|
231
|
-
log_file: string,
|
|
232
|
-
git_binary: string,
|
|
233
|
-
guarded_directory: string | undefined,
|
|
234
|
-
): string {
|
|
235
|
-
return [
|
|
236
|
-
'#!/bin/sh',
|
|
237
|
-
...test_repository_guard.prelude_lines(git_binary, guarded_directory),
|
|
238
|
-
'skip=0',
|
|
239
|
-
'guard_seen=0',
|
|
240
|
-
...git_scan_lines({ guarded_directory, log_file }),
|
|
241
|
-
`exec ${shim_shell.quoted(git_binary)} "$@"`,
|
|
242
|
-
'',
|
|
243
|
-
].join('\n')
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// The real binary, found by walking `PATH` the way the shell would, and written into the shim as an
|
|
247
|
-
// absolute path. That is what lets `PATH` keep the guard directory in front: a subprocess spawned by
|
|
248
|
-
// a test inherits the shim rather than escaping to the real `git` behind it.
|
|
249
|
-
//
|
|
250
|
-
// **An armed guard directory is skipped, and that is not belt-and-braces.** `arm` puts one at the
|
|
251
|
-
// front of `PATH` and every test worker inherits it, so this module loaded inside a worker would find
|
|
252
|
-
// `<guard>/git` — and a shim built from that answer `exec`s a shim, which at the default directory is
|
|
253
|
-
// itself: an unbounded loop, and a `GIT_BINARY` assertion that proves only that the shim exists.
|
|
254
|
-
function is_guard_directory(directory: string): boolean {
|
|
255
|
-
return path.basename(directory).startsWith(GUARD_PREFIX)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function is_executable(candidate: string): boolean {
|
|
259
|
-
try {
|
|
260
|
-
accessSync(candidate, constants.X_OK)
|
|
261
|
-
|
|
262
|
-
return true
|
|
263
|
-
} catch {
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function holds_binary(directory: string, name: string): boolean {
|
|
269
|
-
return !is_guard_directory(directory) && is_executable(path.join(directory, name))
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function resolve_binary(
|
|
273
|
-
name: string,
|
|
274
|
-
search_path: string = process.env['PATH'] ?? '',
|
|
275
|
-
): string | undefined {
|
|
276
|
-
const directory = search_path.split(path.delimiter).find((entry) => holds_binary(entry, name))
|
|
277
|
-
|
|
278
|
-
return directory === undefined ? undefined : path.join(directory, name)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const GIT_BINARY = resolve_binary(GIT_SHIM_NAME)
|
|
282
|
-
|
|
283
|
-
// The repository a write is refused against, resolved once here rather than in the shim: the answer
|
|
284
|
-
// is the same for every call of the run, and asking git for it costs a process. `undefined` where
|
|
285
|
-
// `PROJECT_ROOT` is not a repository at all — see `test-repository-guard.ts` on why that leaves the
|
|
286
|
-
// inherited-environment finding armed and only the resolution one out.
|
|
287
|
-
const GUARDED_GIT_DIRECTORY = test_repository_guard.guarded_git_directory(GIT_BINARY, PROJECT_ROOT)
|
|
288
|
-
|
|
289
|
-
// The recorded invocations, blank lines dropped. Split out from the read so the parsing is testable
|
|
290
|
-
// without a run: an empty log and a log of one empty line must not read the same.
|
|
291
|
-
function calls_of(log_text: string): Array<string> {
|
|
292
|
-
return log_text
|
|
293
|
-
.split('\n')
|
|
294
|
-
.map((line) => line.trim())
|
|
295
|
-
.filter((line) => line !== '')
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Each distinct command once, with its count — a single loop repeated 40 times is one defect, and a
|
|
299
|
-
// list of 40 identical lines hides the second one under it.
|
|
300
|
-
function counted_lines(calls: ReadonlyArray<string>): Array<string> {
|
|
301
|
-
const counts = new Map<string, number>()
|
|
302
|
-
|
|
303
|
-
for (const call of calls) counts.set(call, (counts.get(call) ?? 0) + 1)
|
|
304
|
-
|
|
305
|
-
return [...counts].map(([call, count]) => ` ${String(count)}x ${call}`)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// A heading and its calls, or nothing at all when there are none.
|
|
309
|
-
function section(heading: string, calls: ReadonlyArray<string>): Array<string> {
|
|
310
|
-
return calls.length === 0 ? [] : [heading, ...counted_lines(calls)]
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// One section per kind. The two guards share one record, so a run that both reached the network and
|
|
314
|
-
// wrote into its own repository has to report each under its own heading — filing one under the
|
|
315
|
-
// other's would tell the reader to mock a read that was never made (joshuafolkken/kit#1530).
|
|
316
|
-
function describe_violations(calls: ReadonlyArray<string>): string {
|
|
317
|
-
const writes = calls.filter((call) => test_repository_guard.is_write_violation(call))
|
|
318
|
-
const network = calls.filter((call) => !test_repository_guard.is_write_violation(call))
|
|
319
|
-
|
|
320
|
-
return [
|
|
321
|
-
...section(VIOLATION_HEADING, network),
|
|
322
|
-
...section(test_repository_guard.WRITE_VIOLATION_HEADING, writes),
|
|
323
|
-
].join('\n')
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// One armed guard is one directory holding both the shim and its record, so the directory is the
|
|
327
|
-
// only thing any of the calls below take. Derived rather than passed alongside it: a `disarm` given
|
|
328
|
-
// a log file that lived somewhere else would recursively delete whatever directory that was in.
|
|
329
|
-
function log_in(directory: string): string {
|
|
330
|
-
return path.join(directory, LOG_NAME)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function write_executable(shim_file: string, script: string): void {
|
|
334
|
-
writeFileSync(shim_file, script)
|
|
335
|
-
chmodSync(shim_file, SHIM_MODE)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// **Skipped rather than faked when the real `git` cannot be found.** A shim with nothing to `exec`
|
|
339
|
-
// would fail every local `git` in the suite, which is a far worse answer than leaving this one binary
|
|
340
|
-
// unguarded — and it cannot happen in a checkout, since finding no `git` here means there was no
|
|
341
|
-
// `git` to clone with. The branch is a safety net, not a supported mode.
|
|
342
|
-
function install_git_shim(directory: string, guarded_directory: string | undefined): void {
|
|
343
|
-
if (GIT_BINARY === undefined) return
|
|
344
|
-
|
|
345
|
-
write_executable(
|
|
346
|
-
path.join(directory, GIT_SHIM_NAME),
|
|
347
|
-
git_shim_script(log_in(directory), GIT_BINARY, guarded_directory),
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// The shims, written where they will be spawned from. Shared with this module's own test rather than
|
|
352
|
-
// re-written there: a test that builds its own copy proves that copy works and says nothing about
|
|
353
|
-
// the one the suite actually runs behind.
|
|
354
|
-
// **`undefined` here means "there is no repository to guard", and a parameter default could not say
|
|
355
|
-
// that** — a JavaScript default fires on an explicit `undefined` too, so a caller asking for the
|
|
356
|
-
// no-repository shim silently got one guarding the real checkout, and the branch that omits the
|
|
357
|
-
// resolver had no test at all (review round 2 of joshuafolkken/kit#1530). The default lives in the
|
|
358
|
-
// wrapper below instead, where it is applied rather than defaulted.
|
|
359
|
-
function install_shim_guarding(directory: string, guarded_directory: string | undefined): string {
|
|
360
|
-
const shim_file = path.join(directory, SHIM_NAME)
|
|
361
|
-
|
|
362
|
-
mkdirSync(directory, { recursive: true })
|
|
363
|
-
writeFileSync(log_in(directory), '')
|
|
364
|
-
write_executable(shim_file, shim_script(log_in(directory)))
|
|
365
|
-
install_git_shim(directory, guarded_directory)
|
|
366
|
-
|
|
367
|
-
return shim_file
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function install_shim(directory: string = GUARD_DIRECTORY): string {
|
|
371
|
-
return install_shim_guarding(directory, GUARDED_GIT_DIRECTORY)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// `undefined` for a record that could not be read, which is not the same answer as an empty one:
|
|
375
|
-
// the shim runs in its own process and the test workers are processes again, so the only evidence
|
|
376
|
-
// this side has is the file — and "the file is not there" says nothing about what the run did.
|
|
377
|
-
function read_log(log_file: string): string | undefined {
|
|
378
|
-
try {
|
|
379
|
-
return readFileSync(log_file, 'utf8')
|
|
380
|
-
} catch {
|
|
381
|
-
return undefined
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// `globalSetup` runs before any worker is forked, so the workers inherit this `PATH` and every `gh`
|
|
386
|
-
// and `git` they spawn — directly or through a CLI subprocess of their own — resolves to a shim. They
|
|
387
|
-
// inherit the record's path for the in-process Telegram guard and the redirected temp root with it.
|
|
388
|
-
function arm(directory: string = GUARD_DIRECTORY, temporary_root: string = TEMPORARY_ROOT): void {
|
|
389
|
-
install_shim(directory)
|
|
390
|
-
mkdirSync(temporary_root, { recursive: true })
|
|
391
|
-
process.env['PATH'] = `${directory}${path.delimiter}${process.env['PATH'] ?? ''}`
|
|
392
|
-
process.env[GUARD_LOG_KEY] = log_in(directory)
|
|
393
|
-
process.env[platform_temporary.TEMP_ROOT_KEY] = temporary_root
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Thrown rather than logged: a warning on a suite that already exited 0 is a warning nobody reads,
|
|
397
|
-
// which is the state joshuafolkken/kit#1353 was filed from. The directory goes either way, so a
|
|
398
|
-
// failing run does not leave the next one reading its records.
|
|
399
|
-
function disarm(directory: string = GUARD_DIRECTORY): void {
|
|
400
|
-
const log_text = read_log(log_in(directory))
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
if (log_text === undefined) throw new Error(`${UNREADABLE_LOG_HEADING} ${log_in(directory)}`)
|
|
404
|
-
|
|
405
|
-
const calls = calls_of(log_text)
|
|
406
|
-
|
|
407
|
-
if (calls.length > 0) throw new Error(describe_violations(calls))
|
|
408
|
-
} finally {
|
|
409
|
-
rmSync(directory, { recursive: true, force: true })
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Vitest's `globalSetup` contract: a named `setup` whose return value becomes the teardown.
|
|
414
|
-
function teardown(): void {
|
|
415
|
-
try {
|
|
416
|
-
disarm()
|
|
417
|
-
} finally {
|
|
418
|
-
rmSync(TEMPORARY_ROOT, { recursive: true, force: true })
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function setup(): () => void {
|
|
423
|
-
arm()
|
|
424
|
-
|
|
425
|
-
return teardown
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const test_network_guard = {
|
|
429
|
-
BLOCKED_MESSAGE,
|
|
430
|
-
GIT_BINARY,
|
|
431
|
-
GIT_NETWORK_SUBCOMMANDS,
|
|
432
|
-
GIT_SHIM_NAME,
|
|
433
|
-
GIT_VALUE_OPTIONS,
|
|
434
|
-
GUARDED_GIT_DIRECTORY,
|
|
435
|
-
GUARD_DIRECTORY,
|
|
436
|
-
GUARD_PREFIX,
|
|
437
|
-
SHIM_NAME,
|
|
438
|
-
UNREADABLE_LOG_HEADING,
|
|
439
|
-
VIOLATION_HEADING,
|
|
440
|
-
arm,
|
|
441
|
-
calls_of,
|
|
442
|
-
describe_violations,
|
|
443
|
-
disarm,
|
|
444
|
-
git_shim_script,
|
|
445
|
-
install_shim,
|
|
446
|
-
install_shim_guarding,
|
|
447
|
-
log_in,
|
|
448
|
-
resolve_binary,
|
|
449
|
-
shim_script,
|
|
450
|
-
TEMPORARY_ROOT,
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export { setup, test_network_guard }
|