@joshuafolkken/kit 1.1010.0 → 1.1050.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/verify-ui/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +10 -25
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
- package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
- package/.claude/skills/workflow-commands/backlogrun.md +3 -12
- package/.claude/skills/workflow-commands/chain-rule.md +2 -2
- package/.claude/skills/workflow-commands/followup-reference.md +2 -2
- 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/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +80 -126
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
- package/.codex/hooks.json +1 -5
- package/.github/workflows/auto-tag.yml +21 -0
- package/.github/workflows/publish.yml +9 -10
- 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-6PSTVOLH.js → chunk-5BK3K7V4.js} +114 -90
- package/dist/hooks/{chunk-PAXMLGSS.js → chunk-7VPDUBRN.js} +93 -175
- package/dist/hooks/{chunk-KPZN42RT.js → chunk-DMQN45FN.js} +3 -3
- package/dist/hooks/{chunk-WMIR5SNX.js → chunk-EJDX2TOB.js} +13 -3
- package/dist/hooks/{chunk-A2H5I2VD.js → chunk-GQ7KP45M.js} +2 -2
- package/dist/hooks/{chunk-4GDR2YTV.js → chunk-HJIISEAC.js} +4 -4
- package/dist/hooks/{chunk-AWTFKKFW.js → chunk-NAS4C7GP.js} +6 -33
- package/dist/hooks/{chunk-56LGMJJV.js → chunk-NJQOBE6L.js} +2 -2
- package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-RLDX5SOC.js} +1222 -941
- package/dist/hooks/{chunk-GBJUERYK.js → chunk-WQ4OSBSS.js} +10 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +15 -34
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +23 -16
- package/dist/hooks/stop-guard.js +21 -200
- package/dist/josh.js +42 -125
- package/dist/version/index.js +466 -42
- package/eslint/base.js +2 -11
- package/package.json +36 -10
- 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/file-edits.md +6 -53
- 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 +55 -15
- package/prompts/collaboration-workflow/rule-delivery.md +5 -46
- package/prompts/collaboration-workflow/shell-body.md +7 -9
- package/prompts/collaboration-workflow/turn-batching.md +21 -37
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +15 -18
- package/prompts/collaboration-workflow.md +22 -28
- package/prompts/sonar-hotspot-handling.md +1 -1
- package/scripts/adopt/adopt-logic.ts +2 -2
- package/scripts/adopt/adopt-toolkits.ts +3 -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/auto-ok/auto-ok-cli.ts +9 -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-next.ts +24 -8
- package/scripts/backlog/backlog-offer-cli.ts +6 -5
- package/scripts/backlog/backlog-plan-cli.ts +5 -1
- package/scripts/backlog/backlog-plan.ts +1 -1
- package/scripts/backlog/backlog-pool.ts +31 -11
- package/scripts/backlog/backlog-rank.ts +106 -0
- package/scripts/backlog/backlog-waves.ts +42 -29
- package/scripts/build/build-bin.ts +2 -1
- package/scripts/build/build-config-merge.ts +2 -1
- package/scripts/build/build-hooks.ts +31 -6
- 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/delegation/delegation-policy.ts +11 -0
- package/scripts/doctor/doctor-consumer.ts +5 -12
- package/scripts/doctor/doctor-ports.ts +2 -2
- package/scripts/document/document-byte-budget.ts +13 -21
- package/scripts/document/document-section.ts +2 -6
- package/scripts/document/entry-read-budget.ts +6 -5
- 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-lane-offer.ts +11 -22
- 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/epic/epic-rank.ts +37 -0
- 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/epic/epic-solo.ts +29 -28
- 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 +2 -2
- package/scripts/git/git-error.ts +2 -5
- package/scripts/git/git-execa-error.ts +6 -1
- 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-next-issues.ts +34 -5
- package/scripts/git/git-pr-followup-wrapup.ts +2 -6
- package/scripts/git/git-preflight.ts +25 -10
- package/scripts/git/git-push-transport.ts +12 -69
- package/scripts/git/git-spawn.ts +79 -3
- package/scripts/git/git-ssh-keepalive.ts +74 -0
- 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/gone-branch.ts +3 -2
- package/scripts/git/issue-labels.ts +13 -1
- package/scripts/git/issue-rank.ts +89 -0
- 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-edited-cli.ts +15 -0
- package/scripts/hooks/format-edited-file.ts +2 -9
- package/scripts/hooks/format-rewrite-notice.ts +2 -8
- package/scripts/hooks/pretool-guard-cli.ts +21 -0
- package/scripts/hooks/pretool-guard.ts +14 -15
- package/scripts/init/init-ai-copy.ts +2 -5
- package/scripts/init/init.ts +8 -4
- package/scripts/init/start-exec.ts +7 -1
- package/scripts/init/start-plan.ts +18 -5
- package/scripts/init/start-setup-pr.ts +46 -19
- package/scripts/init/start-steps.ts +43 -10
- package/scripts/init/start.ts +42 -11
- package/scripts/issue/issue-cite.ts +19 -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-command-map.ts +0 -102
- 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 +3 -3
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +4 -4
- package/scripts/lane/lane-child-marker.ts +16 -2
- package/scripts/lane/lane-cli.ts +34 -15
- package/scripts/lane/lane-open.ts +3 -2
- package/scripts/lane/lane-registry.ts +3 -6
- package/scripts/lane/lane-report.ts +10 -4
- 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-plan.ts +185 -0
- package/scripts/propagate/propagate-publish.ts +6 -7
- package/scripts/propagate/propagate-run.ts +5 -1
- package/scripts/propagate/propagate-steps.ts +18 -2
- package/scripts/propagate/propagate-targets.ts +13 -2
- package/scripts/propagate/propagate.ts +63 -44
- package/scripts/refactor/import-graph.ts +2 -6
- package/scripts/registry-migration/migrate-logic.ts +1 -0
- package/scripts/registry-migration/migrate.ts +3 -2
- package/scripts/registry-migration/user-npmrc.ts +2 -2
- package/scripts/release/github-release.ts +1 -1
- package/scripts/release/release-cli.ts +2 -1
- package/scripts/repo/repo-setting.ts +10 -8
- package/scripts/review/review-attest.ts +44 -0
- package/scripts/review/review-brief-cli.ts +49 -2
- package/scripts/review/review-brief.ts +46 -1
- 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/review/review-stamps.ts +7 -0
- package/scripts/rules/decision-oracle.ts +2 -2
- package/scripts/rules/implementation-cut.ts +1 -1
- package/scripts/rules/issue-fold.ts +1 -2
- package/scripts/rules/lane-carry-conflict.ts +1 -2
- package/scripts/rules/lane-park.ts +1 -2
- package/scripts/rules/pre-gate-cut.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-headless.ts +22 -6
- package/scripts/run/run-liveness-cli.ts +4 -7
- package/scripts/run/run-parent-cut-hook.ts +144 -0
- package/scripts/run/run-progress-args.ts +2 -6
- package/scripts/run/run-progress-cli.ts +2 -1
- package/scripts/run/run-progress-read.ts +2 -1
- package/scripts/run/run-progress.ts +21 -8
- package/scripts/run/run-ship-cli.ts +6 -74
- package/scripts/run/run-ship-preflight.ts +87 -0
- package/scripts/run/run-ship-review-steps.ts +5 -26
- package/scripts/run/run-ship-review.ts +9 -0
- package/scripts/run/run-ship-scoped.ts +44 -0
- package/scripts/run/run-ship-stage.ts +5 -1
- package/scripts/run/run-ship-steps.ts +105 -0
- package/scripts/run/run-ship.ts +2 -0
- 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 +4 -8
- package/scripts/run/run-wake-describe.ts +19 -1
- package/scripts/run/run-wake-driver.ts +15 -2
- package/scripts/run/run-wake-session.ts +14 -4
- package/scripts/run/run-watcher-hook.ts +1 -1
- 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/time-runtime/time-bundle-call.ts +2 -2
- package/scripts/ui/ui-routes-cli.ts +3 -6
- package/scripts/version/bump-version.ts +2 -1
- package/scripts/version/npm-registry.ts +97 -0
- package/scripts/version/version-commands.ts +5 -4
- package/scripts/version/version-remote.ts +38 -33
- 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,317 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import { shim_shell } from '#scripts/build/shim-shell'
|
|
3
|
-
import {
|
|
4
|
-
git_location_environment,
|
|
5
|
-
GIT_LOCATION_VARIABLES,
|
|
6
|
-
} from '#scripts/git/git-location-environment'
|
|
7
|
-
|
|
8
|
-
// **A unit test must not write into the repository the suite is running in**, and until
|
|
9
|
-
// joshuafolkken/kit#1530 nothing said so out loud.
|
|
10
|
-
//
|
|
11
|
-
// `lane-change-base.test.ts` builds a real git topology in a temp directory and hands its helper
|
|
12
|
-
// `{ cwd }` and nothing else. Run from the pre-push hook it inherited `GIT_DIR` and `GIT_INDEX_FILE`
|
|
13
|
-
// from the hook environment — both of which beat `cwd` — so the fixture's own commits landed on the
|
|
14
|
-
// branch being pushed. `1085-lane`'s reflog carries three of them, titled `base` and
|
|
15
|
-
// `another lane merged`, 26 seconds after the run's real commit; `1080-lane` took one. The same
|
|
16
|
-
// helper's `git config user.email` wrote a fixture identity into `--local` config, which every
|
|
17
|
-
// linked work tree of the repository shares, so all five checkouts on the machine started authoring
|
|
18
|
-
// commits as `Lane Fixture <lane@example.test>`.
|
|
19
|
-
//
|
|
20
|
-
// **This is the second instance of one class in a day.** joshuafolkken/kit#1515 fixed the first, in
|
|
21
|
-
// `propagate-git`'s probes, by clearing the location variables — and fixing the instance is all it
|
|
22
|
-
// did. The class recurred within hours, in a file written after it, arriving the same way: silently,
|
|
23
|
-
// with the only evidence a reflog someone had to go and read.
|
|
24
|
-
//
|
|
25
|
-
// **So the guard is built one dimension over from #1515's own network guard, on the same mechanism**
|
|
26
|
-
// (`test-network-guard.ts` arms the `PATH` shim, holds the record and fails the run from
|
|
27
|
-
// `globalSetup`'s teardown; this module contributes the `sh` that decides). Reusing it rather than
|
|
28
|
-
// writing a second guard is `CLAUDE.md` → "No clones", and it is also the only way the two can share
|
|
29
|
-
// one record: a run that both reached the network and wrote to the repository reports both.
|
|
30
|
-
//
|
|
31
|
-
// **What it refuses, and why that is narrower than "any command touching the repository".** Reads
|
|
32
|
-
// are the suite's business — half of it reads the repository it is running in, and refusing that
|
|
33
|
-
// would stop the suite rather than the defect. A read also cannot move a branch. So the arm is the
|
|
34
|
-
// *writing* subcommands, and it refuses one on either of two findings:
|
|
35
|
-
//
|
|
36
|
-
// 1. **Any git location variable is set.** A fixture is targeted by clearing the environment, never
|
|
37
|
-
// by out-arguing it, so a writing command reaching git with `GIT_DIR` or `GIT_INDEX_FILE` still
|
|
38
|
-
// in its environment is refused whatever it was aimed at. This is the exact shape of both
|
|
39
|
-
// instances, and it is decided without spawning anything.
|
|
40
|
-
// 2. **The command resolves into the guarded repository.** The environment can be clean and the
|
|
41
|
-
// write still land here, by way of an ambient `cwd` — a test that `process.chdir`s into the
|
|
42
|
-
// checkout and commits. The resolution is git's own `rev-parse --absolute-git-dir`, **asked with
|
|
43
|
-
// the caller's own global options in front of it**, so `-C <fixture>` is not refused for the
|
|
44
|
-
// directory the shim happens to sit in and `--git-dir <checkout>` is not let through for it.
|
|
45
|
-
//
|
|
46
|
-
// The two lists below say which finding a subcommand faces: an unambiguous writer faces both, and
|
|
47
|
-
// one whose reading spelling this suite uses faces the first only.
|
|
48
|
-
//
|
|
49
|
-
// **It fails closed on the second finding**: a resolution that fails means the command is not inside
|
|
50
|
-
// any repository at all — that is an answer, not an unknown, and the first finding has already
|
|
51
|
-
// refused every environment that could have redirected it elsewhere.
|
|
52
|
-
//
|
|
53
|
-
// **The one deliberate fail-open** is a `PROJECT_ROOT` that is not a git repository, where there is
|
|
54
|
-
// no repository for a test to damage and the second finding is left out of the generated shim. The
|
|
55
|
-
// first finding is generated unconditionally, so an inherited environment is refused either way.
|
|
56
|
-
//
|
|
57
|
-
// **What it names.** The shim cannot see which test called it — it is a `PATH` shim, and vitest puts
|
|
58
|
-
// no test identity in the environment. It does not need to: it refuses **at the call**, with `exit 1`
|
|
59
|
-
// and the command and the finding on stderr, so `execa` throws inside the test and vitest names the
|
|
60
|
-
// file and the test itself. The record read at teardown is the backstop for a test that swallows the
|
|
61
|
-
// error, and it carries the command and the finding for the same reason — so nobody has to
|
|
62
|
-
// reconstruct it from a reflog the way joshuafolkken/kit#1530 was.
|
|
63
|
-
|
|
64
|
-
// The `git` subcommands that can write to a repository, an index or a config. Absent from it are the
|
|
65
|
-
// reads the suite lives on (`status`, `log`, `diff`, `rev-parse`, `merge-base`, `show`,
|
|
66
|
-
// `ls-files`, `cat-file`, `describe`) and the network subcommands, which `test-network-guard.ts`
|
|
67
|
-
// refuses outright before this arm is reached.
|
|
68
|
-
//
|
|
69
|
-
// **`symbolic-ref` is deliberately absent**: its writing spelling takes a second argument and its
|
|
70
|
-
// reading spelling does not, and splitting that inside a shell `case` fails open when it gets it
|
|
71
|
-
// wrong. Nothing in this suite writes a symbolic ref, and the branch-moving spellings that matter
|
|
72
|
-
// (`update-ref`, `checkout`, `switch`, `reset`) are all here.
|
|
73
|
-
const GIT_WRITING_SUBCOMMANDS: ReadonlyArray<string> = [
|
|
74
|
-
'add',
|
|
75
|
-
'am',
|
|
76
|
-
'checkout',
|
|
77
|
-
'cherry-pick',
|
|
78
|
-
'clean',
|
|
79
|
-
'commit',
|
|
80
|
-
'gc',
|
|
81
|
-
'init',
|
|
82
|
-
'merge',
|
|
83
|
-
'mv',
|
|
84
|
-
'prune',
|
|
85
|
-
'rebase',
|
|
86
|
-
'reset',
|
|
87
|
-
'restore',
|
|
88
|
-
'revert',
|
|
89
|
-
'rm',
|
|
90
|
-
'switch',
|
|
91
|
-
'update-index',
|
|
92
|
-
'update-ref',
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
// The subcommands whose **reading** spelling this suite actually uses — `worktree list`,
|
|
96
|
-
// `branch --show-current`, `config --get`, `stash list`, `tag -l`, `notes list`, `apply --check`.
|
|
97
|
-
// They are refused on the inherited-environment finding only.
|
|
98
|
-
//
|
|
99
|
-
// **Which is not a hole, but the line between the two findings drawn where it belongs.** An
|
|
100
|
-
// inherited environment is wrong for a read as surely as for a write — it is the other half of what
|
|
101
|
-
// joshuafolkken/kit#1530 did, where `git_command`'s readings answered about the repository being
|
|
102
|
-
// pushed — so finding 1 covers these whole, `config user.email <value>` and `worktree add` included.
|
|
103
|
-
// Finding 2 is the one that cannot tell them apart: an ambient `cwd` inside the checkout is exactly
|
|
104
|
-
// how a legitimate read of the real repository is spelled, and refusing that would stop the suite
|
|
105
|
-
// rather than the defect (`test-network-guard.ts` names the worktree machinery as a read that has to
|
|
106
|
-
// keep working). Splitting them on their flags instead was rejected for the reason the network list
|
|
107
|
-
// refuses `remote` whole: getting a flag `case` wrong in `sh` fails **open**.
|
|
108
|
-
const GIT_AMBIGUOUS_SUBCOMMANDS: ReadonlyArray<string> = [
|
|
109
|
-
'apply',
|
|
110
|
-
'branch',
|
|
111
|
-
'config',
|
|
112
|
-
'notes',
|
|
113
|
-
'stash',
|
|
114
|
-
'tag',
|
|
115
|
-
'worktree',
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
// The word every recorded write violation starts with, so one shared record still says which guard
|
|
119
|
-
// caught which call and `describe_violations` can report each kind under its own heading.
|
|
120
|
-
const WRITE_MARKER = 'repository write:'
|
|
121
|
-
// **The headline states the rule and the detail line states the finding**, because the two findings
|
|
122
|
-
// have different fixes: clearing the environment is the answer to the first and gives the second a
|
|
123
|
-
// misleading instruction, which is what the same sentence said for both until review round 1.
|
|
124
|
-
const BLOCKED_WRITE_MESSAGE =
|
|
125
|
-
'josh: a unit test must not run a writing git command against the repository the suite is running in'
|
|
126
|
-
// Finding-neutral, like the stderr message and for the same reason: each recorded line carries its
|
|
127
|
-
// own `— via …`, and a run failed on the working-directory finding must not be told to clear an
|
|
128
|
-
// environment that was already clean (review round 2).
|
|
129
|
-
const WRITE_VIOLATION_HEADING =
|
|
130
|
-
'The unit suite ran a writing git command against the repository it is running in. Each line names what took it there:'
|
|
131
|
-
|
|
132
|
-
// The record and the stderr detail both read as prose and both expand in `sh`: `$*` is the arguments
|
|
133
|
-
// the shim stood in for, `$guard_via` the finding that refused them.
|
|
134
|
-
const REFUSED_COMMAND = 'git $* — via $guard_via'
|
|
135
|
-
const RECORDED_WRITE = `${WRITE_MARKER} ${REFUSED_COMMAND}`
|
|
136
|
-
|
|
137
|
-
const GIT_DIRECTORY_ARGUMENTS = ['rev-parse', '--path-format=absolute', '--git-common-dir']
|
|
138
|
-
|
|
139
|
-
// **The one failure that is an answer rather than an unknown.** There is no repository at
|
|
140
|
-
// `PROJECT_ROOT`, so finding 2 has nothing to protect and is left out of the generated shim while
|
|
141
|
-
// finding 1 stays armed. Every other failure — an unreadable directory, a git that cannot run — is a
|
|
142
|
-
// guard that does not know what it is guarding, and a guard that cannot answer must not report the
|
|
143
|
-
// run clean (review round 1 of joshuafolkken/kit#1530).
|
|
144
|
-
const NOT_A_REPOSITORY = 'not a git repository'
|
|
145
|
-
const RESOLUTION_HEADING =
|
|
146
|
-
'The unit-suite repository guard could not work out which repository it protects, so the run proves nothing:'
|
|
147
|
-
|
|
148
|
-
function text_of(value: unknown): string {
|
|
149
|
-
return typeof value === 'string' ? value : ''
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// What git said, plus what node said when git never ran — the two places the reason can be, joined
|
|
153
|
-
// so one `includes` decides between them.
|
|
154
|
-
function stderr_of(error: unknown): string {
|
|
155
|
-
if (typeof error !== 'object' || error === null) return ''
|
|
156
|
-
|
|
157
|
-
const stderr = 'stderr' in error ? text_of(error.stderr) : ''
|
|
158
|
-
const message = 'message' in error ? text_of(error.message) : ''
|
|
159
|
-
|
|
160
|
-
return `${stderr} ${message}`.trim()
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function is_absent_repository(error: unknown): boolean {
|
|
164
|
-
return stderr_of(error).includes(NOT_A_REPOSITORY)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// The repository this guard protects, as git itself resolves it — the **common** directory rather
|
|
168
|
-
// than the per-work-tree one, so a lane's shim refuses a write into any of the repository's linked
|
|
169
|
-
// work trees and not only into its own.
|
|
170
|
-
function guarded_git_directory(git_binary: string | undefined, from: string): string | undefined {
|
|
171
|
-
if (git_binary === undefined) return undefined
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
return execFileSync(git_binary, GIT_DIRECTORY_ARGUMENTS, {
|
|
175
|
-
cwd: from,
|
|
176
|
-
encoding: 'utf8',
|
|
177
|
-
// **Cleared here too, and not belt-and-braces.** The guard is armed from `globalSetup`, and
|
|
178
|
-
// under the pre-push hook that process has itself inherited `GIT_DIR` — so asking without
|
|
179
|
-
// clearing it answers about whatever the hook was pointed at, and the shim would be built
|
|
180
|
-
// to protect a repository this suite has nothing to do with.
|
|
181
|
-
env: { ...process.env, ...git_location_environment.location_free_environment() },
|
|
182
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
-
}).trim()
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (!is_absent_repository(error)) {
|
|
186
|
-
throw new Error(`${RESOLUTION_HEADING} ${stderr_of(error)}`, { cause: error })
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return undefined
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Finding 1, one line per variable. `${NAME-}` rather than `$NAME` so an unset name is the empty
|
|
194
|
-
// string under `set -u` as readily as without it.
|
|
195
|
-
function environment_lines(): Array<string> {
|
|
196
|
-
return GIT_LOCATION_VARIABLES.map(
|
|
197
|
-
(name) => `[ -z "\${${name}-}" ] || guard_env="$guard_env ${name}"`,
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// The `sh` function finding 2 resolves through, emitted once at the top of the shim.
|
|
202
|
-
//
|
|
203
|
-
// **It keeps the caller's own global options** — the words the scan counted before the subcommand —
|
|
204
|
-
// and asks git where *that* command would act. `cwd` alone is not that answer, in both directions:
|
|
205
|
-
// `git -C <fixture>` moves it away from the directory the shim was spawned in, and
|
|
206
|
-
// `git --git-dir <checkout>` moves it towards the guarded one. A resolution taken from `cwd` refused
|
|
207
|
-
// the first and let the second straight through, which is the bypass review round 1 demonstrated.
|
|
208
|
-
//
|
|
209
|
-
// The rotation is how a POSIX shell function keeps only its first `n` arguments: each of the first
|
|
210
|
-
// `n` is moved to the end, and everything left in front of them is then dropped. A function has its
|
|
211
|
-
// own positional parameters, so the shim's `$@` is untouched and still `exec`s intact.
|
|
212
|
-
function resolver_lines(git_binary: string): Array<string> {
|
|
213
|
-
return [
|
|
214
|
-
'guard_dir_of() {',
|
|
215
|
-
'\tguard_kept=$1',
|
|
216
|
-
'\tshift',
|
|
217
|
-
'\tguard_i=0',
|
|
218
|
-
'\twhile [ "$guard_i" -lt "$guard_kept" ]; do',
|
|
219
|
-
'\t\tguard_head=$1',
|
|
220
|
-
'\t\tshift',
|
|
221
|
-
'\t\tset -- "$@" "$guard_head"',
|
|
222
|
-
'\t\tguard_i=$((guard_i + 1))',
|
|
223
|
-
'\tdone',
|
|
224
|
-
'\twhile [ "$#" -gt "$guard_kept" ]; do shift; done',
|
|
225
|
-
`\t${shim_shell.quoted(git_binary)} "$@" rev-parse --absolute-git-dir 2>/dev/null`,
|
|
226
|
-
'}',
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Finding 2, asked only when finding 1 found nothing — a contaminated environment is already a
|
|
231
|
-
// refusal, and asking git where it would write costs a process.
|
|
232
|
-
function target_lines(guarded_directory: string): Array<string> {
|
|
233
|
-
const quoted_directory = shim_shell.quoted(guarded_directory)
|
|
234
|
-
|
|
235
|
-
return [
|
|
236
|
-
'if [ -z "$guard_via" ]; then',
|
|
237
|
-
'\tguard_dir=$(guard_dir_of "$guard_seen" "$@")',
|
|
238
|
-
'\tcase "$guard_dir" in',
|
|
239
|
-
`\t\t${quoted_directory}|${quoted_directory}/*) guard_via="a working directory inside $guard_dir" ;;`,
|
|
240
|
-
'\tesac',
|
|
241
|
-
'fi',
|
|
242
|
-
]
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function refusal_lines(log_file: string): Array<string> {
|
|
246
|
-
return [
|
|
247
|
-
'if [ -n "$guard_via" ]; then',
|
|
248
|
-
`\t${shim_shell.record_line(log_file, RECORDED_WRITE)}`,
|
|
249
|
-
`\techo ${shim_shell.quoted(BLOCKED_WRITE_MESSAGE)} >&2`,
|
|
250
|
-
`\techo " refused: ${REFUSED_COMMAND}" >&2`,
|
|
251
|
-
'\texit 1',
|
|
252
|
-
'fi',
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
interface WriteGuard {
|
|
257
|
-
log_file: string
|
|
258
|
-
guarded_directory: string | undefined
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Finding 1, with the two variables it answers through declared empty first.
|
|
262
|
-
function environment_findings(): Array<string> {
|
|
263
|
-
return [
|
|
264
|
-
"guard_env=''",
|
|
265
|
-
...environment_lines(),
|
|
266
|
-
"guard_via=''",
|
|
267
|
-
'[ -z "$guard_env" ] || guard_via="an inherited git environment:$guard_env"',
|
|
268
|
-
]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Finding 1 on its own, which is also the whole of the ambiguous arm.
|
|
272
|
-
function environment_guard_lines(log_file: string): Array<string> {
|
|
273
|
-
return [...environment_findings(), ...refusal_lines(log_file)]
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// The whole `sh` body of the shim's writing-subcommand arm, unindented — the caller indents it into
|
|
277
|
-
// the `case` the way it does the blocking lines it already had. Finding 1 then finding 2, with the
|
|
278
|
-
// refusal after both, so the cheap one answers first and the process is spawned only when it must be.
|
|
279
|
-
function write_guard_lines(guard: WriteGuard): Array<string> {
|
|
280
|
-
const { guarded_directory } = guard
|
|
281
|
-
|
|
282
|
-
if (guarded_directory === undefined) return environment_guard_lines(guard.log_file)
|
|
283
|
-
|
|
284
|
-
return [
|
|
285
|
-
...environment_findings(),
|
|
286
|
-
...target_lines(guarded_directory),
|
|
287
|
-
...refusal_lines(guard.log_file),
|
|
288
|
-
]
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// The shim's prelude: the resolver, where there is a repository to resolve against.
|
|
292
|
-
function prelude_lines(git_binary: string, guarded_directory: string | undefined): Array<string> {
|
|
293
|
-
return guarded_directory === undefined ? [] : resolver_lines(git_binary)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Which heading a recorded line belongs under. The record is shared with the network guard, so the
|
|
297
|
-
// marker is what tells the two apart at teardown.
|
|
298
|
-
function is_write_violation(call: string): boolean {
|
|
299
|
-
return call.startsWith(WRITE_MARKER)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const test_repository_guard = {
|
|
303
|
-
BLOCKED_WRITE_MESSAGE,
|
|
304
|
-
GIT_AMBIGUOUS_SUBCOMMANDS,
|
|
305
|
-
GIT_WRITING_SUBCOMMANDS,
|
|
306
|
-
RESOLUTION_HEADING,
|
|
307
|
-
WRITE_MARKER,
|
|
308
|
-
WRITE_VIOLATION_HEADING,
|
|
309
|
-
environment_guard_lines,
|
|
310
|
-
guarded_git_directory,
|
|
311
|
-
is_write_violation,
|
|
312
|
-
prelude_lines,
|
|
313
|
-
write_guard_lines,
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export { test_repository_guard }
|
|
317
|
-
export type { WriteGuard }
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
// Environment variables that git reads from the environment. A test that sets these affects every
|
|
4
|
-
// subsequent git command in the same worker when isolation is disabled.
|
|
5
|
-
const GIT_ENV_KEYS: ReadonlyArray<string> = [
|
|
6
|
-
'GIT_DIR',
|
|
7
|
-
'GIT_INDEX_FILE',
|
|
8
|
-
'GIT_OBJECT_DIRECTORY',
|
|
9
|
-
'GIT_WORK_TREE',
|
|
10
|
-
'GIT_COMMON_DIR',
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
interface StateSnapshot {
|
|
14
|
-
branch: string
|
|
15
|
-
status: string
|
|
16
|
-
git_env: Record<string, string | undefined>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function run_git(args: string): string {
|
|
20
|
-
try {
|
|
21
|
-
return execSync(`git ${args}`, { encoding: 'utf8', stdio: 'pipe' }).trim()
|
|
22
|
-
} catch {
|
|
23
|
-
return '(error)'
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function capture_git_environment(): Record<string, string | undefined> {
|
|
28
|
-
return Object.fromEntries(GIT_ENV_KEYS.map((key) => [key, process.env[key]]))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function capture_state(): StateSnapshot {
|
|
32
|
-
return {
|
|
33
|
-
branch: run_git('rev-parse --abbrev-ref HEAD'),
|
|
34
|
-
status: run_git('status --porcelain'),
|
|
35
|
-
git_env: capture_git_environment(),
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function environment_diff(
|
|
40
|
-
before: Record<string, string | undefined>,
|
|
41
|
-
after: Record<string, string | undefined>,
|
|
42
|
-
): Array<string> {
|
|
43
|
-
return GIT_ENV_KEYS.filter((key) => before[key] !== after[key]).map(
|
|
44
|
-
(key) => ` ${key}: ${String(before[key])} → ${String(after[key])}`,
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function format_violations(before: StateSnapshot, after: StateSnapshot): string | undefined {
|
|
49
|
-
const lines: Array<string> = []
|
|
50
|
-
|
|
51
|
-
if (before.branch !== after.branch) {
|
|
52
|
-
lines.push(` branch: ${before.branch} → ${after.branch}`)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (before.status !== after.status) {
|
|
56
|
-
lines.push(' working tree changed (run "git status" to inspect)')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
lines.push(...environment_diff(before.git_env, after.git_env))
|
|
60
|
-
|
|
61
|
-
if (lines.length === 0) return undefined
|
|
62
|
-
|
|
63
|
-
return `test-state-guard: tests mutated repository state:\n${lines.join('\n')}`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function setup(): () => void {
|
|
67
|
-
const before = capture_state()
|
|
68
|
-
|
|
69
|
-
return function disarm(): void {
|
|
70
|
-
const after = capture_state()
|
|
71
|
-
const violation = format_violations(before, after)
|
|
72
|
-
|
|
73
|
-
if (violation !== undefined) throw new Error(violation)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const test_state_guard = {
|
|
78
|
-
GIT_ENV_KEYS,
|
|
79
|
-
capture_state,
|
|
80
|
-
env_diff: environment_diff,
|
|
81
|
-
format_violations,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type { StateSnapshot }
|
|
85
|
-
export { setup, test_state_guard }
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { beforeEach } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// **Fixture writes to the real streams no longer reach the gate's output** (joshuafolkken/kit#2296).
|
|
4
|
-
// A unit test that drives a CLI `main`/`run` — `test:declared --match`, `run:review` with a bad
|
|
5
|
-
// argument — writes its `match:` / `Usage: josh …` lines straight to `process.stdout` and
|
|
6
|
-
// `process.stderr`, which Vitest forwards from the worker verbatim. Those lines then sat at the tail
|
|
7
|
-
// of `pnpm josh test:unit`, and a run reading the gate with `tail` took them for the suite's own
|
|
8
|
-
// output. Silencing the two streams for the body of every test drops them; console.* is untouched, so
|
|
9
|
-
// a deliberate debug line still shows, and the restore is a per-test teardown so nothing leaks between
|
|
10
|
-
// tests or into Vitest's own reporter.
|
|
11
|
-
|
|
12
|
-
type WriteFunction = typeof process.stdout.write
|
|
13
|
-
|
|
14
|
-
interface SavedWrites {
|
|
15
|
-
stdout: WriteFunction
|
|
16
|
-
stderr: WriteFunction
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function swallow(): true {
|
|
20
|
-
return true
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Swaps both streams' `write` for a no-op and returns what was there, so the caller restores exactly
|
|
24
|
-
// what it replaced rather than a stream reference captured somewhere else — Vitest wraps these in the
|
|
25
|
-
// worker, and that wrapper is what must come back.
|
|
26
|
-
function silence_streams(): SavedWrites {
|
|
27
|
-
const saved: SavedWrites = { stdout: process.stdout.write, stderr: process.stderr.write }
|
|
28
|
-
|
|
29
|
-
process.stdout.write = swallow
|
|
30
|
-
process.stderr.write = swallow
|
|
31
|
-
|
|
32
|
-
return saved
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function restore_streams(saved: SavedWrites): void {
|
|
36
|
-
process.stdout.write = saved.stdout
|
|
37
|
-
process.stderr.write = saved.stderr
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// The teardown is returned from `beforeEach` rather than kept in a module binding, so two tests never
|
|
41
|
-
// share one saved pair and nothing here writes to a top-level variable from inside a hook. The hook
|
|
42
|
-
// registration is the setup file's whole purpose, so its top-level effect is deliberate.
|
|
43
|
-
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
const saved = silence_streams()
|
|
46
|
-
|
|
47
|
-
return () => {
|
|
48
|
-
restore_streams(saved)
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const test_stdout_guard = { restore_streams, silence_streams }
|
|
53
|
-
|
|
54
|
-
export { test_stdout_guard }
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { appendFileSync } from 'node:fs'
|
|
2
|
-
import { expect } from 'vitest'
|
|
3
|
-
import { GUARD_LOG_KEY } from './unit-guard-environment'
|
|
4
|
-
|
|
5
|
-
// **A unit test that forgets to mock a notification must fail rather than notify**
|
|
6
|
-
// (joshuafolkken/kit#2494). A lane child's new test drove `run_carry_cli.run(['--end', '--stopped',
|
|
7
|
-
// …])` without replacing `./run-stop-notify`, and two runs of it sent two real "backlogrun stopped"
|
|
8
|
-
// confirmations — while the run was still going — and the test passed. `telegram-notify.ts` posts
|
|
9
|
-
// with Node's own `fetch`, which the `gh` / `git` `PATH` shims in `test-network-guard.ts` never see.
|
|
10
|
-
//
|
|
11
|
-
// **This wraps `fetch` in every worker rather than mocking the notifier module**, for the reason the
|
|
12
|
-
// network guard shims a binary rather than mocking a module: any caller that reaches Telegram is
|
|
13
|
-
// caught, not only the import graph a mock was written against. Anything not addressed to Telegram
|
|
14
|
-
// is handed to the real `fetch` untouched, and a test that stubs `fetch` itself replaces this
|
|
15
|
-
// wrapper for its own duration, so the correctly mocked suites (`telegram-notify.test.ts`) see
|
|
16
|
-
// nothing of it.
|
|
17
|
-
//
|
|
18
|
-
// **The attempt is recorded in the network guard's own log, not a second one.** `disarm` already
|
|
19
|
-
// reads that record at teardown and throws with every line in it, so a Telegram send fails the run
|
|
20
|
-
// and is reported exactly as a live `gh api` is — naming the test that made it, since a `fetch` has
|
|
21
|
-
// no command line to quote. The call itself is refused either way, so even an unarmed run sends
|
|
22
|
-
// nothing.
|
|
23
|
-
//
|
|
24
|
-
// **Only this process is covered.** A CLI a test spawns as a subprocess loads no setup file; the
|
|
25
|
-
// unit suite's Telegram sends so far have all been in-process, and the subprocess case is left to
|
|
26
|
-
// the notifier's own mocks.
|
|
27
|
-
|
|
28
|
-
const TELEGRAM_HOST = 'api.telegram.org'
|
|
29
|
-
const RECORD_NAME = 'telegram'
|
|
30
|
-
const BLOCKED_MESSAGE =
|
|
31
|
-
'josh: the unit suite must not send a Telegram notification — mock it instead'
|
|
32
|
-
const UNKNOWN_TEST = '(outside a test)'
|
|
33
|
-
// Marks a `fetch` that is already this wrapper. `isolate: false` re-runs a setup file for every test
|
|
34
|
-
// file in one worker, and wrapping the wrapper each time would stack one closure per file.
|
|
35
|
-
const GUARDED_MARK = Symbol.for('josh.unit-telegram-guard')
|
|
36
|
-
|
|
37
|
-
type Fetch = typeof globalThis.fetch
|
|
38
|
-
type GuardedFetch = Fetch & { [GUARDED_MARK]?: true }
|
|
39
|
-
|
|
40
|
-
function request_url(input: Parameters<Fetch>[0]): string {
|
|
41
|
-
if (typeof input === 'string') return input
|
|
42
|
-
|
|
43
|
-
return input instanceof URL ? input.href : input.url
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The host, compared exactly. The URL carries the bot token in its path, so nothing of it is kept.
|
|
47
|
-
function is_telegram(input: Parameters<Fetch>[0]): boolean {
|
|
48
|
-
return URL.parse(request_url(input))?.hostname === TELEGRAM_HOST
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function current_test(): string {
|
|
52
|
-
const state = expect.getState()
|
|
53
|
-
|
|
54
|
-
return [state.testPath, state.currentTestName ?? UNKNOWN_TEST].filter(Boolean).join(' > ')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// `>>`-style append, for `shim_shell.record_line`'s reason: every worker writes the one record.
|
|
58
|
-
function record(log_file: string | undefined): void {
|
|
59
|
-
if (log_file !== undefined) appendFileSync(log_file, `${RECORD_NAME} ${current_test()}\n`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function guarded_fetch(real_fetch: Fetch): GuardedFetch {
|
|
63
|
-
const guarded: GuardedFetch = async function (input, init) {
|
|
64
|
-
if (!is_telegram(input)) return await real_fetch(input, init)
|
|
65
|
-
|
|
66
|
-
record(process.env[GUARD_LOG_KEY])
|
|
67
|
-
|
|
68
|
-
throw new Error(BLOCKED_MESSAGE)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
guarded[GUARDED_MARK] = true
|
|
72
|
-
|
|
73
|
-
return guarded
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// A plain assignment rather than `vi.stubGlobal`: a test's own `vi.unstubAllGlobals` restores what
|
|
77
|
-
// was there before the *first* stub, which would be the unguarded `fetch` had this been one.
|
|
78
|
-
function install(): void {
|
|
79
|
-
const current: GuardedFetch = fetch
|
|
80
|
-
|
|
81
|
-
if (current[GUARDED_MARK] !== true) Reflect.set(globalThis, 'fetch', guarded_fetch(current))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// The wrapping is the setup file's whole purpose, so its top-level effect is deliberate.
|
|
85
|
-
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
|
86
|
-
install()
|
|
87
|
-
|
|
88
|
-
const test_telegram_guard = { BLOCKED_MESSAGE, RECORD_NAME, guarded_fetch, is_telegram }
|
|
89
|
-
|
|
90
|
-
export { test_telegram_guard }
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// The environment variable the network guard's record is handed to the test workers under
|
|
2
|
-
// (joshuafolkken/kit#2494). The `gh` and `git` shims carry the record's path baked into their own
|
|
3
|
-
// script, but an in-process guard — the Telegram one wraps `fetch` inside each worker — has nothing
|
|
4
|
-
// baked in, so `arm` exports the path and every worker inherits it with `PATH`.
|
|
5
|
-
//
|
|
6
|
-
// A module of its own because both sides must name it and neither may load the other: the network
|
|
7
|
-
// guard runs in Vitest's main process and must not import `vitest`, and the Telegram guard runs in
|
|
8
|
-
// every worker, where loading the network guard would resolve the guarded repository with a `git`
|
|
9
|
-
// spawn once per test file.
|
|
10
|
-
const GUARD_LOG_KEY = 'JOSH_UNIT_GUARD_LOG'
|
|
11
|
-
|
|
12
|
-
export { GUARD_LOG_KEY }
|