@zerwiz/ymir 0.1.3 → 0.1.5
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/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2665 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for bin/fm-teardown.sh's landed-work safety and stale-lock recovery.
|
|
3
|
+
#
|
|
4
|
+
# The check refuses to tear down a worktree whose work has not LANDED, because
|
|
5
|
+
# treehouse return hard-resets the worktree. "Landed" means reachable from a remote
|
|
6
|
+
# OR - for a normal ship task whose commits are not so reachable - its PR is merged
|
|
7
|
+
# and GitHub reports a PR head that contains the current local work, or its content
|
|
8
|
+
# is already in the up-to-date default branch.
|
|
9
|
+
#
|
|
10
|
+
# Covers three fixes:
|
|
11
|
+
# - local-only fork-remote: a fork IS a remote, so fork-pushed upstream-
|
|
12
|
+
# contribution PRs are teardown-eligible (the pre-fix code false-refused them).
|
|
13
|
+
# - squash-merge-then-delete-branch: the branch's own commits live nowhere on a
|
|
14
|
+
# remote after a squash merge deletes the head branch, yet the change is fully in
|
|
15
|
+
# main. Reachability alone false-refused this common GitHub flow; the check now
|
|
16
|
+
# recognizes a merged PR head containing the local work (or the content already
|
|
17
|
+
# in main) as landed.
|
|
18
|
+
# - teardown-lock-race: a killed crew process can leave a transient worktree
|
|
19
|
+
# git index.lock that blocks teardown. The return path retries on the lock
|
|
20
|
+
# error signature (even if the lock self-clears mid-check), then only removes a
|
|
21
|
+
# provably stale lock before re-running safety checks.
|
|
22
|
+
#
|
|
23
|
+
# Matrix:
|
|
24
|
+
# (a) local-only + HEAD on a fork remote-tracking branch -> ALLOW (fork fix)
|
|
25
|
+
# (b) local-only + truly unpushed work (no remote, not main) -> REFUSE (safety)
|
|
26
|
+
# (c) local-only + merged into local main, no remote -> ALLOW (no regression)
|
|
27
|
+
# (d) no-mistakes + HEAD on origin remote-tracking branch -> ALLOW (no regression)
|
|
28
|
+
# (e) no-mistakes + unpushed, no PR, content not in default -> REFUSE (safety)
|
|
29
|
+
# (f) local-only + truly unpushed + --force -> ALLOW (escape hatch)
|
|
30
|
+
# (g) no-mistakes + squash-merged PR, exact PR head -> ALLOW (squash fix)
|
|
31
|
+
# (h) no-mistakes + no PR but content already in default -> ALLOW (content fallback)
|
|
32
|
+
# (i) no-mistakes + dirty worktree, even when work landed -> REFUSE (dirty wins)
|
|
33
|
+
# (j) no-mistakes + gh lookup errors + content not in default -> REFUSE (fail-safe)
|
|
34
|
+
# (k) no-mistakes + merged PR but HEAD moved afterward -> REFUSE (stale PR)
|
|
35
|
+
# (l) no-mistakes + stale origin/main but fetched content -> ALLOW (fresh fetch)
|
|
36
|
+
# (m) no-mistakes + local HEAD ancestor of merged PR head -> ALLOW (lagging local)
|
|
37
|
+
# (n) no-mistakes + replayed unpushed patch in merged PR head -> ALLOW (replayed local)
|
|
38
|
+
# (o) fm-pr-check rerun after HEAD moved -> no stale pr_head
|
|
39
|
+
# (p) fm-pr-check when local HEAD lags -> record remote PR head
|
|
40
|
+
# (q) no-mistakes + NO pr= recorded, PR discovered by branch -> ALLOW (yolo/no-CI merge)
|
|
41
|
+
#
|
|
42
|
+
# Also covers backlog teardown-lock-race: a git index.lock left in the worktree by a
|
|
43
|
+
# killed crew process (bin/fm-teardown.sh's teardown_treehouse_return).
|
|
44
|
+
# (r) provably-stale index.lock (old mtime, no live holder) -> lock removed, ALLOW
|
|
45
|
+
# (s) index.lock with a live holder, any age -> lock kept, REFUSE
|
|
46
|
+
# (t) lsof error while checking index.lock -> lock kept, REFUSE
|
|
47
|
+
# (u) dirty worktree after stale lock cleanup -> lock removed, REFUSE
|
|
48
|
+
# (v) non-linked repo index.lock -> lock removed, ALLOW
|
|
49
|
+
# (w) index.lock mtime read failure -> lock kept, REFUSE
|
|
50
|
+
# (x) transient lock cleared after first failed return -> retry ALLOW
|
|
51
|
+
# (y) persistent lock (never clears, not provably stale) -> REFUSE loudly
|
|
52
|
+
set -u
|
|
53
|
+
|
|
54
|
+
# shellcheck source=tests/lib.sh disable=SC1091
|
|
55
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
56
|
+
fm_git_identity fmtest fmtest@example.invalid
|
|
57
|
+
|
|
58
|
+
TEARDOWN="$ROOT/bin/fm-teardown.sh"
|
|
59
|
+
PR_CHECK="$ROOT/bin/fm-pr-check.sh"
|
|
60
|
+
TMP_ROOT=$(fm_test_tmproot fm-teardown-tests)
|
|
61
|
+
REAL_GIT_FOR_TEST=$(command -v git)
|
|
62
|
+
export REAL_GIT_FOR_TEST
|
|
63
|
+
REAL_PS_FOR_TEST=$(command -v ps)
|
|
64
|
+
export REAL_PS_FOR_TEST
|
|
65
|
+
REAL_LSOF_FOR_TEST=$(command -v lsof)
|
|
66
|
+
export REAL_LSOF_FOR_TEST
|
|
67
|
+
|
|
68
|
+
# Build a fresh sandbox for one test case. Sets up:
|
|
69
|
+
# $CASE/state/ - firstmate state dir (with a fresh watcher beacon)
|
|
70
|
+
# $CASE/fakebin/ - mocks for treehouse, tmux (PATH-prepended by caller)
|
|
71
|
+
# $CASE/origin.git/ - bare upstream repo (so the project clone has origin)
|
|
72
|
+
# $CASE/project/ - clone of origin; acts as the firstmate project dir
|
|
73
|
+
# $CASE/wt/ - a worktree of the project (the task worktree)
|
|
74
|
+
# Echoes the case dir.
|
|
75
|
+
make_case() {
|
|
76
|
+
local name=$1 case_dir fakebin
|
|
77
|
+
case_dir="$TMP_ROOT/$name"
|
|
78
|
+
fakebin="$case_dir/fakebin"
|
|
79
|
+
mkdir -p "$case_dir/state" "$case_dir/config" "$case_dir/data" "$fakebin"
|
|
80
|
+
|
|
81
|
+
# Mocks for the post-check teardown steps. Refuse logic exits before these
|
|
82
|
+
# run; the ALLOW cases need them so the script can complete cleanly.
|
|
83
|
+
cat > "$fakebin/treehouse" <<'SH'
|
|
84
|
+
#!/usr/bin/env bash
|
|
85
|
+
# `treehouse return --force <wt>`: succeed silently.
|
|
86
|
+
exit 0
|
|
87
|
+
SH
|
|
88
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
89
|
+
#!/usr/bin/env bash
|
|
90
|
+
# tmux kill-window etc.: succeed silently.
|
|
91
|
+
exit 0
|
|
92
|
+
SH
|
|
93
|
+
# Default gh-axi mock: no PR is associated with the branch, and viewing any PR
|
|
94
|
+
# number fails. This keeps the landed-work check hermetic (never reaching the real
|
|
95
|
+
# gh-axi) and represents the common "no GitHub PR" baseline. Tests that need a
|
|
96
|
+
# merged PR or a lookup error override this file with the helpers below.
|
|
97
|
+
cat > "$fakebin/gh-axi" <<'SH'
|
|
98
|
+
#!/usr/bin/env bash
|
|
99
|
+
case "${1:-} ${2:-}" in
|
|
100
|
+
"pr list") printf '%s\n' "count: 0 (showing first 0)" "pull_requests[]: []" ; exit 0 ;;
|
|
101
|
+
"pr view") echo "error: pull request not found" >&2 ; exit 1 ;;
|
|
102
|
+
esac
|
|
103
|
+
exit 0
|
|
104
|
+
SH
|
|
105
|
+
cat > "$fakebin/gh" <<'SH'
|
|
106
|
+
#!/usr/bin/env bash
|
|
107
|
+
case "${1:-} ${2:-}" in
|
|
108
|
+
"pr view") echo "error: pull request not found" >&2 ; exit 1 ;;
|
|
109
|
+
esac
|
|
110
|
+
exit 0
|
|
111
|
+
SH
|
|
112
|
+
# Default hermetic no-mistakes stub: `axi status` answers FM_FAKE_AXI_STATUS
|
|
113
|
+
# verbatim (empty by default, i.e. no active run - the pre-teardown run-abort
|
|
114
|
+
# step is then a no-op), and `axi abort` appends one line to
|
|
115
|
+
# FM_FAKE_NM_ABORT_LOG when set. This keeps every case hermetic - without it,
|
|
116
|
+
# `command -v no-mistakes` would fall through to whatever real binary
|
|
117
|
+
# happens to be on the test runner's own PATH. Tests exercising the run-abort
|
|
118
|
+
# path override FM_FAKE_AXI_STATUS/FM_FAKE_NM_ABORT_LOG before run_teardown.
|
|
119
|
+
cat > "$fakebin/no-mistakes" <<'SH'
|
|
120
|
+
#!/usr/bin/env bash
|
|
121
|
+
case "${1:-}" in
|
|
122
|
+
axi)
|
|
123
|
+
shift
|
|
124
|
+
case "${1:-}" in
|
|
125
|
+
status)
|
|
126
|
+
shift
|
|
127
|
+
run_id=""
|
|
128
|
+
if [ "${1:-}" = --run ]; then run_id=${2:-}; fi
|
|
129
|
+
if [ -n "${FM_FAKE_NM_ABORT_LOG:-}" ] \
|
|
130
|
+
&& grep -Fxq "abort --run $run_id" "$FM_FAKE_NM_ABORT_LOG" 2>/dev/null \
|
|
131
|
+
&& [ "${FM_FAKE_NM_ABORT_NOOP:-0}" != 1 ]; then
|
|
132
|
+
if [ "${FM_FAKE_NM_NOT_FOUND_AFTER_ABORT:-0}" = 1 ]; then
|
|
133
|
+
printf 'error: "run \\"%s\\" not found"\n' "$run_id" >&2
|
|
134
|
+
exit 1
|
|
135
|
+
elif [ "${FM_FAKE_NM_EMPTY_AFTER_ABORT:-0}" = 1 ]; then
|
|
136
|
+
exit 0
|
|
137
|
+
elif [ -n "${FM_FAKE_AXI_STATUS_AFTER_ABORT:-}" ]; then
|
|
138
|
+
printf '%s\n' "$FM_FAKE_AXI_STATUS_AFTER_ABORT"
|
|
139
|
+
else
|
|
140
|
+
printf 'run:\n id: "%s"\n outcome: cancelled\n' "$run_id"
|
|
141
|
+
fi
|
|
142
|
+
else
|
|
143
|
+
printf '%s\n' "${FM_FAKE_AXI_STATUS:-}"
|
|
144
|
+
fi
|
|
145
|
+
;;
|
|
146
|
+
abort)
|
|
147
|
+
shift
|
|
148
|
+
[ -z "${FM_FAKE_NM_ABORT_LOG:-}" ] || printf 'abort %s\n' "$*" >> "$FM_FAKE_NM_ABORT_LOG"
|
|
149
|
+
exit 0 ;;
|
|
150
|
+
esac
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
exit 0
|
|
154
|
+
SH
|
|
155
|
+
chmod +x "$fakebin/treehouse" "$fakebin/tmux" "$fakebin/gh-axi" "$fakebin/gh" "$fakebin/no-mistakes"
|
|
156
|
+
|
|
157
|
+
# Bare origin so the clone has an `origin` remote and origin/HEAD.
|
|
158
|
+
git init -q --bare "$case_dir/origin.git"
|
|
159
|
+
git -C "$case_dir/origin.git" symbolic-ref HEAD refs/heads/main
|
|
160
|
+
# Seed origin with one commit BEFORE cloning so the clone is not empty.
|
|
161
|
+
git clone -q "$case_dir/origin.git" "$case_dir/_seed" 2>/dev/null
|
|
162
|
+
git -C "$case_dir/_seed" -c user.email=t@t -c user.name=t \
|
|
163
|
+
commit -q --allow-empty -m "origin baseline"
|
|
164
|
+
git -C "$case_dir/_seed" push -q origin main
|
|
165
|
+
rm -rf "$case_dir/_seed"
|
|
166
|
+
# Clone as the project; give it a `main` branch and an origin/HEAD.
|
|
167
|
+
git clone -q "$case_dir/origin.git" "$case_dir/project"
|
|
168
|
+
git -C "$case_dir/project" remote set-head origin main 2>/dev/null || true
|
|
169
|
+
# Add a worktree on a fresh task branch; that branch is where the crewmate commits.
|
|
170
|
+
git -C "$case_dir/project" worktree add -q -b fm/task-x1 "$case_dir/wt" main
|
|
171
|
+
|
|
172
|
+
# Fresh watcher beacon so fm-guard stays quiet.
|
|
173
|
+
touch "$case_dir/state/.last-watcher-beat"
|
|
174
|
+
|
|
175
|
+
printf '%s\n' "$case_dir"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Write a meta file for the task. Args: case_dir mode kind
|
|
179
|
+
write_meta() {
|
|
180
|
+
local case_dir=$1 mode=$2 kind=$3
|
|
181
|
+
fm_write_meta "$case_dir/state/task-x1.meta" \
|
|
182
|
+
"window=firstmate:fm-task-x1" \
|
|
183
|
+
"endpoint_task_id=task-x1" \
|
|
184
|
+
"worktree=$case_dir/wt" \
|
|
185
|
+
"project=$case_dir/project" \
|
|
186
|
+
"kind=$kind" \
|
|
187
|
+
"mode=$mode" \
|
|
188
|
+
"spawn_gen=teardown-test-task-x1"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Commit something on the worktree's task branch. Args: case_dir [message]
|
|
192
|
+
wt_commit() {
|
|
193
|
+
local case_dir=$1 msg=${2:-wt work}
|
|
194
|
+
git -C "$case_dir/wt" -c user.email=t@t -c user.name=t \
|
|
195
|
+
commit -q --allow-empty -m "$msg"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Add a fork bare repo and register it as a remote on the project, then push
|
|
199
|
+
# the worktree's task branch to it and fetch into the project so the worktree
|
|
200
|
+
# sees the remote-tracking ref. Args: case_dir
|
|
201
|
+
add_fork_with_pushed_branch() {
|
|
202
|
+
local case_dir=$1
|
|
203
|
+
git init -q --bare "$case_dir/fork.git"
|
|
204
|
+
git -C "$case_dir/project" remote add fork "$case_dir/fork.git"
|
|
205
|
+
# Push the task branch from the worktree to the fork, then fetch into project
|
|
206
|
+
# so refs/remotes/fork/fm-task-x1 is visible from the worktree (shared object db).
|
|
207
|
+
git -C "$case_dir/wt" push -q fork fm/task-x1
|
|
208
|
+
git -C "$case_dir/project" fetch -q fork
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Commit a real file change on the worktree's task branch (unlike wt_commit, which
|
|
212
|
+
# makes an empty commit). A non-empty tree is what the content-in-default check
|
|
213
|
+
# inspects. Args: case_dir file content [message]
|
|
214
|
+
wt_commit_file() {
|
|
215
|
+
local case_dir=$1 file=$2 content=$3 msg=${4:-add $2}
|
|
216
|
+
printf '%s\n' "$content" > "$case_dir/wt/$file"
|
|
217
|
+
git -C "$case_dir/wt" add -- "$file"
|
|
218
|
+
git -C "$case_dir/wt" -c user.email=t@t -c user.name=t commit -q -m "$msg"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Land <file>=<content> as a single commit on origin's default branch, simulating a
|
|
222
|
+
# squash merge whose net change matches the task branch but whose commit differs.
|
|
223
|
+
# After this, the branch's content is in origin/main even though the branch's own
|
|
224
|
+
# commits are not reachable from it. Args: case_dir file content
|
|
225
|
+
land_on_origin_main() {
|
|
226
|
+
local case_dir=$1 file=$2 content=$3 tmp
|
|
227
|
+
tmp="$case_dir/_land"
|
|
228
|
+
git clone -q "$case_dir/origin.git" "$tmp"
|
|
229
|
+
printf '%s\n' "$content" > "$tmp/$file"
|
|
230
|
+
git -C "$tmp" add -- "$file"
|
|
231
|
+
git -C "$tmp" -c user.email=t@t -c user.name=t commit -q -m "squash $file"
|
|
232
|
+
git -C "$tmp" push -q origin HEAD:main
|
|
233
|
+
rm -rf "$tmp"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Override GitHub lookups to report PR 7 as merged with the supplied head.
|
|
237
|
+
add_gh_pr_merged_for_head() {
|
|
238
|
+
local case_dir=$1 head=$2
|
|
239
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
240
|
+
#!/usr/bin/env bash
|
|
241
|
+
case "${1:-} ${2:-}" in
|
|
242
|
+
"pr list")
|
|
243
|
+
printf '%s\n' "count: 1 (showing first 1)" "pull_requests[1]{number,state}:" " 7,merged" ; exit 0 ;;
|
|
244
|
+
"pr view")
|
|
245
|
+
printf '%s\n' "pull_request:" " number: 7" " state: merged" ' merged: "2026-06-26T00:00:00Z"' ; exit 0 ;;
|
|
246
|
+
esac
|
|
247
|
+
exit 0
|
|
248
|
+
SH
|
|
249
|
+
cat > "$case_dir/fakebin/gh" <<SH
|
|
250
|
+
#!/usr/bin/env bash
|
|
251
|
+
case "\${1:-} \${2:-}" in
|
|
252
|
+
"pr view")
|
|
253
|
+
case " \$* " in
|
|
254
|
+
*"state,headRefOid,url"*) printf '%s\t%s\t%s\n' 'MERGED' '$head' 'https://github.com/example/repo/pull/7' ; exit 0 ;;
|
|
255
|
+
*"headRefOid"*) printf '%s\n' '$head' ; exit 0 ;;
|
|
256
|
+
esac
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
echo "error: pull request not found" >&2
|
|
260
|
+
exit 1
|
|
261
|
+
SH
|
|
262
|
+
chmod +x "$case_dir/fakebin/gh-axi" "$case_dir/fakebin/gh"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
append_pr_meta_for_current_head() {
|
|
266
|
+
local case_dir=$1 head
|
|
267
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
268
|
+
printf '%s\n' \
|
|
269
|
+
'pr=https://github.com/example/repo/pull/7' \
|
|
270
|
+
"pr_head=$head" >> "$case_dir/state/task-x1.meta"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
append_pr_meta_url() {
|
|
274
|
+
local case_dir=$1
|
|
275
|
+
printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
commit_tree_from_wt_head() {
|
|
279
|
+
local case_dir=$1 parent=$2 msg=$3 tree
|
|
280
|
+
tree=$(git -C "$case_dir/wt" rev-parse "$parent^{tree}") || return 1
|
|
281
|
+
printf '%s\n' "$msg" | git -C "$case_dir/wt" commit-tree "$tree" -p "$parent"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
land_equivalent_patch_on_origin_branch() {
|
|
285
|
+
local case_dir=$1 branch=$2 file=$3 content=$4 msg=$5 tmp
|
|
286
|
+
tmp="$case_dir/_equiv"
|
|
287
|
+
git clone -q "$case_dir/origin.git" "$tmp"
|
|
288
|
+
printf '%s\n' "$content" > "$tmp/$file"
|
|
289
|
+
git -C "$tmp" add -- "$file"
|
|
290
|
+
git -C "$tmp" -c user.email=t@t -c user.name=t commit -q -m "$msg"
|
|
291
|
+
git -C "$tmp" push -q origin "HEAD:refs/heads/$branch"
|
|
292
|
+
git -C "$case_dir/project" fetch -q origin "$branch"
|
|
293
|
+
rm -rf "$tmp"
|
|
294
|
+
git -C "$case_dir/project" rev-parse "refs/remotes/origin/$branch"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Override gh-axi so every call fails, simulating an API/network error.
|
|
298
|
+
add_gh_axi_error() {
|
|
299
|
+
local case_dir=$1
|
|
300
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
301
|
+
#!/usr/bin/env bash
|
|
302
|
+
echo "error: gh-axi unavailable" >&2
|
|
303
|
+
exit 1
|
|
304
|
+
SH
|
|
305
|
+
cat > "$case_dir/fakebin/gh" <<'SH'
|
|
306
|
+
#!/usr/bin/env bash
|
|
307
|
+
echo "error: gh unavailable" >&2
|
|
308
|
+
exit 1
|
|
309
|
+
SH
|
|
310
|
+
chmod +x "$case_dir/fakebin/gh-axi" "$case_dir/fakebin/gh"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Override fakebin/treehouse so `treehouse return --force <wt>` fails with a
|
|
314
|
+
# git "file exists" lock error whenever the worktree's real index.lock is
|
|
315
|
+
# present, and succeeds once it is gone. This drives the lock through
|
|
316
|
+
# fm-teardown.sh's own retry-then-stale-cleanup logic (teardown_treehouse_return
|
|
317
|
+
# in bin/fm-teardown.sh) rather than hand-simulating that logic in the test.
|
|
318
|
+
add_lock_aware_treehouse() {
|
|
319
|
+
local case_dir=$1
|
|
320
|
+
cat > "$case_dir/fakebin/treehouse" <<'SH'
|
|
321
|
+
#!/usr/bin/env bash
|
|
322
|
+
if [ "${1:-}" = return ]; then
|
|
323
|
+
shift
|
|
324
|
+
wt=""
|
|
325
|
+
for a in "$@"; do
|
|
326
|
+
case "$a" in
|
|
327
|
+
--force) ;;
|
|
328
|
+
*) wt=$a ;;
|
|
329
|
+
esac
|
|
330
|
+
done
|
|
331
|
+
lock=$(git -C "$wt" rev-parse --git-path index.lock 2>/dev/null || true)
|
|
332
|
+
case "$lock" in
|
|
333
|
+
/*|'') ;;
|
|
334
|
+
*) lock="$wt/$lock" ;;
|
|
335
|
+
esac
|
|
336
|
+
if [ -n "$lock" ] && [ -e "$lock" ]; then
|
|
337
|
+
echo "fatal: Unable to create '$lock': File exists." >&2
|
|
338
|
+
exit 128
|
|
339
|
+
fi
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
exit 0
|
|
343
|
+
SH
|
|
344
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# treehouse return fails once with the index.lock signature, then clears the lock
|
|
348
|
+
# (simulating a dying crew git process finishing) so the next retry succeeds.
|
|
349
|
+
# The first failure always reports the lock path even if the file is removed in
|
|
350
|
+
# the same attempt - matching the production race where the lock self-clears
|
|
351
|
+
# between the failed return and the supervisor's existence check.
|
|
352
|
+
add_transient_lock_treehouse() {
|
|
353
|
+
local case_dir=$1
|
|
354
|
+
cat > "$case_dir/fakebin/treehouse" <<'SH'
|
|
355
|
+
#!/usr/bin/env bash
|
|
356
|
+
if [ "${1:-}" = return ]; then
|
|
357
|
+
shift
|
|
358
|
+
wt=""
|
|
359
|
+
for a in "$@"; do
|
|
360
|
+
case "$a" in
|
|
361
|
+
--force) ;;
|
|
362
|
+
*) wt=$a ;;
|
|
363
|
+
esac
|
|
364
|
+
done
|
|
365
|
+
lock=$(git -C "$wt" rev-parse --git-path index.lock 2>/dev/null || true)
|
|
366
|
+
case "$lock" in
|
|
367
|
+
/*|'') ;;
|
|
368
|
+
*) lock="$wt/$lock" ;;
|
|
369
|
+
esac
|
|
370
|
+
count_file="${TREEHOUSE_ATTEMPT_FILE:?}"
|
|
371
|
+
count=0
|
|
372
|
+
if [ -f "$count_file" ]; then
|
|
373
|
+
count=$(cat "$count_file")
|
|
374
|
+
fi
|
|
375
|
+
count=$(( count + 1 ))
|
|
376
|
+
printf '%s\n' "$count" > "$count_file"
|
|
377
|
+
if [ "$count" -eq 1 ]; then
|
|
378
|
+
# Emit the real git signature, then drop the lock so a lock-existence-only
|
|
379
|
+
# recovery path would wrongly abort without retrying.
|
|
380
|
+
if [ -n "$lock" ]; then
|
|
381
|
+
echo "fatal: Unable to create '$lock': File exists." >&2
|
|
382
|
+
rm -f "$lock"
|
|
383
|
+
else
|
|
384
|
+
echo "fatal: Unable to create 'index.lock': File exists." >&2
|
|
385
|
+
fi
|
|
386
|
+
exit 128
|
|
387
|
+
fi
|
|
388
|
+
exit 0
|
|
389
|
+
fi
|
|
390
|
+
exit 0
|
|
391
|
+
SH
|
|
392
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# treehouse return always fails with the lock signature while the lock file
|
|
396
|
+
# remains; used to assert exhausted retries still refuse loudly.
|
|
397
|
+
add_persistent_lock_treehouse() {
|
|
398
|
+
local case_dir=$1
|
|
399
|
+
cat > "$case_dir/fakebin/treehouse" <<'SH'
|
|
400
|
+
#!/usr/bin/env bash
|
|
401
|
+
if [ "${1:-}" = return ]; then
|
|
402
|
+
shift
|
|
403
|
+
wt=""
|
|
404
|
+
for a in "$@"; do
|
|
405
|
+
case "$a" in
|
|
406
|
+
--force) ;;
|
|
407
|
+
*) wt=$a ;;
|
|
408
|
+
esac
|
|
409
|
+
done
|
|
410
|
+
lock=$(git -C "$wt" rev-parse --git-path index.lock 2>/dev/null || true)
|
|
411
|
+
case "$lock" in
|
|
412
|
+
/*|'') ;;
|
|
413
|
+
*) lock="$wt/$lock" ;;
|
|
414
|
+
esac
|
|
415
|
+
if [ -z "$lock" ]; then
|
|
416
|
+
lock="index.lock"
|
|
417
|
+
fi
|
|
418
|
+
echo "fatal: Unable to create '$lock': File exists." >&2
|
|
419
|
+
exit 128
|
|
420
|
+
fi
|
|
421
|
+
exit 0
|
|
422
|
+
SH
|
|
423
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
git_index_lock_path() {
|
|
427
|
+
local dir=$1 lock abs_dir
|
|
428
|
+
lock=$(git -C "$dir" rev-parse --git-path index.lock)
|
|
429
|
+
case "$lock" in
|
|
430
|
+
/*) printf '%s\n' "$lock" ;;
|
|
431
|
+
*)
|
|
432
|
+
abs_dir=$(cd "$dir" && pwd -P)
|
|
433
|
+
printf '%s/%s\n' "$abs_dir" "$lock"
|
|
434
|
+
;;
|
|
435
|
+
esac
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
# fakebin/lsof stub: no process ever holds anything open (lsof's not-found exit
|
|
439
|
+
# code), so a lock's staleness is decided by age alone. The cwd scan is a
|
|
440
|
+
# separate successful empty query.
|
|
441
|
+
add_lsof_no_holder() {
|
|
442
|
+
local case_dir=$1
|
|
443
|
+
cat > "$case_dir/fakebin/lsof" <<'SH'
|
|
444
|
+
#!/usr/bin/env bash
|
|
445
|
+
case " $* " in
|
|
446
|
+
*" -d cwd "*) exit 0 ;;
|
|
447
|
+
esac
|
|
448
|
+
exit 1
|
|
449
|
+
SH
|
|
450
|
+
chmod +x "$case_dir/fakebin/lsof"
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# fakebin/lsof stub: a live process holds every queried path open, so a lock is
|
|
454
|
+
# never judged stale regardless of its age.
|
|
455
|
+
add_lsof_live_holder() {
|
|
456
|
+
local case_dir=$1
|
|
457
|
+
cat > "$case_dir/fakebin/lsof" <<'SH'
|
|
458
|
+
#!/usr/bin/env bash
|
|
459
|
+
exit 0
|
|
460
|
+
SH
|
|
461
|
+
chmod +x "$case_dir/fakebin/lsof"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
add_lsof_error() {
|
|
465
|
+
local case_dir=$1
|
|
466
|
+
cat > "$case_dir/fakebin/lsof" <<'SH'
|
|
467
|
+
#!/usr/bin/env bash
|
|
468
|
+
echo "lsof: simulated failure for ${1:-unknown}" >&2
|
|
469
|
+
exit 2
|
|
470
|
+
SH
|
|
471
|
+
chmod +x "$case_dir/fakebin/lsof"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
add_stat_error() {
|
|
475
|
+
local case_dir=$1
|
|
476
|
+
cat > "$case_dir/fakebin/stat" <<'SH'
|
|
477
|
+
#!/usr/bin/env bash
|
|
478
|
+
echo "stat: simulated failure" >&2
|
|
479
|
+
exit 1
|
|
480
|
+
SH
|
|
481
|
+
chmod +x "$case_dir/fakebin/stat"
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
add_git_status_lock_failure() {
|
|
485
|
+
local case_dir=$1
|
|
486
|
+
cat > "$case_dir/fakebin/git" <<'SH'
|
|
487
|
+
#!/usr/bin/env bash
|
|
488
|
+
real=${REAL_GIT_FOR_TEST:?}
|
|
489
|
+
dir=
|
|
490
|
+
args=()
|
|
491
|
+
while [ "$#" -gt 0 ]; do
|
|
492
|
+
case "$1" in
|
|
493
|
+
-C)
|
|
494
|
+
dir=$2
|
|
495
|
+
args+=("$1" "$2")
|
|
496
|
+
shift 2
|
|
497
|
+
;;
|
|
498
|
+
*)
|
|
499
|
+
args+=("$1")
|
|
500
|
+
shift
|
|
501
|
+
;;
|
|
502
|
+
esac
|
|
503
|
+
done
|
|
504
|
+
if [ -n "$dir" ] && [ "${args[2]:-}" = status ] && [ "${args[3]:-}" = --porcelain ]; then
|
|
505
|
+
lock=$("$real" -C "$dir" rev-parse --git-path index.lock 2>/dev/null || true)
|
|
506
|
+
case "$lock" in
|
|
507
|
+
/*|'') ;;
|
|
508
|
+
*) lock="$dir/$lock" ;;
|
|
509
|
+
esac
|
|
510
|
+
if [ -n "$lock" ] && [ -e "$lock" ]; then
|
|
511
|
+
echo "fatal: Unable to create '$lock': File exists." >&2
|
|
512
|
+
exit 128
|
|
513
|
+
fi
|
|
514
|
+
fi
|
|
515
|
+
exec "$real" "${args[@]}"
|
|
516
|
+
SH
|
|
517
|
+
chmod +x "$case_dir/fakebin/git"
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
# Run teardown with PATH mocking. Args: case_dir [extra args...]
|
|
521
|
+
run_teardown() {
|
|
522
|
+
local case_dir=$1; shift
|
|
523
|
+
# FM_DATA_OVERRIDE is pinned to the case dir because teardown closes this
|
|
524
|
+
# home's backlog item itself; without it $DATA would resolve to the real
|
|
525
|
+
# repo's own home and a test could mutate live records.
|
|
526
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
527
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
528
|
+
FM_DATA_OVERRIDE="$case_dir/data" \
|
|
529
|
+
FM_CONFIG_OVERRIDE="$case_dir/config" \
|
|
530
|
+
PATH="$case_dir/fakebin:${FM_TEARDOWN_TEST_PATH:-$PATH}" \
|
|
531
|
+
"$TEARDOWN" task-x1 "$@"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# Seed a real backlog carrying task-x1 as In flight, so a teardown in this case
|
|
535
|
+
# has a row to close. Uses the real tasks-axi (the fixture's default fakebin has
|
|
536
|
+
# no tasks-axi stub, so PATH resolves the installed one).
|
|
537
|
+
seed_backlog_in_flight() {
|
|
538
|
+
local case_dir=$1 kind=${2:-ship}
|
|
539
|
+
mkdir -p "$case_dir/data"
|
|
540
|
+
printf '%s\n' '# Backlog' '' '## In flight' '' '## Queued' '' '## Done' \
|
|
541
|
+
> "$case_dir/data/backlog.md"
|
|
542
|
+
tasks-axi add task-x1 "teardown fixture task" --kind "$kind" \
|
|
543
|
+
--file "$case_dir/data/backlog.md" >/dev/null
|
|
544
|
+
tasks-axi start task-x1 --file "$case_dir/data/backlog.md" >/dev/null
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
backlog_row_state() {
|
|
548
|
+
local case_dir=$1
|
|
549
|
+
tasks-axi show task-x1 --file "$case_dir/data/backlog.md" 2>/dev/null |
|
|
550
|
+
sed -n 's/^ state: *//p' | head -1
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
# Build the teardown test's executable search path without lsof, regardless of
|
|
554
|
+
# whether the host installs it in /usr/bin, /usr/sbin, or a package-manager bin.
|
|
555
|
+
make_path_without_lsof() { # <case-dir>
|
|
556
|
+
local case_dir=$1 path_dir="$1/path-without-lsof" cmd resolved
|
|
557
|
+
mkdir -p "$path_dir"
|
|
558
|
+
for cmd in awk bash basename cat chmod cp cut date dirname env find git grep head hostname id ln \
|
|
559
|
+
mkdir mktemp mv perl ps readlink realpath rm sed sh sleep sort stat tail timeout tr uname wc xargs; do
|
|
560
|
+
resolved=$(command -v "$cmd" 2>/dev/null) || continue
|
|
561
|
+
case "$resolved" in /*) ln -sf "$resolved" "$path_dir/$cmd" ;; esac
|
|
562
|
+
done
|
|
563
|
+
printf '%s\n' "$path_dir"
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
test_local_only_fork_remote_allows() {
|
|
567
|
+
local case_dir rc
|
|
568
|
+
case_dir=$(make_case fork-allow)
|
|
569
|
+
write_meta "$case_dir" local-only ship
|
|
570
|
+
wt_commit "$case_dir" "fix the thing"
|
|
571
|
+
add_fork_with_pushed_branch "$case_dir"
|
|
572
|
+
|
|
573
|
+
set +e
|
|
574
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
575
|
+
rc=$?
|
|
576
|
+
set -e
|
|
577
|
+
|
|
578
|
+
expect_code 0 "$rc" "fork-allow: teardown should succeed when HEAD is on a fork remote"
|
|
579
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "fork-allow: teardown printed a REFUSED line"
|
|
580
|
+
jq -e --arg id task-x1 '
|
|
581
|
+
.schema == "fm-secondmate-home-summary.v1"
|
|
582
|
+
and all(.endpoints[]; .id != $id)
|
|
583
|
+
' "$case_dir/state/home-summary.json" >/dev/null \
|
|
584
|
+
|| fail "successful task teardown did not publish the task's removal from the home summary ledger"
|
|
585
|
+
pass "local-only worktree with HEAD on a fork remote is torn down and the home summary is refreshed"
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
test_teardown_closes_the_backlog_item_itself() {
|
|
589
|
+
local case_dir out
|
|
590
|
+
case_dir=$(make_case tasks-axi-close)
|
|
591
|
+
write_meta "$case_dir" no-mistakes ship
|
|
592
|
+
printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta"
|
|
593
|
+
seed_backlog_in_flight "$case_dir"
|
|
594
|
+
|
|
595
|
+
out=$(run_teardown "$case_dir") || fail "teardown failed with a real backlog"
|
|
596
|
+
[ "$(backlog_row_state "$case_dir")" = "done" ] \
|
|
597
|
+
|| fail "teardown returned success while its backlog item was still open: $(backlog_row_state "$case_dir")"
|
|
598
|
+
assert_grep 'https://github.com/example/repo/pull/7' "$case_dir/data/backlog.md" \
|
|
599
|
+
"closed backlog item did not record the task's PR"
|
|
600
|
+
assert_absent "$case_dir/state/task-x1.backlog-close" \
|
|
601
|
+
"a landed close left its pending-close record behind"
|
|
602
|
+
printf '%s\n' "$out" | grep -F 'tasks-axi ready' >/dev/null \
|
|
603
|
+
|| fail "teardown dropped the dependency-cleared follow-up: $out"
|
|
604
|
+
printf '%s\n' "$out" | grep -F 'check date gates' >/dev/null \
|
|
605
|
+
|| fail "teardown did not preserve date-gate check: $out"
|
|
606
|
+
printf '%s\n' "$out" | grep -F 'Run tasks-axi done' >/dev/null \
|
|
607
|
+
&& fail "teardown still asked a later turn to close the item it already closed: $out"
|
|
608
|
+
pass "teardown closes its own backlog item before reporting success"
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
test_teardown_manual_backend_leaves_the_backlog_to_the_operator() {
|
|
612
|
+
local case_dir out backlog_path
|
|
613
|
+
case_dir=$(make_case tasks-axi-manual-optout)
|
|
614
|
+
write_meta "$case_dir" no-mistakes ship
|
|
615
|
+
printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta"
|
|
616
|
+
printf '%s\n' manual > "$case_dir/config/backlog-backend"
|
|
617
|
+
seed_backlog_in_flight "$case_dir"
|
|
618
|
+
|
|
619
|
+
out=$(run_teardown "$case_dir") || fail "teardown failed with manual backlog backend"
|
|
620
|
+
[ "$(backlog_row_state "$case_dir")" = in_flight ] \
|
|
621
|
+
|| fail "manual backlog backend was mutated by teardown anyway"
|
|
622
|
+
backlog_path=$(cd "$case_dir/data" && pwd -P)/backlog.md
|
|
623
|
+
printf '%s\n' "$out" | grep -F "Update $backlog_path - move task-x1 to Done" >/dev/null \
|
|
624
|
+
|| fail "teardown did not prompt manual backlog update under opt-out: $out"
|
|
625
|
+
pass "teardown honors config/backlog-backend=manual and still finishes cleanly"
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
test_local_only_truly_unpushed_refuses() {
|
|
629
|
+
local case_dir rc
|
|
630
|
+
case_dir=$(make_case truly-unpushed)
|
|
631
|
+
write_meta "$case_dir" local-only ship
|
|
632
|
+
wt_commit "$case_dir" "unpushed work"
|
|
633
|
+
# No fork, no push to origin, not merged into main.
|
|
634
|
+
|
|
635
|
+
set +e
|
|
636
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
637
|
+
rc=$?
|
|
638
|
+
set -e
|
|
639
|
+
|
|
640
|
+
expect_code 1 "$rc" "truly-unpushed: teardown should refuse"
|
|
641
|
+
grep -q REFUSED "$case_dir/stderr" || fail "truly-unpushed: no REFUSED line in stderr"
|
|
642
|
+
pass "local-only worktree with truly unpushed work is refused (safety preserved)"
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
test_local_only_merged_to_local_main_allows() {
|
|
646
|
+
local case_dir rc
|
|
647
|
+
case_dir=$(make_case merged-main)
|
|
648
|
+
write_meta "$case_dir" local-only ship
|
|
649
|
+
wt_commit "$case_dir" "merged work"
|
|
650
|
+
# Fast-forward the project's main to the worktree's HEAD commit so HEAD is
|
|
651
|
+
# reachable from main. update-ref works whether or not main is checked out,
|
|
652
|
+
# and the worktree shares the project's object db so the commit is visible.
|
|
653
|
+
local wt_head
|
|
654
|
+
wt_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
655
|
+
git -C "$case_dir/project" update-ref refs/heads/main "$wt_head"
|
|
656
|
+
|
|
657
|
+
set +e
|
|
658
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
659
|
+
rc=$?
|
|
660
|
+
set -e
|
|
661
|
+
|
|
662
|
+
expect_code 0 "$rc" "merged-main: teardown should succeed when work is merged into local main"
|
|
663
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "merged-main: teardown printed a REFUSED line"
|
|
664
|
+
pass "local-only worktree with work merged into local main is torn down (no regression)"
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
test_no_mistakes_origin_remote_allows() {
|
|
668
|
+
local case_dir rc
|
|
669
|
+
case_dir=$(make_case nm-origin)
|
|
670
|
+
write_meta "$case_dir" no-mistakes ship
|
|
671
|
+
wt_commit "$case_dir" "shippable work"
|
|
672
|
+
# Push the task branch to origin and fetch so the worktree sees it.
|
|
673
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
674
|
+
git -C "$case_dir/project" fetch -q origin
|
|
675
|
+
|
|
676
|
+
set +e
|
|
677
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
678
|
+
rc=$?
|
|
679
|
+
set -e
|
|
680
|
+
|
|
681
|
+
expect_code 0 "$rc" "nm-origin: teardown should succeed when HEAD is on origin"
|
|
682
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "nm-origin: teardown printed a REFUSED line"
|
|
683
|
+
grep -F 'blockers are gone and date is due' "$case_dir/stdout" >/dev/null \
|
|
684
|
+
|| fail "nm-origin: teardown manual prompt did not preserve date-gate check"
|
|
685
|
+
pass "no-mistakes worktree with HEAD on origin is torn down (no regression)"
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
test_no_mistakes_truly_unpushed_refuses() {
|
|
689
|
+
local case_dir rc
|
|
690
|
+
case_dir=$(make_case nm-unpushed)
|
|
691
|
+
write_meta "$case_dir" no-mistakes ship
|
|
692
|
+
# Real content that is not pushed, has no PR (default gh-axi mock), and never
|
|
693
|
+
# landed on origin/main: genuinely unlanded work that must still refuse.
|
|
694
|
+
wt_commit_file "$case_dir" feature.txt hello "unpushed work"
|
|
695
|
+
|
|
696
|
+
set +e
|
|
697
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
698
|
+
rc=$?
|
|
699
|
+
set -e
|
|
700
|
+
|
|
701
|
+
expect_code 1 "$rc" "nm-unpushed: teardown should refuse"
|
|
702
|
+
grep -q REFUSED "$case_dir/stderr" || fail "nm-unpushed: no REFUSED line in stderr"
|
|
703
|
+
pass "no-mistakes worktree with genuinely unlanded work is refused (safety preserved)"
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
test_squash_merged_branch_deleted_allows() {
|
|
707
|
+
local case_dir rc pr_head
|
|
708
|
+
case_dir=$(make_case squash-merged)
|
|
709
|
+
write_meta "$case_dir" no-mistakes ship
|
|
710
|
+
# Real branch content that is NOT pushed and NOT on origin/main: a squash merge
|
|
711
|
+
# rewrote it into a different commit on main and auto-deleted the head branch, so
|
|
712
|
+
# HEAD is unreachable from every remote-tracking branch. The matching merged PR is
|
|
713
|
+
# the only signal that the work landed.
|
|
714
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
715
|
+
append_pr_meta_for_current_head "$case_dir"
|
|
716
|
+
pr_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
717
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
718
|
+
|
|
719
|
+
set +e
|
|
720
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
721
|
+
rc=$?
|
|
722
|
+
set -e
|
|
723
|
+
|
|
724
|
+
expect_code 0 "$rc" "squash-merged: teardown should succeed when the PR is merged"
|
|
725
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "squash-merged: teardown printed a REFUSED line"
|
|
726
|
+
pass "squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)"
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
test_squash_merged_pr_allows_when_head_ancestor_of_pr_head() {
|
|
730
|
+
local case_dir rc local_head pr_head
|
|
731
|
+
case_dir=$(make_case squash-ancestor)
|
|
732
|
+
write_meta "$case_dir" no-mistakes ship
|
|
733
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
734
|
+
append_pr_meta_url "$case_dir"
|
|
735
|
+
local_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
736
|
+
pr_head=$(commit_tree_from_wt_head "$case_dir" "$local_head" "no-mistakes follow-up")
|
|
737
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
738
|
+
|
|
739
|
+
set +e
|
|
740
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
741
|
+
rc=$?
|
|
742
|
+
set -e
|
|
743
|
+
|
|
744
|
+
expect_code 0 "$rc" "squash-ancestor: teardown should succeed when local HEAD is in the merged PR head"
|
|
745
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "squash-ancestor: teardown printed a REFUSED line"
|
|
746
|
+
pass "squash-merged PR accepts a local HEAD that is an ancestor of the final PR head"
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
test_no_pr_recorded_discovers_merged_pr_by_branch_allows() {
|
|
750
|
+
local case_dir rc local_head pr_head
|
|
751
|
+
case_dir=$(make_case no-pr-branch-discovery)
|
|
752
|
+
write_meta "$case_dir" no-mistakes ship
|
|
753
|
+
# Reproduces the real false-refusal report exactly, with NO pr=/pr_head=
|
|
754
|
+
# recorded in meta at all (fm-pr-check.sh was never run, e.g. a yolo merge on
|
|
755
|
+
# a repo with no PR CI so the "checks green" trigger that fires it never
|
|
756
|
+
# happened): a branch with a commit, a no-mistakes auto-fix commit pushed on
|
|
757
|
+
# top that never made it back into the local worktree, a squash merge onto
|
|
758
|
+
# main under a brand-new SHA, and the head branch deleted (simulated here by
|
|
759
|
+
# never pushing fm/task-x1 at all, so no refs/remotes/origin/fm/task-x1
|
|
760
|
+
# exists to make HEAD "reachable").
|
|
761
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
762
|
+
local_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
763
|
+
pr_head=$(commit_tree_from_wt_head "$case_dir" "$local_head" "no-mistakes auto-fix")
|
|
764
|
+
land_on_origin_main "$case_dir" feature.txt hello
|
|
765
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
766
|
+
seed_backlog_in_flight "$case_dir"
|
|
767
|
+
# No append_pr_meta_* call: state/task-x1.meta has no pr= or pr_head= line.
|
|
768
|
+
|
|
769
|
+
! grep -qE '^(pr|pr_head)=' "$case_dir/state/task-x1.meta" \
|
|
770
|
+
|| fail "no-pr-branch-discovery: test setup bug, meta unexpectedly has a pr= line"
|
|
771
|
+
|
|
772
|
+
set +e
|
|
773
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
774
|
+
rc=$?
|
|
775
|
+
set -e
|
|
776
|
+
|
|
777
|
+
expect_code 0 "$rc" "no-pr-branch-discovery: teardown should succeed by discovering the merged PR from the branch name"
|
|
778
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "no-pr-branch-discovery: teardown printed a REFUSED line"
|
|
779
|
+
assert_grep 'https://github.com/example/repo/pull/7' "$case_dir/data/backlog.md" \
|
|
780
|
+
"no-pr-branch-discovery: resolved PR URL was not recorded on completion"
|
|
781
|
+
pass "teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded"
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
test_squash_merged_pr_allows_replayed_unpushed_patch() {
|
|
785
|
+
local case_dir rc parent_head pr_head
|
|
786
|
+
case_dir=$(make_case squash-replayed-patch)
|
|
787
|
+
write_meta "$case_dir" no-mistakes ship
|
|
788
|
+
wt_commit_file "$case_dir" local-parent.txt parent "local parent"
|
|
789
|
+
parent_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
790
|
+
git -C "$case_dir/wt" push -q origin "$parent_head:refs/heads/fm/task-x1"
|
|
791
|
+
git -C "$case_dir/project" fetch -q origin fm/task-x1
|
|
792
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
793
|
+
append_pr_meta_url "$case_dir"
|
|
794
|
+
pr_head=$(land_equivalent_patch_on_origin_branch "$case_dir" pr-head feature.txt hello "add feature")
|
|
795
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
796
|
+
|
|
797
|
+
set +e
|
|
798
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
799
|
+
rc=$?
|
|
800
|
+
set -e
|
|
801
|
+
|
|
802
|
+
expect_code 0 "$rc" "squash-replayed-patch: teardown should succeed when unpushed local patch is in the merged PR head"
|
|
803
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "squash-replayed-patch: teardown printed a REFUSED line"
|
|
804
|
+
pass "squash-merged PR accepts replayed unpushed local patches contained in the PR head"
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
test_merged_pr_with_later_local_commit_refuses() {
|
|
808
|
+
local case_dir rc pr_head
|
|
809
|
+
case_dir=$(make_case stale-pr-head)
|
|
810
|
+
write_meta "$case_dir" no-mistakes ship
|
|
811
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
812
|
+
append_pr_meta_for_current_head "$case_dir"
|
|
813
|
+
pr_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
814
|
+
wt_commit_file "$case_dir" later.txt local-only "local follow-up"
|
|
815
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
816
|
+
|
|
817
|
+
set +e
|
|
818
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
819
|
+
rc=$?
|
|
820
|
+
set -e
|
|
821
|
+
|
|
822
|
+
expect_code 1 "$rc" "stale-pr-head: teardown should refuse when HEAD moved after PR recording"
|
|
823
|
+
grep -q REFUSED "$case_dir/stderr" || fail "stale-pr-head: no REFUSED line in stderr"
|
|
824
|
+
pass "merged PR does not allow teardown after a later local commit"
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
test_pr_check_does_not_refresh_stale_pr_head() {
|
|
828
|
+
local case_dir rc pr_head new_head count
|
|
829
|
+
case_dir=$(make_case pr-check-stale)
|
|
830
|
+
write_meta "$case_dir" no-mistakes ship
|
|
831
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
832
|
+
pr_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
833
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
834
|
+
|
|
835
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
836
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
837
|
+
PATH="$case_dir/fakebin:$PATH" \
|
|
838
|
+
"$PR_CHECK" task-x1 https://github.com/example/repo/pull/7 >/dev/null
|
|
839
|
+
|
|
840
|
+
wt_commit_file "$case_dir" later.txt local-only "local follow-up"
|
|
841
|
+
new_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
842
|
+
|
|
843
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
844
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
845
|
+
PATH="$case_dir/fakebin:$PATH" \
|
|
846
|
+
"$PR_CHECK" task-x1 https://github.com/example/repo/pull/7 >/dev/null
|
|
847
|
+
|
|
848
|
+
count=$(grep -c '^pr_head=' "$case_dir/state/task-x1.meta" || true)
|
|
849
|
+
expect_code 1 "$count" "pr-check-stale: stale rerun should not append a second pr_head"
|
|
850
|
+
! grep -qxF "pr_head=$new_head" "$case_dir/state/task-x1.meta" \
|
|
851
|
+
|| fail "pr-check-stale: stale rerun recorded the later local HEAD"
|
|
852
|
+
|
|
853
|
+
set +e
|
|
854
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
855
|
+
rc=$?
|
|
856
|
+
set -e
|
|
857
|
+
|
|
858
|
+
expect_code 1 "$rc" "pr-check-stale: teardown should refuse after a later local commit"
|
|
859
|
+
grep -q REFUSED "$case_dir/stderr" || fail "pr-check-stale: no REFUSED line in stderr"
|
|
860
|
+
pass "fm-pr-check does not refresh PR head after HEAD moves"
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
test_pr_check_records_remote_head_when_local_lags() {
|
|
864
|
+
local case_dir local_head pr_head
|
|
865
|
+
case_dir=$(make_case pr-check-local-lags)
|
|
866
|
+
write_meta "$case_dir" no-mistakes ship
|
|
867
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
868
|
+
local_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
869
|
+
pr_head=$(commit_tree_from_wt_head "$case_dir" "$local_head" "no-mistakes follow-up")
|
|
870
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
871
|
+
|
|
872
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
873
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
874
|
+
PATH="$case_dir/fakebin:$PATH" \
|
|
875
|
+
"$PR_CHECK" task-x1 https://github.com/example/repo/pull/7 >/dev/null
|
|
876
|
+
|
|
877
|
+
grep -qxF "pr_head=$pr_head" "$case_dir/state/task-x1.meta" \
|
|
878
|
+
|| fail "pr-check-local-lags: did not record GitHub PR head"
|
|
879
|
+
! grep -qxF "pr_head=$local_head" "$case_dir/state/task-x1.meta" \
|
|
880
|
+
|| fail "pr-check-local-lags: recorded local HEAD instead of remote PR head"
|
|
881
|
+
pass "fm-pr-check records the remote PR head when the local worktree lags"
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
test_content_in_default_fallback_allows() {
|
|
885
|
+
local case_dir rc
|
|
886
|
+
case_dir=$(make_case content-landed)
|
|
887
|
+
write_meta "$case_dir" no-mistakes ship
|
|
888
|
+
# No pr= recorded and the default gh-axi mock reports no PR, so the merged-PR path
|
|
889
|
+
# cannot fire and the content check must carry it. The branch adds feature.txt, and
|
|
890
|
+
# the same net change has independently landed on origin/main via a squash commit.
|
|
891
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
892
|
+
land_on_origin_main "$case_dir" feature.txt hello
|
|
893
|
+
|
|
894
|
+
set +e
|
|
895
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
896
|
+
rc=$?
|
|
897
|
+
set -e
|
|
898
|
+
|
|
899
|
+
expect_code 0 "$rc" "content-landed: teardown should succeed when content is already in the default branch"
|
|
900
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "content-landed: teardown printed a REFUSED line"
|
|
901
|
+
pass "worktree whose content already landed in the default branch is torn down (content fallback)"
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
test_content_fallback_refreshes_stale_origin_ref() {
|
|
905
|
+
local case_dir rc
|
|
906
|
+
case_dir=$(make_case content-stale-ref)
|
|
907
|
+
write_meta "$case_dir" no-mistakes ship
|
|
908
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
909
|
+
git -C "$case_dir/project" config --unset-all remote.origin.fetch
|
|
910
|
+
git -C "$case_dir/project" config --add remote.origin.fetch '+refs/heads/not-main:refs/remotes/origin/not-main'
|
|
911
|
+
land_on_origin_main "$case_dir" feature.txt hello
|
|
912
|
+
|
|
913
|
+
set +e
|
|
914
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
915
|
+
rc=$?
|
|
916
|
+
set -e
|
|
917
|
+
|
|
918
|
+
expect_code 0 "$rc" "content-stale-ref: teardown should use the freshly fetched default branch"
|
|
919
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "content-stale-ref: teardown printed a REFUSED line"
|
|
920
|
+
pass "content fallback refreshes origin default before comparing trees"
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
test_dirty_worktree_refuses() {
|
|
924
|
+
local case_dir rc pr_head
|
|
925
|
+
case_dir=$(make_case dirty-wt)
|
|
926
|
+
write_meta "$case_dir" no-mistakes ship
|
|
927
|
+
printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta"
|
|
928
|
+
# The committed work has fully landed (merged PR + content in default), but an
|
|
929
|
+
# uncommitted edit remains. Dirtiness must refuse regardless: the reset would
|
|
930
|
+
# discard those changes.
|
|
931
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
932
|
+
land_on_origin_main "$case_dir" feature.txt hello
|
|
933
|
+
pr_head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
934
|
+
add_gh_pr_merged_for_head "$case_dir" "$pr_head"
|
|
935
|
+
printf '%s\n' "uncommitted edit" > "$case_dir/wt/feature.txt"
|
|
936
|
+
|
|
937
|
+
set +e
|
|
938
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
939
|
+
rc=$?
|
|
940
|
+
set -e
|
|
941
|
+
|
|
942
|
+
expect_code 1 "$rc" "dirty-wt: teardown should refuse a dirty worktree even when the committed work has landed"
|
|
943
|
+
grep -q REFUSED "$case_dir/stderr" || fail "dirty-wt: no REFUSED line in stderr"
|
|
944
|
+
grep -q "uncommitted changes" "$case_dir/stderr" || fail "dirty-wt: refusal did not cite uncommitted changes"
|
|
945
|
+
pass "dirty worktree is refused even when its committed work has landed (dirty always wins)"
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
test_gh_error_and_content_absent_refuses() {
|
|
949
|
+
local case_dir rc
|
|
950
|
+
case_dir=$(make_case gh-error)
|
|
951
|
+
write_meta "$case_dir" no-mistakes ship
|
|
952
|
+
printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta"
|
|
953
|
+
# Real content not pushed, the PR lookup errors, and origin/main never gained the
|
|
954
|
+
# content. The fail-safe must refuse rather than allow on a transient gh failure.
|
|
955
|
+
wt_commit_file "$case_dir" feature.txt hello "add feature"
|
|
956
|
+
add_gh_axi_error "$case_dir"
|
|
957
|
+
|
|
958
|
+
set +e
|
|
959
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
960
|
+
rc=$?
|
|
961
|
+
set -e
|
|
962
|
+
|
|
963
|
+
expect_code 1 "$rc" "gh-error: teardown should refuse when the PR lookup errors and content is not landed"
|
|
964
|
+
grep -q REFUSED "$case_dir/stderr" || fail "gh-error: no REFUSED line in stderr"
|
|
965
|
+
pass "gh lookup error with content not in default refuses (fail-safe)"
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
test_stale_index_lock_cleared_and_teardown_succeeds() {
|
|
969
|
+
local case_dir rc lock
|
|
970
|
+
case_dir=$(make_case stale-index-lock)
|
|
971
|
+
write_meta "$case_dir" no-mistakes ship
|
|
972
|
+
wt_commit "$case_dir" "shippable work"
|
|
973
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
974
|
+
git -C "$case_dir/project" fetch -q origin
|
|
975
|
+
|
|
976
|
+
add_lock_aware_treehouse "$case_dir"
|
|
977
|
+
add_lsof_no_holder "$case_dir"
|
|
978
|
+
|
|
979
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
980
|
+
mkdir -p "$(dirname "$lock")"
|
|
981
|
+
: > "$lock"
|
|
982
|
+
touch -t 200001010000 "$lock"
|
|
983
|
+
|
|
984
|
+
set +e
|
|
985
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
986
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
987
|
+
rc=$?
|
|
988
|
+
set -e
|
|
989
|
+
|
|
990
|
+
expect_code 0 "$rc" "stale-index-lock: teardown should succeed after clearing the provably stale lock"
|
|
991
|
+
assert_grep "removed provably-stale git lock" "$case_dir/stderr" \
|
|
992
|
+
"stale-index-lock: teardown did not report clearing the stale lock"
|
|
993
|
+
assert_absent "$lock" "stale-index-lock: stale lock file should have been removed"
|
|
994
|
+
pass "provably-stale worktree index.lock (old, no live holder) is cleared and teardown succeeds"
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
test_live_index_lock_is_never_removed_and_teardown_refuses() {
|
|
998
|
+
local case_dir rc lock
|
|
999
|
+
case_dir=$(make_case live-index-lock)
|
|
1000
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1001
|
+
wt_commit "$case_dir" "shippable work"
|
|
1002
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1003
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1004
|
+
|
|
1005
|
+
add_lock_aware_treehouse "$case_dir"
|
|
1006
|
+
add_lsof_live_holder "$case_dir"
|
|
1007
|
+
|
|
1008
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1009
|
+
mkdir -p "$(dirname "$lock")"
|
|
1010
|
+
: > "$lock"
|
|
1011
|
+
# Even an old mtime must not be enough on its own: a live holder always wins.
|
|
1012
|
+
touch -t 200001010000 "$lock"
|
|
1013
|
+
|
|
1014
|
+
set +e
|
|
1015
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
1016
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1017
|
+
rc=$?
|
|
1018
|
+
set -e
|
|
1019
|
+
|
|
1020
|
+
expect_code 1 "$rc" "live-index-lock: teardown should refuse when the lock has a live holder"
|
|
1021
|
+
assert_grep "not provably stale" "$case_dir/stderr" \
|
|
1022
|
+
"live-index-lock: teardown did not explain the refusal"
|
|
1023
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "removed provably-stale git lock" \
|
|
1024
|
+
"live-index-lock: teardown removed a lock with a live holder"
|
|
1025
|
+
[ -e "$lock" ] || fail "live-index-lock: live-held lock file was removed"
|
|
1026
|
+
pass "live-held worktree index.lock is never removed and teardown refuses"
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
test_lsof_error_never_clears_index_lock() {
|
|
1030
|
+
local case_dir rc lock
|
|
1031
|
+
case_dir=$(make_case lsof-error-index-lock)
|
|
1032
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1033
|
+
wt_commit "$case_dir" "shippable work"
|
|
1034
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1035
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1036
|
+
|
|
1037
|
+
add_lock_aware_treehouse "$case_dir"
|
|
1038
|
+
add_lsof_error "$case_dir"
|
|
1039
|
+
|
|
1040
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1041
|
+
mkdir -p "$(dirname "$lock")"
|
|
1042
|
+
: > "$lock"
|
|
1043
|
+
touch -t 200001010000 "$lock"
|
|
1044
|
+
|
|
1045
|
+
set +e
|
|
1046
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
1047
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1048
|
+
rc=$?
|
|
1049
|
+
set -e
|
|
1050
|
+
|
|
1051
|
+
expect_code 1 "$rc" "lsof-error-index-lock: teardown should refuse when lsof errors"
|
|
1052
|
+
assert_grep "REFUSED: cannot determine leaked processes" "$case_dir/stderr" \
|
|
1053
|
+
"lsof-error-index-lock: teardown did not report the lsof failure"
|
|
1054
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "removed provably-stale git lock" \
|
|
1055
|
+
"lsof-error-index-lock: teardown removed a lock after lsof failed"
|
|
1056
|
+
[ -e "$lock" ] || fail "lsof-error-index-lock: lock file was removed after lsof failed"
|
|
1057
|
+
pass "lsof errors leave worktree index.lock in place and refuse teardown"
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
test_stale_index_lock_cleanup_rechecks_dirty_worktree() {
|
|
1061
|
+
local case_dir rc lock
|
|
1062
|
+
case_dir=$(make_case stale-lock-dirty-recheck)
|
|
1063
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1064
|
+
wt_commit_file "$case_dir" feature.txt landed "landed work"
|
|
1065
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1066
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1067
|
+
printf '%s\n' dirty > "$case_dir/wt/feature.txt"
|
|
1068
|
+
|
|
1069
|
+
add_lock_aware_treehouse "$case_dir"
|
|
1070
|
+
add_lsof_no_holder "$case_dir"
|
|
1071
|
+
add_git_status_lock_failure "$case_dir"
|
|
1072
|
+
|
|
1073
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1074
|
+
mkdir -p "$(dirname "$lock")"
|
|
1075
|
+
: > "$lock"
|
|
1076
|
+
touch -t 200001010000 "$lock"
|
|
1077
|
+
|
|
1078
|
+
set +e
|
|
1079
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
1080
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1081
|
+
rc=$?
|
|
1082
|
+
set -e
|
|
1083
|
+
|
|
1084
|
+
expect_code 1 "$rc" "stale-lock-dirty-recheck: teardown should refuse dirty work after clearing the stale lock"
|
|
1085
|
+
assert_grep "removed provably-stale git lock" "$case_dir/stderr" \
|
|
1086
|
+
"stale-lock-dirty-recheck: teardown did not report clearing the stale lock"
|
|
1087
|
+
assert_grep "uncommitted changes present" "$case_dir/stderr" \
|
|
1088
|
+
"stale-lock-dirty-recheck: teardown did not re-run the dirty check"
|
|
1089
|
+
assert_absent "$lock" "stale-lock-dirty-recheck: stale lock file should have been removed"
|
|
1090
|
+
[ -f "$case_dir/state/task-x1.meta" ] || fail "stale-lock-dirty-recheck: teardown completed despite dirty work"
|
|
1091
|
+
pass "stale lock cleanup rechecks and refuses dirty worktree before return"
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
test_non_linked_index_lock_path_is_checked_from_worktree() {
|
|
1095
|
+
local case_dir rc lock
|
|
1096
|
+
case_dir=$(make_case non-linked-index-lock)
|
|
1097
|
+
git -C "$case_dir/project" worktree remove --force "$case_dir/wt"
|
|
1098
|
+
git clone -q "$case_dir/origin.git" "$case_dir/wt"
|
|
1099
|
+
git -C "$case_dir/wt" checkout -q -b fm/task-x1
|
|
1100
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1101
|
+
wt_commit "$case_dir" "shippable normal clone work"
|
|
1102
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1103
|
+
git -C "$case_dir/wt" fetch -q origin
|
|
1104
|
+
|
|
1105
|
+
add_lock_aware_treehouse "$case_dir"
|
|
1106
|
+
add_lsof_no_holder "$case_dir"
|
|
1107
|
+
|
|
1108
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1109
|
+
mkdir -p "$(dirname "$lock")"
|
|
1110
|
+
: > "$lock"
|
|
1111
|
+
touch -t 200001010000 "$lock"
|
|
1112
|
+
|
|
1113
|
+
set +e
|
|
1114
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
1115
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1116
|
+
rc=$?
|
|
1117
|
+
set -e
|
|
1118
|
+
|
|
1119
|
+
expect_code 0 "$rc" "non-linked-index-lock: teardown should clear a normal repo index.lock"
|
|
1120
|
+
assert_grep "removed provably-stale git lock" "$case_dir/stderr" \
|
|
1121
|
+
"non-linked-index-lock: teardown did not report clearing the stale lock"
|
|
1122
|
+
assert_absent "$lock" "non-linked-index-lock: stale lock file should have been removed"
|
|
1123
|
+
pass "normal repo index.lock is resolved from the worktree and cleared when stale"
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
test_index_lock_mtime_read_failure_refuses() {
|
|
1127
|
+
local case_dir rc lock
|
|
1128
|
+
case_dir=$(make_case mtime-error-index-lock)
|
|
1129
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1130
|
+
wt_commit "$case_dir" "shippable work"
|
|
1131
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1132
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1133
|
+
|
|
1134
|
+
add_lock_aware_treehouse "$case_dir"
|
|
1135
|
+
add_lsof_no_holder "$case_dir"
|
|
1136
|
+
add_stat_error "$case_dir"
|
|
1137
|
+
|
|
1138
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1139
|
+
mkdir -p "$(dirname "$lock")"
|
|
1140
|
+
: > "$lock"
|
|
1141
|
+
touch -t 200001010000 "$lock"
|
|
1142
|
+
|
|
1143
|
+
set +e
|
|
1144
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
1145
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1146
|
+
rc=$?
|
|
1147
|
+
set -e
|
|
1148
|
+
|
|
1149
|
+
expect_code 1 "$rc" "mtime-error-index-lock: teardown should refuse when lock mtime cannot be read"
|
|
1150
|
+
assert_grep "cannot read mtime for git lock" "$case_dir/stderr" \
|
|
1151
|
+
"mtime-error-index-lock: teardown did not report the mtime read failure"
|
|
1152
|
+
assert_grep "not provably stale" "$case_dir/stderr" \
|
|
1153
|
+
"mtime-error-index-lock: teardown did not explain the refusal"
|
|
1154
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "removed provably-stale git lock" \
|
|
1155
|
+
"mtime-error-index-lock: teardown removed a lock after mtime read failed"
|
|
1156
|
+
[ -e "$lock" ] || fail "mtime-error-index-lock: lock file was removed after mtime read failed"
|
|
1157
|
+
pass "lock mtime read failures leave worktree index.lock in place and refuse teardown"
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
test_transient_index_lock_clears_after_first_attempt_and_retry_succeeds() {
|
|
1161
|
+
local case_dir rc lock attempt_file
|
|
1162
|
+
case_dir=$(make_case transient-index-lock-retry)
|
|
1163
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1164
|
+
wt_commit "$case_dir" "shippable work"
|
|
1165
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1166
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1167
|
+
|
|
1168
|
+
add_transient_lock_treehouse "$case_dir"
|
|
1169
|
+
add_lsof_no_holder "$case_dir"
|
|
1170
|
+
|
|
1171
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1172
|
+
mkdir -p "$(dirname "$lock")"
|
|
1173
|
+
: > "$lock"
|
|
1174
|
+
# Fresh lock: not old enough for the force-remove path; patience must win.
|
|
1175
|
+
touch "$lock"
|
|
1176
|
+
|
|
1177
|
+
attempt_file="$case_dir/treehouse-attempts"
|
|
1178
|
+
: > "$attempt_file"
|
|
1179
|
+
|
|
1180
|
+
set +e
|
|
1181
|
+
TREEHOUSE_ATTEMPT_FILE="$attempt_file" \
|
|
1182
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRIES=2 \
|
|
1183
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=0 \
|
|
1184
|
+
FM_STALE_WORKTREE_LOCK_AGE_SECS=3600 \
|
|
1185
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1186
|
+
rc=$?
|
|
1187
|
+
set -e
|
|
1188
|
+
|
|
1189
|
+
expect_code 0 "$rc" "transient-index-lock: teardown should succeed on retry after lock self-clears"
|
|
1190
|
+
assert_grep "succeeded on retry" "$case_dir/stderr" \
|
|
1191
|
+
"transient-index-lock: teardown did not report success on retry"
|
|
1192
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "removed provably-stale git lock" \
|
|
1193
|
+
"transient-index-lock: teardown force-removed a lock that only needed patience"
|
|
1194
|
+
[ "$(cat "$attempt_file")" = 2 ] \
|
|
1195
|
+
|| fail "transient-index-lock: expected exactly 2 treehouse return attempts, got $(cat "$attempt_file")"
|
|
1196
|
+
assert_absent "$lock" "transient-index-lock: lock should remain cleared after success"
|
|
1197
|
+
pass "transient index.lock cleared after first failed return is retried successfully without force-remove"
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
test_persistent_index_lock_exhausts_retries_and_refuses_loudly() {
|
|
1201
|
+
local case_dir rc lock
|
|
1202
|
+
case_dir=$(make_case persistent-index-lock)
|
|
1203
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1204
|
+
wt_commit "$case_dir" "shippable work"
|
|
1205
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1206
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1207
|
+
|
|
1208
|
+
add_persistent_lock_treehouse "$case_dir"
|
|
1209
|
+
# Fresh lock with a live holder: never provably stale, never force-removed.
|
|
1210
|
+
add_lsof_live_holder "$case_dir"
|
|
1211
|
+
|
|
1212
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1213
|
+
mkdir -p "$(dirname "$lock")"
|
|
1214
|
+
: > "$lock"
|
|
1215
|
+
touch "$lock"
|
|
1216
|
+
|
|
1217
|
+
set +e
|
|
1218
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRIES=2 \
|
|
1219
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=0 \
|
|
1220
|
+
FM_STALE_WORKTREE_LOCK_AGE_SECS=3600 \
|
|
1221
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1222
|
+
rc=$?
|
|
1223
|
+
set -e
|
|
1224
|
+
|
|
1225
|
+
expect_code 1 "$rc" "persistent-index-lock: teardown should refuse when the lock never clears"
|
|
1226
|
+
assert_grep "persisted across" "$case_dir/stderr" \
|
|
1227
|
+
"persistent-index-lock: teardown did not mention the exhausted retry window"
|
|
1228
|
+
assert_grep "not provably stale" "$case_dir/stderr" \
|
|
1229
|
+
"persistent-index-lock: teardown did not explain the refusal"
|
|
1230
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "removed provably-stale git lock" \
|
|
1231
|
+
"persistent-index-lock: teardown removed a non-stale lock"
|
|
1232
|
+
[ -e "$lock" ] || fail "persistent-index-lock: lock file was removed"
|
|
1233
|
+
[ -f "$case_dir/state/task-x1.meta" ] \
|
|
1234
|
+
|| fail "persistent-index-lock: teardown completed despite persistent lock"
|
|
1235
|
+
pass "persistent index.lock exhausts retries and refuses without force-removing the lock"
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
test_empty_retry_wait_uses_default_without_aborting() {
|
|
1239
|
+
local case_dir rc lock attempt_file
|
|
1240
|
+
case_dir=$(make_case empty-retry-wait)
|
|
1241
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1242
|
+
wt_commit "$case_dir" "shippable work"
|
|
1243
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1244
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1245
|
+
|
|
1246
|
+
add_transient_lock_treehouse "$case_dir"
|
|
1247
|
+
add_lsof_no_holder "$case_dir"
|
|
1248
|
+
|
|
1249
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1250
|
+
mkdir -p "$(dirname "$lock")"
|
|
1251
|
+
: > "$lock"
|
|
1252
|
+
|
|
1253
|
+
attempt_file="$case_dir/treehouse-attempts"
|
|
1254
|
+
: > "$attempt_file"
|
|
1255
|
+
|
|
1256
|
+
set +e
|
|
1257
|
+
TREEHOUSE_ATTEMPT_FILE="$attempt_file" \
|
|
1258
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRIES=1 \
|
|
1259
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS='' \
|
|
1260
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS='' \
|
|
1261
|
+
FM_STALE_WORKTREE_LOCK_AGE_SECS=3600 \
|
|
1262
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1263
|
+
rc=$?
|
|
1264
|
+
set -e
|
|
1265
|
+
|
|
1266
|
+
expect_code 0 "$rc" "empty-retry-wait: teardown should fall back to the default wait"
|
|
1267
|
+
assert_grep "waiting 1s and retrying" "$case_dir/stderr" \
|
|
1268
|
+
"empty-retry-wait: teardown did not use the default retry wait"
|
|
1269
|
+
[ "$(cat "$attempt_file")" = 2 ] \
|
|
1270
|
+
|| fail "empty-retry-wait: expected exactly 2 treehouse return attempts, got $(cat "$attempt_file")"
|
|
1271
|
+
pass "empty retry wait overrides use the default without aborting teardown"
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
test_fractional_legacy_retry_wait_refuses_without_arithmetic_error() {
|
|
1275
|
+
local case_dir rc lock
|
|
1276
|
+
case_dir=$(make_case fractional-legacy-retry-wait)
|
|
1277
|
+
write_meta "$case_dir" no-mistakes ship
|
|
1278
|
+
wt_commit "$case_dir" "shippable work"
|
|
1279
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
1280
|
+
git -C "$case_dir/project" fetch -q origin
|
|
1281
|
+
|
|
1282
|
+
add_persistent_lock_treehouse "$case_dir"
|
|
1283
|
+
add_lsof_live_holder "$case_dir"
|
|
1284
|
+
|
|
1285
|
+
lock=$(git_index_lock_path "$case_dir/wt")
|
|
1286
|
+
mkdir -p "$(dirname "$lock")"
|
|
1287
|
+
: > "$lock"
|
|
1288
|
+
|
|
1289
|
+
set +e
|
|
1290
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRIES=1 \
|
|
1291
|
+
FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS='' \
|
|
1292
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0.1 \
|
|
1293
|
+
FM_STALE_WORKTREE_LOCK_AGE_SECS=3600 \
|
|
1294
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1295
|
+
rc=$?
|
|
1296
|
+
set -e
|
|
1297
|
+
|
|
1298
|
+
expect_code 1 "$rc" "fractional-legacy-retry-wait: teardown should fail only for the persistent lock"
|
|
1299
|
+
assert_grep "waiting 0.1s each" "$case_dir/stderr" \
|
|
1300
|
+
"fractional-legacy-retry-wait: teardown did not preserve the legacy fractional wait"
|
|
1301
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "syntax error" \
|
|
1302
|
+
"fractional-legacy-retry-wait: teardown hit an arithmetic error"
|
|
1303
|
+
pass "fractional legacy retry wait remains supported without arithmetic"
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
test_local_only_force_overrides_unpushed() {
|
|
1307
|
+
local case_dir rc
|
|
1308
|
+
case_dir=$(make_case force-override)
|
|
1309
|
+
write_meta "$case_dir" local-only ship
|
|
1310
|
+
wt_commit "$case_dir" "unpushed work"
|
|
1311
|
+
|
|
1312
|
+
set +e
|
|
1313
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1314
|
+
rc=$?
|
|
1315
|
+
set -e
|
|
1316
|
+
|
|
1317
|
+
expect_code 0 "$rc" "force-override: --force should bypass the unpushed-work check"
|
|
1318
|
+
! grep -q REFUSED "$case_dir/stderr" || fail "force-override: REFUSED printed despite --force"
|
|
1319
|
+
pass "local-only worktree with unpushed work is torn down under --force (escape hatch)"
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
test_teardown_missing_busy_sidecar_completes() {
|
|
1323
|
+
local case_dir gen rc
|
|
1324
|
+
case_dir=$(make_case missing-busy-sidecar)
|
|
1325
|
+
write_meta "$case_dir" local-only ship
|
|
1326
|
+
gen=$("$ROOT/bin/fm-busy-event.sh" arm "$case_dir/state" task-x1)
|
|
1327
|
+
printf 'busy_gen=%s\n' "$gen" >> "$case_dir/state/task-x1.meta"
|
|
1328
|
+
rm -f "$case_dir/state/task-x1.busy-gen"
|
|
1329
|
+
|
|
1330
|
+
set +e
|
|
1331
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1332
|
+
rc=$?
|
|
1333
|
+
set -e
|
|
1334
|
+
|
|
1335
|
+
expect_code 0 "$rc" "missing-busy-sidecar: teardown should treat the incarnation as already retired"
|
|
1336
|
+
assert_absent "$case_dir/state/task-x1.busy-state" \
|
|
1337
|
+
"missing-busy-sidecar: teardown left the orphan busy record"
|
|
1338
|
+
assert_absent "$case_dir/state/task-x1.meta" \
|
|
1339
|
+
"missing-busy-sidecar: teardown remained incomplete"
|
|
1340
|
+
pass "teardown completes when an exact busy-state sidecar is already absent"
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
test_herdr_teardown_clears_escalation_marker() {
|
|
1344
|
+
local case_dir marker
|
|
1345
|
+
case_dir=$(make_case herdr-marker-cleanup)
|
|
1346
|
+
write_meta "$case_dir" local-only ship
|
|
1347
|
+
sed -i.bak 's/^window=.*/window=default:wG:pQ/' "$case_dir/state/task-x1.meta"
|
|
1348
|
+
rm -f "$case_dir/state/task-x1.meta.bak"
|
|
1349
|
+
printf '%s\n' \
|
|
1350
|
+
'backend=herdr' \
|
|
1351
|
+
'herdr_session=default' \
|
|
1352
|
+
'herdr_workspace_id=wG' \
|
|
1353
|
+
'herdr_tab_id=wG:tQ' \
|
|
1354
|
+
'herdr_pane_id=wG:pQ' >> "$case_dir/state/task-x1.meta"
|
|
1355
|
+
# A reachable session whose exact pane is already structurally gone: the
|
|
1356
|
+
# locked close is a no-op and the record gate sees a confirmed-gone pane.
|
|
1357
|
+
cat > "$case_dir/fakebin/herdr" <<SH
|
|
1358
|
+
#!/usr/bin/env bash
|
|
1359
|
+
case "\${1:-} \${2:-}" in
|
|
1360
|
+
"session list") printf '%s\n' '{"sessions":[{"name":"default","running":true,"socket_path":"$case_dir/herdr.sock"}]}' ;;
|
|
1361
|
+
"status --json") printf '%s\n' '{"server":{"running":true}}' ;;
|
|
1362
|
+
"pane get") printf '%s\n' '{"error":{"code":"pane_not_found"}}'; exit 1 ;;
|
|
1363
|
+
*) exit 0 ;;
|
|
1364
|
+
esac
|
|
1365
|
+
SH
|
|
1366
|
+
chmod +x "$case_dir/fakebin/herdr"
|
|
1367
|
+
marker="$case_dir/state/.herdr-escalated-default_wG_pQ"
|
|
1368
|
+
: > "$marker"
|
|
1369
|
+
|
|
1370
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" \
|
|
1371
|
+
|| fail "herdr-marker-cleanup: forced teardown failed: $(cat "$case_dir/stderr")"
|
|
1372
|
+
[ ! -e "$marker" ] || fail "herdr-marker-cleanup: teardown left the pane's escalation marker behind"
|
|
1373
|
+
pass "herdr teardown removes pane-owned escalation dedupe state"
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
# Flat (non-projected) Herdr endpoint whose fake pane exists until a locked
|
|
1377
|
+
# close removes it. The socket path is case-local so the derived presentation
|
|
1378
|
+
# lock never collides with another test or a real fleet session.
|
|
1379
|
+
configure_flat_herdr_teardown_case() { # <case-dir>
|
|
1380
|
+
local case_dir=$1
|
|
1381
|
+
sed -i.bak 's/^window=.*/window=default:wG:pQ/' "$case_dir/state/task-x1.meta"
|
|
1382
|
+
rm -f "$case_dir/state/task-x1.meta.bak"
|
|
1383
|
+
printf '%s\n' \
|
|
1384
|
+
'backend=herdr' \
|
|
1385
|
+
'herdr_session=default' \
|
|
1386
|
+
'herdr_workspace_id=wG' \
|
|
1387
|
+
'herdr_tab_id=wG:tQ' \
|
|
1388
|
+
'herdr_pane_id=wG:pQ' >> "$case_dir/state/task-x1.meta"
|
|
1389
|
+
cat > "$case_dir/fakebin/herdr" <<SH
|
|
1390
|
+
#!/usr/bin/env bash
|
|
1391
|
+
set -u
|
|
1392
|
+
printf '%s\n' "\$*" >> "\${FM_FAKE_HERDR_LOG:?}"
|
|
1393
|
+
case "\${1:-} \${2:-}" in
|
|
1394
|
+
"workspace list")
|
|
1395
|
+
printf '%s\n' '{"result":{"workspaces":[{"workspace_id":"wH","active_tab_id":"wH:t1","focused":true},{"workspace_id":"wG","active_tab_id":"wG:tQ","focused":false}]}}'
|
|
1396
|
+
;;
|
|
1397
|
+
"tab list")
|
|
1398
|
+
case "\$*" in
|
|
1399
|
+
*"--workspace wH"*) printf '%s\n' '{"result":{"tabs":[{"tab_id":"wH:t1","focused":true}]}}' ;;
|
|
1400
|
+
*"--workspace wG"*) printf '%s\n' '{"result":{"tabs":[{"tab_id":"wG:tQ","workspace_id":"wG"}]}}' ;;
|
|
1401
|
+
*) printf '%s\n' '{"result":{"tabs":[]}}' ;;
|
|
1402
|
+
esac
|
|
1403
|
+
;;
|
|
1404
|
+
"pane list")
|
|
1405
|
+
printf '%s\n' '{"result":{"panes":[{"pane_id":"wG:pQ","tab_id":"wG:tQ"}]}}'
|
|
1406
|
+
;;
|
|
1407
|
+
"status --json")
|
|
1408
|
+
printf '%s\n' '{"server":{"running":true}}'
|
|
1409
|
+
;;
|
|
1410
|
+
"session list")
|
|
1411
|
+
if [ "\${FM_FAKE_HERDR_SESSION_LIST_GARBAGE:-0}" = 1 ]; then
|
|
1412
|
+
printf '%s\n' 'not-json'
|
|
1413
|
+
else
|
|
1414
|
+
printf '%s\n' '{"sessions":[{"name":"default","running":true,"socket_path":"$case_dir/herdr.sock"}]}'
|
|
1415
|
+
fi
|
|
1416
|
+
;;
|
|
1417
|
+
"pane close")
|
|
1418
|
+
: > "\${FM_FAKE_HERDR_CLOSED:?}"
|
|
1419
|
+
;;
|
|
1420
|
+
"pane get")
|
|
1421
|
+
if [ "\${FM_FAKE_HERDR_PANE_GET_GARBAGE:-0}" = 1 ]; then
|
|
1422
|
+
printf '%s\n' 'not-json'
|
|
1423
|
+
exit 0
|
|
1424
|
+
fi
|
|
1425
|
+
if [ -e "\${FM_FAKE_HERDR_CLOSED:?}" ]; then
|
|
1426
|
+
printf '%s\n' '{"error":{"code":"pane_not_found"}}' >&2
|
|
1427
|
+
exit 1
|
|
1428
|
+
fi
|
|
1429
|
+
printf '%s\n' '{"result":{"pane":{"pane_id":"wG:pQ","tab_id":"wG:tQ","workspace_id":"wG"}}}'
|
|
1430
|
+
;;
|
|
1431
|
+
"agent get")
|
|
1432
|
+
printf '%s\n' '{"error":{"code":"agent_not_found"}}' >&2
|
|
1433
|
+
exit 1
|
|
1434
|
+
;;
|
|
1435
|
+
esac
|
|
1436
|
+
SH
|
|
1437
|
+
chmod +x "$case_dir/fakebin/herdr"
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
test_herdr_flat_teardown_refuses_orphaning_records_then_retry_completes() {
|
|
1441
|
+
local case_dir log closed lock ready release holder_pid rc thlog
|
|
1442
|
+
case_dir=$(make_case herdr-orphan-refusal)
|
|
1443
|
+
write_meta "$case_dir" local-only ship
|
|
1444
|
+
configure_flat_herdr_teardown_case "$case_dir"
|
|
1445
|
+
log="$case_dir/herdr.log"; : > "$log"
|
|
1446
|
+
closed="$case_dir/closed"
|
|
1447
|
+
: > "$case_dir/state/task-x1.status"
|
|
1448
|
+
: > "$case_dir/state/task-x1.turn-ended"
|
|
1449
|
+
# Record every treehouse invocation: the contended-lock refusal must fire
|
|
1450
|
+
# BEFORE the isolated copy is returned, so phase 1 may not invoke it at all.
|
|
1451
|
+
thlog="$case_dir/treehouse.log"; : > "$thlog"
|
|
1452
|
+
cat > "$case_dir/fakebin/treehouse" <<SH
|
|
1453
|
+
#!/usr/bin/env bash
|
|
1454
|
+
printf '%s\n' "\$*" >> "$thlog"
|
|
1455
|
+
exit 0
|
|
1456
|
+
SH
|
|
1457
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
1458
|
+
|
|
1459
|
+
lock=$(FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" PATH="$case_dir/fakebin:$PATH" \
|
|
1460
|
+
bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_presentation_session_lock_path default' "$ROOT") \
|
|
1461
|
+
|| fail "herdr-orphan-refusal: could not resolve the fixture presentation lock path"
|
|
1462
|
+
ready="$case_dir/lock-ready"; release="$case_dir/lock-release"
|
|
1463
|
+
ROOT="$ROOT" LOCK="$lock" READY="$ready" RELEASE="$release" bash -c '
|
|
1464
|
+
. "$ROOT/bin/fm-wake-lib.sh"
|
|
1465
|
+
fm_lock_try_acquire "$LOCK" || exit 1
|
|
1466
|
+
: > "$READY"
|
|
1467
|
+
while [ ! -e "$RELEASE" ]; do sleep 0.1; done
|
|
1468
|
+
fm_lock_release "$LOCK"
|
|
1469
|
+
' &
|
|
1470
|
+
holder_pid=$!
|
|
1471
|
+
local waited=0
|
|
1472
|
+
while [ ! -e "$ready" ] && [ "$waited" -lt 50 ]; do sleep 0.1; waited=$((waited + 1)); done
|
|
1473
|
+
[ -e "$ready" ] || fail "herdr-orphan-refusal: the contending lock holder never started"
|
|
1474
|
+
|
|
1475
|
+
rc=0
|
|
1476
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" \
|
|
1477
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1478
|
+
if [ "$rc" -eq 0 ]; then
|
|
1479
|
+
: > "$release"; wait "$holder_pid" 2>/dev/null || true
|
|
1480
|
+
fail "herdr-orphan-refusal: teardown reported success while the exact pane still existed under lock contention"
|
|
1481
|
+
fi
|
|
1482
|
+
[ -e "$case_dir/state/task-x1.meta" ] || { : > "$release"; fail "herdr-orphan-refusal: refusal erased the durable endpoint metadata"; }
|
|
1483
|
+
[ -e "$case_dir/state/task-x1.status" ] || { : > "$release"; fail "herdr-orphan-refusal: refusal erased the task status record"; }
|
|
1484
|
+
[ -e "$case_dir/state/task-x1.turn-ended" ] || { : > "$release"; fail "herdr-orphan-refusal: refusal erased the turn-end record"; }
|
|
1485
|
+
assert_grep "presentation lock is contended" "$case_dir/stderr" \
|
|
1486
|
+
"herdr-orphan-refusal: the pre-return refusal was not explained visibly"
|
|
1487
|
+
if [ -s "$thlog" ]; then
|
|
1488
|
+
: > "$release"; fail "herdr-orphan-refusal: the contended refusal still returned the isolated copy: $(cat "$thlog")"
|
|
1489
|
+
fi
|
|
1490
|
+
[ -d "$case_dir/wt" ] || { : > "$release"; fail "herdr-orphan-refusal: the contended refusal removed the isolated copy"; }
|
|
1491
|
+
if [ "$(git -C "$case_dir/wt" rev-parse --abbrev-ref HEAD 2>/dev/null)" != "fm/task-x1" ]; then
|
|
1492
|
+
: > "$release"; fail "herdr-orphan-refusal: the contended refusal dropped the task branch before refusing"
|
|
1493
|
+
fi
|
|
1494
|
+
if grep -q "teardown task-x1 complete" "$case_dir/stdout"; then
|
|
1495
|
+
: > "$release"; fail "herdr-orphan-refusal: refusal still reported cleanup complete"
|
|
1496
|
+
fi
|
|
1497
|
+
if grep -q "^pane close" "$log"; then
|
|
1498
|
+
: > "$release"; fail "herdr-orphan-refusal: an unlocked pane close was attempted under contention"
|
|
1499
|
+
fi
|
|
1500
|
+
|
|
1501
|
+
: > "$release"
|
|
1502
|
+
wait "$holder_pid" 2>/dev/null || true
|
|
1503
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_BACKEND_HERDR_IDLE_SHELL_PROOF_POLLS=1 \
|
|
1504
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout2" 2> "$case_dir/stderr2" \
|
|
1505
|
+
|| fail "herdr-orphan-refusal: the retry after lock release failed: $(cat "$case_dir/stderr2")"
|
|
1506
|
+
[ -e "$closed" ] || fail "herdr-orphan-refusal: the retry never closed the pane under the lock"
|
|
1507
|
+
[ -s "$thlog" ] || fail "herdr-orphan-refusal: the successful retry never returned the isolated copy"
|
|
1508
|
+
[ ! -e "$case_dir/state/task-x1.meta" ] || fail "herdr-orphan-refusal: the successful retry left the metadata behind"
|
|
1509
|
+
[ ! -e "$case_dir/state/task-x1.status" ] || fail "herdr-orphan-refusal: the successful retry left the status record behind"
|
|
1510
|
+
grep -q "teardown task-x1 complete" "$case_dir/stdout2" \
|
|
1511
|
+
|| fail "herdr-orphan-refusal: the successful retry did not report completion"
|
|
1512
|
+
pass "herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly"
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
test_herdr_flat_teardown_refuses_records_on_unparseable_presence() {
|
|
1516
|
+
local case_dir log closed rc
|
|
1517
|
+
case_dir=$(make_case herdr-garbage-presence)
|
|
1518
|
+
write_meta "$case_dir" local-only ship
|
|
1519
|
+
configure_flat_herdr_teardown_case "$case_dir"
|
|
1520
|
+
log="$case_dir/herdr.log"; : > "$log"
|
|
1521
|
+
closed="$case_dir/closed"
|
|
1522
|
+
: > "$case_dir/state/task-x1.status"
|
|
1523
|
+
rc=0
|
|
1524
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_FAKE_HERDR_PANE_GET_GARBAGE=1 \
|
|
1525
|
+
FM_BACKEND_HERDR_IDLE_SHELL_PROOF_POLLS=1 \
|
|
1526
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1527
|
+
[ "$rc" -ne 0 ] \
|
|
1528
|
+
|| fail "herdr-garbage-presence: teardown erased records on an unparseable pane presence"
|
|
1529
|
+
[ -e "$case_dir/state/task-x1.meta" ] \
|
|
1530
|
+
|| fail "herdr-garbage-presence: ambiguous presence erased the durable endpoint metadata"
|
|
1531
|
+
[ -e "$case_dir/state/task-x1.status" ] \
|
|
1532
|
+
|| fail "herdr-garbage-presence: ambiguous presence erased the task status record"
|
|
1533
|
+
assert_grep "ambiguous structured presence" "$case_dir/stderr" \
|
|
1534
|
+
"herdr-garbage-presence: the ambiguity refusal was not explained visibly"
|
|
1535
|
+
pass "herdr flat teardown never erases records when pane presence is unparseable"
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
assert_herdr_teardown_preflight_refuses_before_changes() {
|
|
1539
|
+
local mode=$1 case_dir log closed rc thlog teardown_bin
|
|
1540
|
+
case_dir=$(make_case "herdr-preflight-$mode")
|
|
1541
|
+
write_meta "$case_dir" local-only ship
|
|
1542
|
+
configure_flat_herdr_teardown_case "$case_dir"
|
|
1543
|
+
log="$case_dir/herdr.log"; : > "$log"
|
|
1544
|
+
closed="$case_dir/closed"
|
|
1545
|
+
: > "$case_dir/state/task-x1.status"
|
|
1546
|
+
: > "$case_dir/state/task-x1.turn-ended"
|
|
1547
|
+
thlog="$case_dir/treehouse.log"; : > "$thlog"
|
|
1548
|
+
cat > "$case_dir/fakebin/treehouse" <<SH
|
|
1549
|
+
#!/usr/bin/env bash
|
|
1550
|
+
printf '%s\n' "\$*" >> "$thlog"
|
|
1551
|
+
exit 0
|
|
1552
|
+
SH
|
|
1553
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
1554
|
+
|
|
1555
|
+
teardown_bin=$TEARDOWN
|
|
1556
|
+
case "$mode" in
|
|
1557
|
+
missing-adapter|missing-parser|missing-explicit-close-helper)
|
|
1558
|
+
mkdir -p "$case_dir/test-root"
|
|
1559
|
+
cp -R "$ROOT/bin" "$case_dir/test-root/bin"
|
|
1560
|
+
if [ "$mode" = missing-adapter ]; then
|
|
1561
|
+
rm -f "$case_dir/test-root/bin/backends/herdr.sh"
|
|
1562
|
+
elif [ "$mode" = missing-explicit-close-helper ]; then
|
|
1563
|
+
sed -i.bak 's/^fm_backend_herdr_explicit_close_pane_confirmed()/fm_backend_herdr_explicit_close_pane_confirmed_unavailable()/' \
|
|
1564
|
+
"$case_dir/test-root/bin/backends/herdr.sh"
|
|
1565
|
+
rm -f "$case_dir/test-root/bin/backends/herdr.sh.bak"
|
|
1566
|
+
else
|
|
1567
|
+
sed -i.bak 's/^fm_backend_herdr_parse_target()/fm_backend_herdr_parse_target_unavailable()/' \
|
|
1568
|
+
"$case_dir/test-root/bin/backends/herdr.sh"
|
|
1569
|
+
rm -f "$case_dir/test-root/bin/backends/herdr.sh.bak"
|
|
1570
|
+
fi
|
|
1571
|
+
teardown_bin="$case_dir/test-root/bin/fm-teardown.sh"
|
|
1572
|
+
;;
|
|
1573
|
+
esac
|
|
1574
|
+
rc=0
|
|
1575
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$case_dir/state" FM_DATA_OVERRIDE="$case_dir/data" \
|
|
1576
|
+
FM_CONFIG_OVERRIDE="$case_dir/config" FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" \
|
|
1577
|
+
FM_FAKE_HERDR_SESSION_LIST_GARBAGE="$([ "$mode" = unresolvable-lock ] && printf 1 || printf 0)" \
|
|
1578
|
+
PATH="$case_dir/fakebin:$PATH" \
|
|
1579
|
+
"$teardown_bin" task-x1 --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1580
|
+
[ "$rc" -ne 0 ] || fail "herdr-preflight-$mode: teardown continued without its required preflight"
|
|
1581
|
+
assert_grep "nothing was changed" "$case_dir/stderr" \
|
|
1582
|
+
"herdr-preflight-$mode: the retryable pre-return refusal was not explained visibly"
|
|
1583
|
+
[ -d "$case_dir/wt" ] || fail "herdr-preflight-$mode: refusal removed the isolated copy"
|
|
1584
|
+
[ "$(git -C "$case_dir/wt" rev-parse --abbrev-ref HEAD 2>/dev/null)" = "fm/task-x1" ] \
|
|
1585
|
+
|| fail "herdr-preflight-$mode: refusal dropped the task branch"
|
|
1586
|
+
[ -e "$case_dir/state/task-x1.meta" ] \
|
|
1587
|
+
|| fail "herdr-preflight-$mode: refusal erased the durable endpoint metadata"
|
|
1588
|
+
[ -e "$case_dir/state/task-x1.status" ] \
|
|
1589
|
+
|| fail "herdr-preflight-$mode: refusal erased the task status record"
|
|
1590
|
+
[ -e "$case_dir/state/task-x1.turn-ended" ] \
|
|
1591
|
+
|| fail "herdr-preflight-$mode: refusal erased the turn-end record"
|
|
1592
|
+
[ ! -s "$thlog" ] || fail "herdr-preflight-$mode: refusal returned the isolated copy"
|
|
1593
|
+
[ ! -e "$closed" ] || fail "herdr-preflight-$mode: refusal attempted an unlocked pane close"
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
test_herdr_flat_teardown_preflight_refuses_before_changes() {
|
|
1597
|
+
assert_herdr_teardown_preflight_refuses_before_changes unresolvable-lock
|
|
1598
|
+
assert_herdr_teardown_preflight_refuses_before_changes missing-adapter
|
|
1599
|
+
assert_herdr_teardown_preflight_refuses_before_changes missing-parser
|
|
1600
|
+
assert_herdr_teardown_preflight_refuses_before_changes missing-explicit-close-helper
|
|
1601
|
+
pass "herdr flat teardown preflight refuses before every destructive change"
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
configure_secondmate_with_herdr_child() { # <case-dir>
|
|
1605
|
+
local case_dir=$1 home="$1/secondmate-home"
|
|
1606
|
+
mkdir -p "$home/state" "$home/data" "$home/config" "$home/projects"
|
|
1607
|
+
printf '%s\n' task-x1 > "$home/.fm-secondmate-home"
|
|
1608
|
+
printf '%s\n' "home=$home" >> "$case_dir/state/task-x1.meta"
|
|
1609
|
+
fm_write_meta "$home/state/child-herdr.meta" \
|
|
1610
|
+
"window=childsession:wC:p1" \
|
|
1611
|
+
"endpoint_task_id=child-herdr" \
|
|
1612
|
+
"worktree=$case_dir/wt" \
|
|
1613
|
+
"project=$case_dir/project" \
|
|
1614
|
+
"kind=ship" \
|
|
1615
|
+
"mode=local-only" \
|
|
1616
|
+
"backend=herdr" \
|
|
1617
|
+
"herdr_session=childsession" \
|
|
1618
|
+
"herdr_workspace_id=wC" \
|
|
1619
|
+
"herdr_tab_id=wC:t1" \
|
|
1620
|
+
"herdr_pane_id=wC:p1"
|
|
1621
|
+
: > "$home/state/child-herdr.status"
|
|
1622
|
+
: > "$home/state/child-herdr.turn-ended"
|
|
1623
|
+
cat > "$case_dir/fakebin/herdr" <<SH
|
|
1624
|
+
#!/usr/bin/env bash
|
|
1625
|
+
set -u
|
|
1626
|
+
printf '%s\n' "\$*" >> "\${FM_FAKE_HERDR_LOG:?}"
|
|
1627
|
+
case "\${1:-} \${2:-}" in
|
|
1628
|
+
"session list")
|
|
1629
|
+
if [ "\${FM_FAKE_HERDR_SESSION_LIST_GARBAGE:-0}" = 1 ]; then
|
|
1630
|
+
printf '%s\n' 'not-json'
|
|
1631
|
+
else
|
|
1632
|
+
printf '%s\n' '{"sessions":[{"name":"childsession","running":true,"socket_path":"$case_dir/child.sock"}]}'
|
|
1633
|
+
fi
|
|
1634
|
+
;;
|
|
1635
|
+
"workspace list") exit 1 ;;
|
|
1636
|
+
"pane get")
|
|
1637
|
+
if [ -e "\${FM_FAKE_HERDR_CLOSED:?}" ]; then
|
|
1638
|
+
if [ "\${FM_FAKE_HERDR_PRESENCE_UNKNOWN:-0}" = 1 ]; then
|
|
1639
|
+
printf '%s\n' 'not-json'
|
|
1640
|
+
else
|
|
1641
|
+
printf '%s\n' '{"error":{"code":"pane_not_found"}}' >&2
|
|
1642
|
+
exit 1
|
|
1643
|
+
fi
|
|
1644
|
+
else
|
|
1645
|
+
printf '%s\n' '{"result":{"pane":{"pane_id":"wC:p1","tab_id":"wC:t1","workspace_id":"wC"}}}'
|
|
1646
|
+
fi
|
|
1647
|
+
;;
|
|
1648
|
+
"pane close") : > "\${FM_FAKE_HERDR_CLOSED:?}" ;;
|
|
1649
|
+
esac
|
|
1650
|
+
SH
|
|
1651
|
+
chmod +x "$case_dir/fakebin/herdr"
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
test_forced_secondmate_herdr_child_preflight_refuses_before_changes() {
|
|
1655
|
+
local case_dir home log closed rc thlog
|
|
1656
|
+
case_dir=$(make_case herdr-child-preflight)
|
|
1657
|
+
write_meta "$case_dir" local-only secondmate
|
|
1658
|
+
configure_secondmate_with_herdr_child "$case_dir"
|
|
1659
|
+
home="$case_dir/secondmate-home"
|
|
1660
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; thlog="$case_dir/treehouse.log"
|
|
1661
|
+
: > "$log"; : > "$thlog"
|
|
1662
|
+
cat > "$case_dir/fakebin/treehouse" <<SH
|
|
1663
|
+
#!/usr/bin/env bash
|
|
1664
|
+
printf '%s\n' "\$*" >> "$thlog"
|
|
1665
|
+
exit 0
|
|
1666
|
+
SH
|
|
1667
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
1668
|
+
rc=0
|
|
1669
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" \
|
|
1670
|
+
FM_FAKE_HERDR_SESSION_LIST_GARBAGE=1 \
|
|
1671
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1672
|
+
[ "$rc" -ne 0 ] || fail "herdr-child-preflight: teardown continued through an unresolvable child lock"
|
|
1673
|
+
[ -e "$case_dir/state/task-x1.meta" ] || fail "herdr-child-preflight: refusal erased the parent record"
|
|
1674
|
+
[ -e "$home/state/child-herdr.meta" ] || fail "herdr-child-preflight: refusal erased the child record"
|
|
1675
|
+
[ -e "$home/state/child-herdr.status" ] || fail "herdr-child-preflight: refusal erased child status"
|
|
1676
|
+
[ -d "$home" ] || fail "herdr-child-preflight: refusal removed the secondmate home"
|
|
1677
|
+
[ ! -s "$thlog" ] || fail "herdr-child-preflight: refusal returned work before child preflight"
|
|
1678
|
+
[ ! -e "$closed" ] || fail "herdr-child-preflight: refusal attempted a child close"
|
|
1679
|
+
assert_grep "nothing was changed" "$case_dir/stderr" \
|
|
1680
|
+
"herdr-child-preflight: refusal did not explain its non-mutating boundary"
|
|
1681
|
+
pass "forced secondmate teardown preflights every Herdr child before cleanup mutation"
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
configure_secondmate_with_tmux_children() { # <case-dir>
|
|
1685
|
+
local case_dir=$1 home="$1/secondmate-home" child child_wt
|
|
1686
|
+
mkdir -p "$home/state" "$home/data" "$home/config" "$home/projects"
|
|
1687
|
+
printf '%s\n' task-x1 > "$home/.fm-secondmate-home"
|
|
1688
|
+
printf '%s\n' "home=$home" >> "$case_dir/state/task-x1.meta"
|
|
1689
|
+
for child in child-a child-b; do
|
|
1690
|
+
child_wt="$case_dir/$child-wt"
|
|
1691
|
+
git -C "$case_dir/project" worktree add -q -b "fm/$child" "$child_wt" main
|
|
1692
|
+
fm_write_meta "$home/state/$child.meta" \
|
|
1693
|
+
"window=firstmate:fm-$child" \
|
|
1694
|
+
"endpoint_task_id=$child" \
|
|
1695
|
+
"worktree=$child_wt" \
|
|
1696
|
+
"project=$case_dir/project" \
|
|
1697
|
+
"kind=ship" \
|
|
1698
|
+
"mode=local-only"
|
|
1699
|
+
: > "$home/state/$child.status"
|
|
1700
|
+
done
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
test_forced_secondmate_teardown_holds_descendant_lifecycle_locks() {
|
|
1704
|
+
local case_dir home lock ready release holder_pid rc waited=0 child
|
|
1705
|
+
case_dir=$(make_case descendant-locks)
|
|
1706
|
+
write_meta "$case_dir" local-only secondmate
|
|
1707
|
+
configure_secondmate_with_tmux_children "$case_dir"
|
|
1708
|
+
home="$case_dir/secondmate-home"
|
|
1709
|
+
: > "$case_dir/kill.log"
|
|
1710
|
+
: > "$case_dir/treehouse.log"
|
|
1711
|
+
cat > "$case_dir/fakebin/tmux" <<SH
|
|
1712
|
+
#!/usr/bin/env bash
|
|
1713
|
+
printf '%s\n' "\$*" >> "$case_dir/kill.log"
|
|
1714
|
+
exit 0
|
|
1715
|
+
SH
|
|
1716
|
+
cat > "$case_dir/fakebin/treehouse" <<SH
|
|
1717
|
+
#!/usr/bin/env bash
|
|
1718
|
+
printf '%s\n' "\$*" >> "$case_dir/treehouse.log"
|
|
1719
|
+
exit 0
|
|
1720
|
+
SH
|
|
1721
|
+
chmod +x "$case_dir/fakebin/tmux" "$case_dir/fakebin/treehouse"
|
|
1722
|
+
|
|
1723
|
+
lock="$home/state/.control-child-b.lock"
|
|
1724
|
+
ready="$case_dir/lock-ready"
|
|
1725
|
+
release="$case_dir/lock-release"
|
|
1726
|
+
ROOT="$ROOT" LOCK="$lock" READY="$ready" RELEASE="$release" \
|
|
1727
|
+
HOME_STATE="$home/state" OWNER_PID="$$" bash -c '
|
|
1728
|
+
export FM_STATE_OVERRIDE="$HOME_STATE"
|
|
1729
|
+
. "$ROOT/bin/fm-wake-lib.sh"
|
|
1730
|
+
fm_lock_try_acquire "$LOCK" || exit 1
|
|
1731
|
+
: > "$READY"
|
|
1732
|
+
while [ ! -e "$RELEASE" ] && kill -0 "$OWNER_PID" 2>/dev/null; do sleep 0.1; done
|
|
1733
|
+
fm_lock_release "$LOCK"
|
|
1734
|
+
' &
|
|
1735
|
+
holder_pid=$!
|
|
1736
|
+
while [ ! -e "$ready" ] && [ "$waited" -lt 50 ]; do
|
|
1737
|
+
sleep 0.1
|
|
1738
|
+
waited=$((waited + 1))
|
|
1739
|
+
done
|
|
1740
|
+
[ -e "$ready" ] || fail "descendant-locks: the contending lifecycle action never acquired its lock"
|
|
1741
|
+
|
|
1742
|
+
rc=0
|
|
1743
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1744
|
+
if [ "$rc" -eq 0 ]; then
|
|
1745
|
+
: > "$release"
|
|
1746
|
+
wait "$holder_pid" 2>/dev/null || true
|
|
1747
|
+
fail "descendant-locks: forced teardown ignored a descendant lifecycle lock"
|
|
1748
|
+
fi
|
|
1749
|
+
assert_grep "descendant task child-b has a lifecycle action in flight" "$case_dir/stderr" \
|
|
1750
|
+
"descendant-locks: refusal did not name the contended descendant"
|
|
1751
|
+
[ ! -e "$home/state/.control-child-a.lock" ] \
|
|
1752
|
+
&& [ ! -e "$home/state/.meta-child-a.lock" ] \
|
|
1753
|
+
|| { : > "$release"; wait "$holder_pid" 2>/dev/null || true; fail "descendant-locks: refusal leaked earlier descendant locks"; }
|
|
1754
|
+
[ ! -s "$case_dir/kill.log" ] \
|
|
1755
|
+
|| { : > "$release"; wait "$holder_pid" 2>/dev/null || true; fail "descendant-locks: refusal killed an endpoint"; }
|
|
1756
|
+
[ ! -s "$case_dir/treehouse.log" ] \
|
|
1757
|
+
|| { : > "$release"; wait "$holder_pid" 2>/dev/null || true; fail "descendant-locks: refusal returned a worktree"; }
|
|
1758
|
+
[ -e "$case_dir/state/task-x1.meta" ] && [ -d "$home" ] \
|
|
1759
|
+
|| { : > "$release"; wait "$holder_pid" 2>/dev/null || true; fail "descendant-locks: refusal removed parent state"; }
|
|
1760
|
+
for child in child-a child-b; do
|
|
1761
|
+
[ -e "$home/state/$child.meta" ] && [ -d "$case_dir/$child-wt" ] \
|
|
1762
|
+
|| { : > "$release"; wait "$holder_pid" 2>/dev/null || true; fail "descendant-locks: refusal removed $child state or worktree"; }
|
|
1763
|
+
done
|
|
1764
|
+
|
|
1765
|
+
: > "$release"
|
|
1766
|
+
wait "$holder_pid" 2>/dev/null || true
|
|
1767
|
+
rc=0
|
|
1768
|
+
run_teardown "$case_dir" --force > "$case_dir/retry.stdout" 2> "$case_dir/retry.stderr" || rc=$?
|
|
1769
|
+
expect_code 0 "$rc" "descendant-locks: uncontended retry should complete"
|
|
1770
|
+
[ ! -e "$case_dir/state/task-x1.meta" ] && [ ! -d "$home" ] \
|
|
1771
|
+
|| fail "descendant-locks: uncontended retry retained retired task state"
|
|
1772
|
+
[ -s "$case_dir/kill.log" ] && [ -s "$case_dir/treehouse.log" ] \
|
|
1773
|
+
|| fail "descendant-locks: uncontended retry did not perform endpoint and worktree cleanup"
|
|
1774
|
+
pass "forced secondmate teardown holds every descendant lifecycle and metadata lock"
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
test_forced_secondmate_herdr_child_retains_records_when_close_unconfirmed() {
|
|
1778
|
+
local case_dir home log closed rc
|
|
1779
|
+
case_dir=$(make_case herdr-child-unconfirmed-close)
|
|
1780
|
+
write_meta "$case_dir" local-only secondmate
|
|
1781
|
+
configure_secondmate_with_herdr_child "$case_dir"
|
|
1782
|
+
home="$case_dir/secondmate-home"
|
|
1783
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; : > "$log"
|
|
1784
|
+
rc=0
|
|
1785
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_FAKE_HERDR_PRESENCE_UNKNOWN=1 \
|
|
1786
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1787
|
+
[ "$rc" -ne 0 ] || fail "herdr-child-unconfirmed-close: teardown erased records after an ambiguous close"
|
|
1788
|
+
[ -e "$closed" ] || fail "herdr-child-unconfirmed-close: fixture did not attempt the child close"
|
|
1789
|
+
[ -e "$home/state/child-herdr.meta" ] || fail "herdr-child-unconfirmed-close: ambiguous close erased child metadata"
|
|
1790
|
+
[ -e "$home/state/child-herdr.status" ] || fail "herdr-child-unconfirmed-close: ambiguous close erased child status"
|
|
1791
|
+
[ -e "$case_dir/state/task-x1.meta" ] || fail "herdr-child-unconfirmed-close: failed child cleanup erased parent metadata"
|
|
1792
|
+
[ -d "$home" ] || fail "herdr-child-unconfirmed-close: failed child cleanup removed the secondmate home"
|
|
1793
|
+
assert_grep "retaining that child's durable identity records" "$case_dir/stderr" \
|
|
1794
|
+
"herdr-child-unconfirmed-close: refusal did not explain child record retention"
|
|
1795
|
+
pass "forced secondmate teardown retains Herdr child identity until exact pane disappearance"
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
configure_nested_secondmate_with_herdr_grandchild() { # <case-dir>
|
|
1799
|
+
local case_dir=$1 home="$1/secondmate-home" nested_home="$1/secondmate-home/nested-home"
|
|
1800
|
+
mkdir -p "$home/state" "$home/data" "$home/config" "$home/projects"
|
|
1801
|
+
mkdir -p "$nested_home/state" "$nested_home/data" "$nested_home/config" "$nested_home/projects"
|
|
1802
|
+
printf '%s\n' task-x1 > "$home/.fm-secondmate-home"
|
|
1803
|
+
printf '%s\n' nested-sm > "$nested_home/.fm-secondmate-home"
|
|
1804
|
+
printf '%s\n' "home=$home" >> "$case_dir/state/task-x1.meta"
|
|
1805
|
+
fm_write_meta "$home/state/nested-sm.meta" \
|
|
1806
|
+
"window=firstmate:fm-nested-sm" \
|
|
1807
|
+
"endpoint_task_id=nested-sm" \
|
|
1808
|
+
"worktree=$case_dir/wt" \
|
|
1809
|
+
"project=$case_dir/project" \
|
|
1810
|
+
"kind=secondmate" \
|
|
1811
|
+
"mode=local-only" \
|
|
1812
|
+
"home=$nested_home"
|
|
1813
|
+
fm_write_meta "$nested_home/state/grandchild-herdr.meta" \
|
|
1814
|
+
"window=grandchildsession:wG:p1" \
|
|
1815
|
+
"endpoint_task_id=grandchild-herdr" \
|
|
1816
|
+
"worktree=$case_dir/wt" \
|
|
1817
|
+
"project=$case_dir/project" \
|
|
1818
|
+
"kind=ship" \
|
|
1819
|
+
"mode=local-only" \
|
|
1820
|
+
"backend=herdr" \
|
|
1821
|
+
"herdr_session=grandchildsession" \
|
|
1822
|
+
"herdr_workspace_id=wG" \
|
|
1823
|
+
"herdr_tab_id=wG:t1" \
|
|
1824
|
+
"herdr_pane_id=wG:p1"
|
|
1825
|
+
: > "$nested_home/state/grandchild-herdr.status"
|
|
1826
|
+
: > "$nested_home/state/grandchild-herdr.turn-ended"
|
|
1827
|
+
cat > "$case_dir/fakebin/herdr" <<SH
|
|
1828
|
+
#!/usr/bin/env bash
|
|
1829
|
+
set -u
|
|
1830
|
+
printf '%s\n' "\$*" >> "\${FM_FAKE_HERDR_LOG:?}"
|
|
1831
|
+
case "\${1:-} \${2:-}" in
|
|
1832
|
+
"session list")
|
|
1833
|
+
printf '%s\n' '{"sessions":[{"name":"grandchildsession","running":true,"socket_path":"$case_dir/grandchild.sock"}]}'
|
|
1834
|
+
;;
|
|
1835
|
+
"workspace list") exit 1 ;;
|
|
1836
|
+
"pane get")
|
|
1837
|
+
if [ -e "\${FM_FAKE_HERDR_CLOSED:?}" ]; then
|
|
1838
|
+
printf '%s\n' 'not-json'
|
|
1839
|
+
else
|
|
1840
|
+
printf '%s\n' '{"result":{"pane":{"pane_id":"wG:p1","tab_id":"wG:t1","workspace_id":"wG"}}}'
|
|
1841
|
+
fi
|
|
1842
|
+
;;
|
|
1843
|
+
"pane close") : > "\${FM_FAKE_HERDR_CLOSED:?}" ;;
|
|
1844
|
+
esac
|
|
1845
|
+
SH
|
|
1846
|
+
chmod +x "$case_dir/fakebin/herdr"
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
test_forced_teardown_retains_nested_secondmate_home_when_grandchild_close_unconfirmed() {
|
|
1850
|
+
local case_dir home nested_home log closed rc
|
|
1851
|
+
case_dir=$(make_case herdr-grandchild-unconfirmed-close)
|
|
1852
|
+
write_meta "$case_dir" local-only secondmate
|
|
1853
|
+
configure_nested_secondmate_with_herdr_grandchild "$case_dir"
|
|
1854
|
+
home="$case_dir/secondmate-home"; nested_home="$home/nested-home"
|
|
1855
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; : > "$log"
|
|
1856
|
+
rc=0
|
|
1857
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" \
|
|
1858
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1859
|
+
[ "$rc" -ne 0 ] \
|
|
1860
|
+
|| fail "herdr-grandchild-unconfirmed-close: teardown erased records after an ambiguous grandchild close"
|
|
1861
|
+
[ -e "$closed" ] \
|
|
1862
|
+
|| fail "herdr-grandchild-unconfirmed-close: fixture did not attempt the grandchild close"
|
|
1863
|
+
[ -d "$nested_home" ] \
|
|
1864
|
+
|| fail "herdr-grandchild-unconfirmed-close: the recursive failure still removed the nested secondmate home"
|
|
1865
|
+
[ -e "$nested_home/state/grandchild-herdr.meta" ] \
|
|
1866
|
+
|| fail "herdr-grandchild-unconfirmed-close: ambiguous close erased the grandchild's metadata"
|
|
1867
|
+
[ -e "$nested_home/state/grandchild-herdr.status" ] \
|
|
1868
|
+
|| fail "herdr-grandchild-unconfirmed-close: ambiguous close erased the grandchild's status record"
|
|
1869
|
+
[ -e "$home/state/nested-sm.meta" ] \
|
|
1870
|
+
|| fail "herdr-grandchild-unconfirmed-close: the recursive failure erased the nested secondmate's own record"
|
|
1871
|
+
[ -e "$case_dir/state/task-x1.meta" ] \
|
|
1872
|
+
|| fail "herdr-grandchild-unconfirmed-close: the recursive failure erased the top-level secondmate's record"
|
|
1873
|
+
pass "forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed"
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
configure_herdr_projection_teardown_case() { # <case-dir>
|
|
1877
|
+
local case_dir=$1 token=AbCdEfGhIjKlMnOpQrStUv
|
|
1878
|
+
sed -i.bak 's/^window=.*/window=fmtest:w1:p2/' "$case_dir/state/task-x1.meta"
|
|
1879
|
+
rm -f "$case_dir/state/task-x1.meta.bak"
|
|
1880
|
+
printf '%s\n' \
|
|
1881
|
+
'backend=herdr' \
|
|
1882
|
+
'herdr_session=fmtest' \
|
|
1883
|
+
'herdr_workspace_id=w1' \
|
|
1884
|
+
'herdr_tab_id=w1:t2' \
|
|
1885
|
+
'herdr_pane_id=w1:p2' >> "$case_dir/state/task-x1.meta"
|
|
1886
|
+
printf '%s\n' \
|
|
1887
|
+
'version=1' \
|
|
1888
|
+
'task_id=task-x1' \
|
|
1889
|
+
"projection_id=$token" > "$case_dir/state/task-x1.herdr-presentation"
|
|
1890
|
+
cat > "$case_dir/fakebin/herdr" <<'SH'
|
|
1891
|
+
#!/usr/bin/env bash
|
|
1892
|
+
set -u
|
|
1893
|
+
printf '%s\n' "$*" >> "${FM_FAKE_HERDR_LOG:?}"
|
|
1894
|
+
case "${1:-} ${2:-}" in
|
|
1895
|
+
"workspace list")
|
|
1896
|
+
if [ -e "${FM_FAKE_HERDR_RESTORED:?}" ]; then
|
|
1897
|
+
printf '%s\n' '{"result":{"workspaces":[{"workspace_id":"w2","active_tab_id":"w2:t2","label":"2ndmate-bravo","focused":true},{"workspace_id":"w3","active_tab_id":"w3:t1","label":"2ndmate-alpha","focused":false}]}}'
|
|
1898
|
+
elif [ -e "${FM_FAKE_HERDR_CLOSED:?}" ]; then
|
|
1899
|
+
printf '%s\n' '{"result":{"workspaces":[{"workspace_id":"w2","active_tab_id":"w2:t2","label":"2ndmate-bravo","focused":false},{"workspace_id":"w3","active_tab_id":"w3:t1","label":"2ndmate-alpha","focused":true}]}}'
|
|
1900
|
+
else
|
|
1901
|
+
printf '%s\n' '{"result":{"workspaces":[{"workspace_id":"w1","active_tab_id":"w1:t2","label":"firstmate/task-x1 · p:AbCdEfGhIjKlMnOpQrStUv","focused":false},{"workspace_id":"w2","active_tab_id":"w2:t2","label":"2ndmate-bravo","focused":true},{"workspace_id":"w3","active_tab_id":"w3:t1","label":"2ndmate-alpha","focused":false}]}}'
|
|
1902
|
+
fi
|
|
1903
|
+
;;
|
|
1904
|
+
"tab list")
|
|
1905
|
+
case "$*" in
|
|
1906
|
+
*"--workspace w2"*) printf '%s\n' '{"result":{"tabs":[{"tab_id":"w2:t2","focused":true}]}}' ;;
|
|
1907
|
+
*"--workspace w3"*) printf '%s\n' '{"result":{"tabs":[{"tab_id":"w3:t1","focused":true}]}}' ;;
|
|
1908
|
+
*) printf '%s\n' '{"result":{"tabs":[]}}' ;;
|
|
1909
|
+
esac
|
|
1910
|
+
;;
|
|
1911
|
+
"status --json")
|
|
1912
|
+
printf '%s\n' '{"server":{"running":true}}'
|
|
1913
|
+
;;
|
|
1914
|
+
"session list")
|
|
1915
|
+
printf '%s\n' '{"sessions":[{"name":"fmtest","running":true,"socket_path":"/tmp/fmtest.sock"}]}'
|
|
1916
|
+
;;
|
|
1917
|
+
"pane close")
|
|
1918
|
+
if [ "${FM_FAKE_HERDR_CLOSE_FAIL:-0}" = 1 ]; then
|
|
1919
|
+
exit 1
|
|
1920
|
+
fi
|
|
1921
|
+
: > "${FM_FAKE_HERDR_CLOSED:?}"
|
|
1922
|
+
;;
|
|
1923
|
+
"pane get")
|
|
1924
|
+
if [ -e "${FM_FAKE_HERDR_CLOSED:?}" ]; then
|
|
1925
|
+
if [ "${FM_FAKE_HERDR_PRESENCE_UNKNOWN:-0}" = 1 ]; then
|
|
1926
|
+
printf '%s\n' '{"error":{"code":"internal"}}' >&2
|
|
1927
|
+
exit 1
|
|
1928
|
+
fi
|
|
1929
|
+
printf '%s\n' '{"error":{"code":"pane_not_found"}}' >&2
|
|
1930
|
+
exit 1
|
|
1931
|
+
fi
|
|
1932
|
+
printf '%s\n' '{"result":{"pane":{"pane_id":"w1:p2","tab_id":"w1:t2","workspace_id":"w1"}}}'
|
|
1933
|
+
;;
|
|
1934
|
+
"tab get")
|
|
1935
|
+
printf '%s\n' '{"result":{"tab":{"tab_id":"w2:t2","workspace_id":"w2"}}}'
|
|
1936
|
+
;;
|
|
1937
|
+
"tab focus")
|
|
1938
|
+
if [ "${FM_FAKE_HERDR_RESTORE_FAIL:-0}" = 1 ]; then
|
|
1939
|
+
exit 1
|
|
1940
|
+
fi
|
|
1941
|
+
: > "${FM_FAKE_HERDR_RESTORED:?}"
|
|
1942
|
+
printf '%s\n' '{"result":{"tab":{"tab_id":"w2:t2","workspace_id":"w2","focused":true}}}'
|
|
1943
|
+
;;
|
|
1944
|
+
"agent get")
|
|
1945
|
+
printf '%s\n' '{"error":{"code":"agent_not_found"}}' >&2
|
|
1946
|
+
exit 1
|
|
1947
|
+
;;
|
|
1948
|
+
esac
|
|
1949
|
+
SH
|
|
1950
|
+
chmod +x "$case_dir/fakebin/herdr"
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
test_herdr_projection_teardown_retires_journal_only_after_confirmed_close() {
|
|
1954
|
+
local case_dir log closed restored
|
|
1955
|
+
case_dir=$(make_case herdr-projection-confirmed-close)
|
|
1956
|
+
write_meta "$case_dir" local-only ship
|
|
1957
|
+
configure_herdr_projection_teardown_case "$case_dir"
|
|
1958
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; restored="$case_dir/restored"; : > "$log"
|
|
1959
|
+
|
|
1960
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_FAKE_HERDR_RESTORED="$restored" \
|
|
1961
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" \
|
|
1962
|
+
|| fail "herdr-projection-confirmed-close: forced teardown failed"
|
|
1963
|
+
[ ! -e "$case_dir/state/task-x1.herdr-presentation" ] \
|
|
1964
|
+
|| fail "confirmed exact-pane close did not retire the presentation journal"
|
|
1965
|
+
assert_not_contains "$(cat "$log")" "workspace close" \
|
|
1966
|
+
"projected teardown must never call workspace close"
|
|
1967
|
+
assert_contains "$(cat "$log")" "tab focus w2:t2" \
|
|
1968
|
+
"projected teardown did not restore the exact pre-close active tab"
|
|
1969
|
+
pass "herdr projection teardown retires its journal only after confirming the exact recorded pane is gone"
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
test_herdr_projection_teardown_retains_journal_when_close_unconfirmed() {
|
|
1973
|
+
local case_dir log closed restored
|
|
1974
|
+
case_dir=$(make_case herdr-projection-unconfirmed-close)
|
|
1975
|
+
write_meta "$case_dir" local-only ship
|
|
1976
|
+
configure_herdr_projection_teardown_case "$case_dir"
|
|
1977
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; restored="$case_dir/restored"; : > "$log"
|
|
1978
|
+
|
|
1979
|
+
local rc=0
|
|
1980
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_FAKE_HERDR_RESTORED="$restored" FM_FAKE_HERDR_PRESENCE_UNKNOWN=1 \
|
|
1981
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
1982
|
+
[ "$rc" -ne 0 ] \
|
|
1983
|
+
|| fail "herdr-projection-unconfirmed-close: teardown reported success after an unknown post-close presence read"
|
|
1984
|
+
[ -e "$closed" ] \
|
|
1985
|
+
|| fail "herdr-projection-unconfirmed-close: regression did not exercise an attempted close"
|
|
1986
|
+
[ -e "$case_dir/state/task-x1.herdr-presentation" ] \
|
|
1987
|
+
|| fail "unconfirmed task-pane close incorrectly retired the presentation journal"
|
|
1988
|
+
[ -e "$case_dir/state/task-x1.meta" ] \
|
|
1989
|
+
|| fail "unconfirmed task-pane close erased the durable endpoint metadata"
|
|
1990
|
+
assert_grep "close could not be confirmed" "$case_dir/stderr" \
|
|
1991
|
+
"unconfirmed projected close did not explain why the journal was retained"
|
|
1992
|
+
assert_grep "not confirmed gone" "$case_dir/stderr" \
|
|
1993
|
+
"unconfirmed projected close did not explain why the records were retained"
|
|
1994
|
+
assert_not_contains "$(cat "$log")" "workspace close" \
|
|
1995
|
+
"unconfirmed projected close must not escalate to workspace cleanup"
|
|
1996
|
+
pass "herdr projection teardown retains every record when post-close presence is unknown"
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
test_herdr_projection_teardown_surfaces_restore_failure_without_blocking_cleanup() {
|
|
2000
|
+
local case_dir log closed restored
|
|
2001
|
+
case_dir=$(make_case herdr-projection-restore-failure)
|
|
2002
|
+
write_meta "$case_dir" local-only ship
|
|
2003
|
+
configure_herdr_projection_teardown_case "$case_dir"
|
|
2004
|
+
log="$case_dir/herdr.log"; closed="$case_dir/closed"; restored="$case_dir/restored"; : > "$log"
|
|
2005
|
+
|
|
2006
|
+
FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_CLOSED="$closed" FM_FAKE_HERDR_RESTORED="$restored" \
|
|
2007
|
+
FM_FAKE_HERDR_RESTORE_FAIL=1 \
|
|
2008
|
+
run_teardown "$case_dir" --force > "$case_dir/stdout" 2> "$case_dir/stderr" \
|
|
2009
|
+
|| fail "herdr-projection-restore-failure: a confirmed close with a failed focus restore blocked teardown"
|
|
2010
|
+
[ -e "$closed" ] \
|
|
2011
|
+
|| fail "herdr-projection-restore-failure: regression did not exercise the exact projected-pane close"
|
|
2012
|
+
[ ! -e "$case_dir/state/task-x1.herdr-presentation" ] \
|
|
2013
|
+
|| fail "herdr-projection-restore-failure: confirmed closure did not retire the presentation journal"
|
|
2014
|
+
assert_grep "exact-tab restoration failed" "$case_dir/stderr" \
|
|
2015
|
+
"herdr-projection-restore-failure: teardown swallowed the focus helper's restore warning"
|
|
2016
|
+
pass "herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure"
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
# --- Fix 1: conclude/abort the task's own parked no-mistakes run before the
|
|
2020
|
+
# worker is removed, and Fix 2: reap leaked descendant processes rooted under
|
|
2021
|
+
# the task's own worktree/tasktmp - both exercised through the real teardown
|
|
2022
|
+
# path (bin/fm-teardown.sh), never by matching its source text. ------------
|
|
2023
|
+
|
|
2024
|
+
# A parked-at-a-gate `axi status` TOON payload for <branch>/<head>, matching
|
|
2025
|
+
# the shape no-mistakes actually emits (see tests/fm-crew-state.test.sh's
|
|
2026
|
+
# run_parked fixture, the same shape bin/fm-crew-state.sh's own tests pin).
|
|
2027
|
+
parked_axi_status_toon() { # <branch> <head> [run-id]
|
|
2028
|
+
cat <<EOF
|
|
2029
|
+
run:
|
|
2030
|
+
id: "${3:-01RUN}"
|
|
2031
|
+
branch: $1
|
|
2032
|
+
status: awaiting_approval
|
|
2033
|
+
awaiting_agent: parked 2m10s
|
|
2034
|
+
head: "$2"
|
|
2035
|
+
pr: ""
|
|
2036
|
+
findings: none
|
|
2037
|
+
gate: review
|
|
2038
|
+
EOF
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
running_axi_status_toon() { # <branch> <head> [run-id]
|
|
2042
|
+
cat <<EOF
|
|
2043
|
+
run:
|
|
2044
|
+
id: "${3:-01RUN}"
|
|
2045
|
+
branch: $1
|
|
2046
|
+
status: running
|
|
2047
|
+
head: "$2"
|
|
2048
|
+
pr: ""
|
|
2049
|
+
steps[1]{step,status,findings,summary}:
|
|
2050
|
+
test,running,0,"agent under way"
|
|
2051
|
+
EOF
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
# Land a shippable commit on the task branch and push it to origin, the same
|
|
2055
|
+
# "definitely landed, teardown must ALLOW" shape test_no_mistakes_origin_remote_allows
|
|
2056
|
+
# uses, so these new cases exercise the abort/reap steps on a real successful
|
|
2057
|
+
# teardown rather than a refusal path.
|
|
2058
|
+
land_shippable_commit() {
|
|
2059
|
+
local case_dir=$1
|
|
2060
|
+
wt_commit "$case_dir" "shippable work"
|
|
2061
|
+
git -C "$case_dir/wt" push -q origin fm/task-x1
|
|
2062
|
+
git -C "$case_dir/project" fetch -q origin
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
test_parked_own_run_is_aborted_before_teardown() {
|
|
2066
|
+
local case_dir rc head
|
|
2067
|
+
case_dir=$(make_case parked-run-abort)
|
|
2068
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2069
|
+
land_shippable_commit "$case_dir"
|
|
2070
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2071
|
+
|
|
2072
|
+
local rc=0
|
|
2073
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head")" \
|
|
2074
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2075
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2076
|
+
|
|
2077
|
+
expect_code 0 "$rc" "parked-run-abort: teardown should still succeed"
|
|
2078
|
+
assert_present "$case_dir/nm-abort.log" \
|
|
2079
|
+
"parked-run-abort: no-mistakes axi abort was never invoked for the task's own parked run"
|
|
2080
|
+
assert_grep "abort --run 01RUN" "$case_dir/nm-abort.log" \
|
|
2081
|
+
"parked-run-abort: no-mistakes axi abort did not target the verified run id"
|
|
2082
|
+
assert_grep "parked at a gate; aborting" "$case_dir/stderr" \
|
|
2083
|
+
"parked-run-abort: teardown did not report aborting the parked run before removing the worker"
|
|
2084
|
+
pass "a task's own parked no-mistakes run is aborted, not orphaned, before the worker is removed"
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
test_mismatched_run_after_abort_refuses_unconfirmed() {
|
|
2088
|
+
local case_dir rc head
|
|
2089
|
+
case_dir=$(make_case parked-run-replaced)
|
|
2090
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2091
|
+
land_shippable_commit "$case_dir"
|
|
2092
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2093
|
+
|
|
2094
|
+
rc=0
|
|
2095
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head" 01RUN)" \
|
|
2096
|
+
FM_FAKE_AXI_STATUS_AFTER_ABORT="$(parked_axi_status_toon fm/task-x1 "$head" 02RUN)" \
|
|
2097
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2098
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2099
|
+
|
|
2100
|
+
expect_code 1 "$rc" "parked-run-replaced: a different run does not confirm the targeted abort"
|
|
2101
|
+
assert_grep "abort --run 01RUN" "$case_dir/nm-abort.log" \
|
|
2102
|
+
"parked-run-replaced: teardown did not abort only the verified run"
|
|
2103
|
+
assert_present "$case_dir/wt" "parked-run-replaced: teardown removed the worktree without confirmation"
|
|
2104
|
+
pass "a different run cannot confirm the targeted abort"
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
test_empty_status_after_abort_refuses_unconfirmed() {
|
|
2108
|
+
local case_dir rc head
|
|
2109
|
+
case_dir=$(make_case parked-run-empty-confirmation)
|
|
2110
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2111
|
+
land_shippable_commit "$case_dir"
|
|
2112
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2113
|
+
|
|
2114
|
+
rc=0
|
|
2115
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head")" \
|
|
2116
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2117
|
+
FM_FAKE_NM_EMPTY_AFTER_ABORT=1 \
|
|
2118
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2119
|
+
|
|
2120
|
+
expect_code 1 "$rc" "parked-run-empty-confirmation: empty status should refuse"
|
|
2121
|
+
assert_present "$case_dir/wt" "parked-run-empty-confirmation: teardown removed the worktree"
|
|
2122
|
+
pass "empty post-abort status is not accepted as confirmation"
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
test_not_found_status_after_abort_confirms_completion() {
|
|
2126
|
+
local case_dir rc head
|
|
2127
|
+
case_dir=$(make_case parked-run-not-found-confirmation)
|
|
2128
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2129
|
+
land_shippable_commit "$case_dir"
|
|
2130
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2131
|
+
|
|
2132
|
+
rc=0
|
|
2133
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head")" \
|
|
2134
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2135
|
+
FM_FAKE_NM_NOT_FOUND_AFTER_ABORT=1 \
|
|
2136
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2137
|
+
|
|
2138
|
+
expect_code 0 "$rc" "parked-run-not-found-confirmation: explicit not-found should confirm completion"
|
|
2139
|
+
pass "the CLI's exact run-not-found signal confirms completion"
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
test_parked_own_run_refuses_when_abort_is_unconfirmed() {
|
|
2143
|
+
local case_dir rc head pid
|
|
2144
|
+
case_dir=$(make_case parked-run-abort-unconfirmed)
|
|
2145
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2146
|
+
land_shippable_commit "$case_dir"
|
|
2147
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2148
|
+
( cd "$case_dir/wt" && exec sleep 300 ) &
|
|
2149
|
+
pid=$!
|
|
2150
|
+
disown
|
|
2151
|
+
|
|
2152
|
+
cat > "$case_dir/fakebin/treehouse" <<EOF
|
|
2153
|
+
#!/usr/bin/env bash
|
|
2154
|
+
printf 'return\n' >> "$case_dir/treehouse.log"
|
|
2155
|
+
EOF
|
|
2156
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
2157
|
+
|
|
2158
|
+
rc=0
|
|
2159
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head")" \
|
|
2160
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2161
|
+
FM_FAKE_NM_ABORT_NOOP=1 \
|
|
2162
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2163
|
+
|
|
2164
|
+
expect_code 1 "$rc" "parked-run-abort-unconfirmed: teardown should refuse"
|
|
2165
|
+
assert_grep "REFUSED: no-mistakes run for task-x1 is still parked after axi abort" "$case_dir/stderr" \
|
|
2166
|
+
"parked-run-abort-unconfirmed: teardown did not explain the parked-run refusal"
|
|
2167
|
+
assert_present "$case_dir/wt" \
|
|
2168
|
+
"parked-run-abort-unconfirmed: teardown removed the worktree after refusing"
|
|
2169
|
+
assert_present "$case_dir/state/task-x1.meta" \
|
|
2170
|
+
"parked-run-abort-unconfirmed: teardown removed task metadata after refusing"
|
|
2171
|
+
assert_absent "$case_dir/treehouse.log" \
|
|
2172
|
+
"parked-run-abort-unconfirmed: teardown returned the worktree after refusing"
|
|
2173
|
+
kill -0 "$pid" 2>/dev/null || fail "parked-run-abort-unconfirmed: process reap ran before refusal"
|
|
2174
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2175
|
+
pass "teardown refuses before reap or removal when a task-owned run remains parked"
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
test_another_branchs_parked_run_is_never_touched() {
|
|
2179
|
+
local case_dir rc
|
|
2180
|
+
case_dir=$(make_case parked-run-not-ours)
|
|
2181
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2182
|
+
land_shippable_commit "$case_dir"
|
|
2183
|
+
|
|
2184
|
+
local rc=0
|
|
2185
|
+
# A parked run reported for a DIFFERENT branch - e.g. another crew's task
|
|
2186
|
+
# still validating on the shared gate - must never be aborted by this task's
|
|
2187
|
+
# teardown.
|
|
2188
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/some-other-task deadbeef)" \
|
|
2189
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2190
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2191
|
+
|
|
2192
|
+
expect_code 0 "$rc" "parked-run-not-ours: teardown should still succeed"
|
|
2193
|
+
assert_absent "$case_dir/nm-abort.log" \
|
|
2194
|
+
"parked-run-not-ours: teardown called axi abort for a run on another branch"
|
|
2195
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "aborting" \
|
|
2196
|
+
"parked-run-not-ours: teardown reported aborting a run it does not own"
|
|
2197
|
+
pass "a parked run on another branch is never aborted by this task's teardown (ownership is precise)"
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
test_own_autonomous_run_is_left_alone() {
|
|
2201
|
+
local case_dir rc head
|
|
2202
|
+
case_dir=$(make_case autonomous-run-left-alone)
|
|
2203
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2204
|
+
land_shippable_commit "$case_dir"
|
|
2205
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2206
|
+
|
|
2207
|
+
rc=0
|
|
2208
|
+
FM_FAKE_AXI_STATUS="$(running_axi_status_toon fm/task-x1 "$head")" \
|
|
2209
|
+
FM_FAKE_NM_ABORT_LOG="$case_dir/nm-abort.log" \
|
|
2210
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2211
|
+
|
|
2212
|
+
expect_code 0 "$rc" "autonomous-run-left-alone: teardown should still succeed"
|
|
2213
|
+
assert_absent "$case_dir/nm-abort.log" \
|
|
2214
|
+
"autonomous-run-left-alone: teardown aborted a task-owned autonomous run"
|
|
2215
|
+
assert_not_contains "$(cat "$case_dir/stderr")" "aborting" \
|
|
2216
|
+
"autonomous-run-left-alone: teardown reported aborting an autonomous run"
|
|
2217
|
+
pass "a task-owned autonomous running step is left alone rather than aborted"
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
test_leaked_worktree_process_is_reaped() {
|
|
2221
|
+
local case_dir rc pid
|
|
2222
|
+
case_dir=$(make_case leaked-process-reap)
|
|
2223
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2224
|
+
land_shippable_commit "$case_dir"
|
|
2225
|
+
|
|
2226
|
+
# A backgrounded, disowned process rooted (by cwd) under the task's own
|
|
2227
|
+
# worktree - the same shape the observed incident's leaked `go test`
|
|
2228
|
+
# binaries took (reparented to init, no live task meta to attribute them
|
|
2229
|
+
# to once an unpatched teardown had already run).
|
|
2230
|
+
( cd "$case_dir/wt" && exec sleep 300 ) &
|
|
2231
|
+
pid=$!
|
|
2232
|
+
disown
|
|
2233
|
+
sleep 0.3
|
|
2234
|
+
kill -0 "$pid" 2>/dev/null || fail "leaked-process-reap: setup sleeper did not start"
|
|
2235
|
+
|
|
2236
|
+
rc=0
|
|
2237
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2238
|
+
|
|
2239
|
+
expect_code 0 "$rc" "leaked-process-reap: teardown should still succeed"
|
|
2240
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
2241
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2242
|
+
fail "leaked-process-reap: leaked worktree process survived teardown"
|
|
2243
|
+
fi
|
|
2244
|
+
assert_grep "reaping leaked worktree process" "$case_dir/stderr" \
|
|
2245
|
+
"leaked-process-reap: teardown did not report reaping the leaked process"
|
|
2246
|
+
pass "a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving"
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
test_leaked_tasktmp_process_is_reaped() {
|
|
2250
|
+
local case_dir rc pid
|
|
2251
|
+
case_dir=$(make_case leaked-tasktmp-reap)
|
|
2252
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2253
|
+
printf '%s\n' "tasktmp=$case_dir/tasktmp" >> "$case_dir/state/task-x1.meta"
|
|
2254
|
+
mkdir -p "$case_dir/tasktmp"
|
|
2255
|
+
land_shippable_commit "$case_dir"
|
|
2256
|
+
|
|
2257
|
+
( cd "$case_dir/tasktmp" && exec sleep 300 ) &
|
|
2258
|
+
pid=$!
|
|
2259
|
+
disown
|
|
2260
|
+
sleep 0.3
|
|
2261
|
+
kill -0 "$pid" 2>/dev/null || fail "leaked-tasktmp-reap: setup sleeper did not start"
|
|
2262
|
+
|
|
2263
|
+
rc=0
|
|
2264
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2265
|
+
|
|
2266
|
+
expect_code 0 "$rc" "leaked-tasktmp-reap: teardown should still succeed"
|
|
2267
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
2268
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2269
|
+
fail "leaked-tasktmp-reap: leaked tasktmp process survived teardown"
|
|
2270
|
+
fi
|
|
2271
|
+
assert_grep "reaping leaked worktree process" "$case_dir/stderr" \
|
|
2272
|
+
"leaked-tasktmp-reap: teardown did not report reaping the leaked tasktmp process"
|
|
2273
|
+
pass "a leaked descendant process rooted under the task's per-task tasktmp is reaped by teardown too"
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
test_lsof_absent_reaps_tmux_process_group() {
|
|
2277
|
+
local case_dir rc pid path_without_lsof
|
|
2278
|
+
case_dir=$(make_case lsof-absent-process-group-reap)
|
|
2279
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2280
|
+
land_shippable_commit "$case_dir"
|
|
2281
|
+
path_without_lsof=$(make_path_without_lsof "$case_dir")
|
|
2282
|
+
PATH="$path_without_lsof" command -v lsof >/dev/null 2>&1 \
|
|
2283
|
+
&& fail "lsof-absent-process-group-reap: fixture path unexpectedly exposes lsof"
|
|
2284
|
+
|
|
2285
|
+
perl -e 'setpgrp(0, 0); chdir shift or die; exec "sleep", "300"' "$case_dir/wt" &
|
|
2286
|
+
pid=$!
|
|
2287
|
+
disown
|
|
2288
|
+
sleep 0.3
|
|
2289
|
+
kill -0 "$pid" 2>/dev/null || fail "lsof-absent-process-group-reap: setup sleeper did not start"
|
|
2290
|
+
cat > "$case_dir/fakebin/tmux" <<EOF
|
|
2291
|
+
#!/usr/bin/env bash
|
|
2292
|
+
if [ "\${1:-}" = display-message ] && [ "\${*: -1}" = '#{pane_pid}' ]; then
|
|
2293
|
+
printf '%s\n' '$pid'
|
|
2294
|
+
fi
|
|
2295
|
+
exit 0
|
|
2296
|
+
EOF
|
|
2297
|
+
chmod +x "$case_dir/fakebin/tmux"
|
|
2298
|
+
|
|
2299
|
+
rc=0
|
|
2300
|
+
FM_TEARDOWN_TEST_PATH="$path_without_lsof" \
|
|
2301
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2302
|
+
|
|
2303
|
+
expect_code 0 "$rc" "lsof-absent-process-group-reap: teardown should succeed"
|
|
2304
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
2305
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2306
|
+
fail "lsof-absent-process-group-reap: tmux process group survived teardown"
|
|
2307
|
+
fi
|
|
2308
|
+
assert_grep "reaping leaked worktree process group" "$case_dir/stderr" \
|
|
2309
|
+
"lsof-absent-process-group-reap: teardown did not use the process-group fallback"
|
|
2310
|
+
pass "missing lsof falls back to reaping the tmux pane process group"
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
test_lsof_error_refuses_before_removal() {
|
|
2314
|
+
local case_dir rc
|
|
2315
|
+
case_dir=$(make_case lsof-error-refusal)
|
|
2316
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2317
|
+
land_shippable_commit "$case_dir"
|
|
2318
|
+
cat > "$case_dir/fakebin/lsof" <<'SH'
|
|
2319
|
+
#!/usr/bin/env bash
|
|
2320
|
+
exit 1
|
|
2321
|
+
SH
|
|
2322
|
+
cat > "$case_dir/fakebin/treehouse" <<EOF
|
|
2323
|
+
#!/usr/bin/env bash
|
|
2324
|
+
printf 'return\n' >> "$case_dir/treehouse.log"
|
|
2325
|
+
EOF
|
|
2326
|
+
chmod +x "$case_dir/fakebin/lsof" "$case_dir/fakebin/treehouse"
|
|
2327
|
+
|
|
2328
|
+
rc=0
|
|
2329
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2330
|
+
|
|
2331
|
+
expect_code 1 "$rc" "lsof-error-refusal: teardown should refuse"
|
|
2332
|
+
assert_grep "REFUSED: cannot determine leaked processes under $case_dir/wt for task-x1 (lsof failed)" "$case_dir/stderr" \
|
|
2333
|
+
"lsof-error-refusal: teardown did not explain the lsof refusal"
|
|
2334
|
+
assert_present "$case_dir/wt" "lsof-error-refusal: teardown removed the worktree"
|
|
2335
|
+
assert_present "$case_dir/state/task-x1.meta" "lsof-error-refusal: teardown removed task metadata"
|
|
2336
|
+
assert_absent "$case_dir/treehouse.log" "lsof-error-refusal: teardown returned the worktree"
|
|
2337
|
+
pass "an erroring lsof scan refuses teardown and preserves the task"
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
test_reused_pid_identity_is_not_force_killed() {
|
|
2341
|
+
local case_dir rc pid
|
|
2342
|
+
case_dir=$(make_case reused-pid-identity)
|
|
2343
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2344
|
+
land_shippable_commit "$case_dir"
|
|
2345
|
+
|
|
2346
|
+
perl -e '$SIG{TERM} = "IGNORE"; sleep 300' &
|
|
2347
|
+
pid=$!
|
|
2348
|
+
disown
|
|
2349
|
+
sleep 0.2
|
|
2350
|
+
cat > "$case_dir/fakebin/lsof" <<EOF
|
|
2351
|
+
#!/usr/bin/env bash
|
|
2352
|
+
count=0
|
|
2353
|
+
[ ! -f '$case_dir/lsof-count' ] || count=\$(cat '$case_dir/lsof-count')
|
|
2354
|
+
count=\$((count + 1))
|
|
2355
|
+
printf '%s\n' "\$count" > '$case_dir/lsof-count'
|
|
2356
|
+
if [ "\$count" -le 3 ]; then printf 'p%s\nfcwd\nn%s\n' '$pid' '$case_dir/wt'; fi
|
|
2357
|
+
EOF
|
|
2358
|
+
cat > "$case_dir/fakebin/ps" <<'SH'
|
|
2359
|
+
#!/usr/bin/env bash
|
|
2360
|
+
if [ "${1:-}" = -p ] && [ "${2:-}" = "${FM_FAKE_REUSED_PID:-}" ] \
|
|
2361
|
+
&& [ "${3:-}" = -o ] && [ "${4:-}" = lstart= ]; then
|
|
2362
|
+
count=0
|
|
2363
|
+
[ ! -f "$FM_FAKE_PS_COUNT" ] || count=$(cat "$FM_FAKE_PS_COUNT")
|
|
2364
|
+
count=$((count + 1))
|
|
2365
|
+
printf '%s\n' "$count" > "$FM_FAKE_PS_COUNT"
|
|
2366
|
+
if [ "$count" -le 2 ]; then printf 'Tue Aug 4 10:00:00 2026\n'
|
|
2367
|
+
else printf 'Tue Aug 4 10:00:01 2026\n'; fi
|
|
2368
|
+
exit 0
|
|
2369
|
+
fi
|
|
2370
|
+
exec "$REAL_PS_FOR_TEST" "$@"
|
|
2371
|
+
SH
|
|
2372
|
+
chmod +x "$case_dir/fakebin/lsof" "$case_dir/fakebin/ps"
|
|
2373
|
+
|
|
2374
|
+
rc=0
|
|
2375
|
+
FM_PROC_ROOT_OVERRIDE="$case_dir/no-proc" \
|
|
2376
|
+
FM_FAKE_REUSED_PID="$pid" FM_FAKE_PS_COUNT="$case_dir/ps-count" \
|
|
2377
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2378
|
+
|
|
2379
|
+
expect_code 0 "$rc" "reused-pid-identity: teardown should skip the replacement process"
|
|
2380
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
2381
|
+
fail "reused-pid-identity: teardown force-killed a process whose start time changed"
|
|
2382
|
+
fi
|
|
2383
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2384
|
+
pass "a reused pid with a different start time is never force-killed"
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
test_exec_changed_process_is_still_reaped() {
|
|
2388
|
+
local case_dir rc pid marker done_flag survived=0
|
|
2389
|
+
case_dir=$(make_case exec-changed-process)
|
|
2390
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2391
|
+
land_shippable_commit "$case_dir"
|
|
2392
|
+
marker="$case_dir/exec-now"
|
|
2393
|
+
done_flag="$case_dir/exec-done"
|
|
2394
|
+
|
|
2395
|
+
( cd "$case_dir/wt" && exec perl -e '
|
|
2396
|
+
my ($marker, $done) = @ARGV;
|
|
2397
|
+
until (-e $marker) { select undef, undef, undef, 0.01; }
|
|
2398
|
+
open my $fh, ">", $done or die "open";
|
|
2399
|
+
close $fh;
|
|
2400
|
+
exec "perl", "-e", '\''$SIG{TERM} = "IGNORE"; sleep 300'\'';
|
|
2401
|
+
' "$marker" "$done_flag" ) &
|
|
2402
|
+
pid=$!
|
|
2403
|
+
disown
|
|
2404
|
+
sleep 0.2
|
|
2405
|
+
cat > "$case_dir/fakebin/ps" <<'SH'
|
|
2406
|
+
#!/usr/bin/env bash
|
|
2407
|
+
if [ "${1:-}" = -p ] && [ "${2:-}" = "${FM_FAKE_EXEC_PID:-}" ] \
|
|
2408
|
+
&& [ "${3:-}" = -o ] && [ "${4:-}" = lstart= ]; then
|
|
2409
|
+
out=$("$REAL_PS_FOR_TEST" "$@") || exit $?
|
|
2410
|
+
[ -e "$FM_FAKE_EXEC_MARKER" ] || : > "$FM_FAKE_EXEC_MARKER"
|
|
2411
|
+
printf '%s\n' "$out"
|
|
2412
|
+
exit 0
|
|
2413
|
+
fi
|
|
2414
|
+
exec "$REAL_PS_FOR_TEST" "$@"
|
|
2415
|
+
SH
|
|
2416
|
+
cat > "$case_dir/fakebin/lsof" <<'SH'
|
|
2417
|
+
#!/usr/bin/env bash
|
|
2418
|
+
count=0
|
|
2419
|
+
[ ! -f "$FM_FAKE_LSOF_COUNT" ] || count=$(cat "$FM_FAKE_LSOF_COUNT")
|
|
2420
|
+
count=$((count + 1))
|
|
2421
|
+
printf '%s\n' "$count" > "$FM_FAKE_LSOF_COUNT"
|
|
2422
|
+
if [ "$count" -eq 2 ]; then
|
|
2423
|
+
i=0
|
|
2424
|
+
while [ "$i" -lt 100 ]; do
|
|
2425
|
+
[ ! -e "$FM_FAKE_EXEC_DONE" ] || break
|
|
2426
|
+
sleep 0.01
|
|
2427
|
+
i=$((i + 1))
|
|
2428
|
+
done
|
|
2429
|
+
fi
|
|
2430
|
+
exec "$REAL_LSOF_FOR_TEST" "$@"
|
|
2431
|
+
SH
|
|
2432
|
+
chmod +x "$case_dir/fakebin/ps" "$case_dir/fakebin/lsof"
|
|
2433
|
+
|
|
2434
|
+
rc=0
|
|
2435
|
+
FM_PROC_ROOT_OVERRIDE="$case_dir/no-proc" \
|
|
2436
|
+
FM_FAKE_EXEC_PID="$pid" FM_FAKE_EXEC_MARKER="$marker" \
|
|
2437
|
+
FM_FAKE_EXEC_DONE="$done_flag" FM_FAKE_LSOF_COUNT="$case_dir/lsof-count" \
|
|
2438
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2439
|
+
|
|
2440
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
2441
|
+
survived=1
|
|
2442
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2443
|
+
fi
|
|
2444
|
+
expect_code 0 "$rc" "exec-changed-process: teardown should succeed"
|
|
2445
|
+
[ "$survived" -eq 0 ] || fail "exec-changed-process: exec-changed leaked process survived teardown"
|
|
2446
|
+
pass "an exec change preserves birth identity and the process is reaped"
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
test_process_spawned_during_grace_is_reaped_on_later_pass() {
|
|
2450
|
+
local case_dir rc pid child_file child_pid="" parent_survived=0 child_survived=0
|
|
2451
|
+
case_dir=$(make_case grace-spawn-convergence)
|
|
2452
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2453
|
+
land_shippable_commit "$case_dir"
|
|
2454
|
+
child_file="$case_dir/child.pid"
|
|
2455
|
+
|
|
2456
|
+
( cd "$case_dir/wt" && exec perl -e '
|
|
2457
|
+
my $file = shift;
|
|
2458
|
+
$SIG{TERM} = sub {
|
|
2459
|
+
my $child = fork();
|
|
2460
|
+
die "fork" unless defined $child;
|
|
2461
|
+
if (!$child) { exec "sleep", "300"; }
|
|
2462
|
+
open my $fh, ">", $file or die "open";
|
|
2463
|
+
print {$fh} "$child\n";
|
|
2464
|
+
close $fh;
|
|
2465
|
+
exit 0;
|
|
2466
|
+
};
|
|
2467
|
+
sleep 300;
|
|
2468
|
+
' "$child_file" ) &
|
|
2469
|
+
pid=$!
|
|
2470
|
+
disown
|
|
2471
|
+
sleep 0.2
|
|
2472
|
+
|
|
2473
|
+
rc=0
|
|
2474
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2475
|
+
|
|
2476
|
+
if [ -f "$child_file" ]; then child_pid=$(cat "$child_file"); fi
|
|
2477
|
+
if [ -n "$child_pid" ] && kill -0 "$child_pid" 2>/dev/null; then
|
|
2478
|
+
child_survived=1
|
|
2479
|
+
kill -KILL "$child_pid" 2>/dev/null || true
|
|
2480
|
+
fi
|
|
2481
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
2482
|
+
parent_survived=1
|
|
2483
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
2484
|
+
fi
|
|
2485
|
+
expect_code 0 "$rc" "grace-spawn-convergence: teardown should converge"
|
|
2486
|
+
assert_present "$child_file" "grace-spawn-convergence: TERM handler did not spawn a child"
|
|
2487
|
+
[ "$child_survived" -eq 0 ] || fail "grace-spawn-convergence: spawned child survived"
|
|
2488
|
+
[ "$parent_survived" -eq 0 ] || fail "grace-spawn-convergence: original process survived"
|
|
2489
|
+
pass "a process spawned during grace is reaped on a later pass"
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
test_persistent_scan_refuses_after_bounded_retries() {
|
|
2493
|
+
local case_dir rc wt_path fake_pid=99999999
|
|
2494
|
+
case_dir=$(make_case persistent-reap-refusal)
|
|
2495
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2496
|
+
land_shippable_commit "$case_dir"
|
|
2497
|
+
wt_path=$(cd "$case_dir/wt" && pwd -P)
|
|
2498
|
+
cat > "$case_dir/fakebin/lsof" <<EOF
|
|
2499
|
+
#!/usr/bin/env bash
|
|
2500
|
+
printf 'p%s\nfcwd\nn%s\n' '$fake_pid' '$wt_path'
|
|
2501
|
+
EOF
|
|
2502
|
+
cat > "$case_dir/fakebin/ps" <<'SH'
|
|
2503
|
+
#!/usr/bin/env bash
|
|
2504
|
+
if [ "${1:-}" = -p ] && [ "${2:-}" = "${FM_FAKE_PERSISTENT_PID:-}" ] \
|
|
2505
|
+
&& [ "${3:-}" = -o ] && [ "${4:-}" = lstart= ]; then
|
|
2506
|
+
printf 'Tue Aug 4 10:00:00 2026\n'
|
|
2507
|
+
exit 0
|
|
2508
|
+
fi
|
|
2509
|
+
exec "$REAL_PS_FOR_TEST" "$@"
|
|
2510
|
+
SH
|
|
2511
|
+
chmod +x "$case_dir/fakebin/lsof" "$case_dir/fakebin/ps"
|
|
2512
|
+
|
|
2513
|
+
rc=0
|
|
2514
|
+
FM_PROC_ROOT_OVERRIDE="$case_dir/no-proc" FM_FAKE_PERSISTENT_PID="$fake_pid" \
|
|
2515
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2516
|
+
|
|
2517
|
+
expect_code 1 "$rc" "persistent-reap-refusal: teardown should refuse"
|
|
2518
|
+
assert_grep "remain after 3 reap attempts" "$case_dir/stderr" \
|
|
2519
|
+
"persistent-reap-refusal: teardown did not report bounded non-convergence"
|
|
2520
|
+
assert_present "$case_dir/wt" "persistent-reap-refusal: teardown removed the worktree"
|
|
2521
|
+
assert_present "$case_dir/state/task-x1.meta" "persistent-reap-refusal: teardown removed task metadata"
|
|
2522
|
+
pass "persistent leaked processes refuse teardown after bounded retries"
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
test_process_exit_during_identity_lookup_does_not_refuse() {
|
|
2526
|
+
local case_dir rc wt_path fake_pid=99999998
|
|
2527
|
+
case_dir=$(make_case identity-exit-convergence)
|
|
2528
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2529
|
+
land_shippable_commit "$case_dir"
|
|
2530
|
+
wt_path=$(cd "$case_dir/wt" && pwd -P)
|
|
2531
|
+
cat > "$case_dir/fakebin/lsof" <<EOF
|
|
2532
|
+
#!/usr/bin/env bash
|
|
2533
|
+
count=0
|
|
2534
|
+
[ ! -f "$case_dir/lsof-count" ] || count=\$(cat "$case_dir/lsof-count")
|
|
2535
|
+
count=\$((count + 1))
|
|
2536
|
+
printf '%s\n' "\$count" > "$case_dir/lsof-count"
|
|
2537
|
+
if [ "\$count" -eq 1 ]; then
|
|
2538
|
+
printf 'p%s\nfcwd\nn%s\n' '$fake_pid' '$wt_path'
|
|
2539
|
+
fi
|
|
2540
|
+
EOF
|
|
2541
|
+
cat > "$case_dir/fakebin/ps" <<'SH'
|
|
2542
|
+
#!/usr/bin/env bash
|
|
2543
|
+
if [ "${1:-}" = -p ] && [ "${2:-}" = "${FM_FAKE_EXITED_PID:-}" ]; then
|
|
2544
|
+
exit 1
|
|
2545
|
+
fi
|
|
2546
|
+
exec "$REAL_PS_FOR_TEST" "$@"
|
|
2547
|
+
SH
|
|
2548
|
+
cat > "$case_dir/fakebin/treehouse" <<EOF
|
|
2549
|
+
#!/usr/bin/env bash
|
|
2550
|
+
printf 'returned\n' > "$case_dir/treehouse.log"
|
|
2551
|
+
EOF
|
|
2552
|
+
chmod +x "$case_dir/fakebin/lsof" "$case_dir/fakebin/ps" "$case_dir/fakebin/treehouse"
|
|
2553
|
+
|
|
2554
|
+
rc=0
|
|
2555
|
+
FM_PROC_ROOT_OVERRIDE="$case_dir/no-proc" FM_FAKE_EXITED_PID="$fake_pid" \
|
|
2556
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2557
|
+
|
|
2558
|
+
expect_code 0 "$rc" "identity-exit-convergence: teardown should succeed"
|
|
2559
|
+
assert_present "$case_dir/treehouse.log" \
|
|
2560
|
+
"identity-exit-convergence: teardown did not reach worktree return"
|
|
2561
|
+
! grep -q REFUSED "$case_dir/stderr" || \
|
|
2562
|
+
fail "identity-exit-convergence: a disappeared process caused teardown refusal"
|
|
2563
|
+
pass "a process exiting during identity lookup does not block teardown"
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
test_run_abort_precedes_process_reap_precedes_worktree_removal() {
|
|
2567
|
+
local case_dir rc head pid abort_log
|
|
2568
|
+
case_dir=$(make_case abort-then-reap-then-remove-order)
|
|
2569
|
+
write_meta "$case_dir" no-mistakes ship
|
|
2570
|
+
land_shippable_commit "$case_dir"
|
|
2571
|
+
head=$(git -C "$case_dir/wt" rev-parse HEAD)
|
|
2572
|
+
abort_log="$case_dir/nm-abort.log"
|
|
2573
|
+
|
|
2574
|
+
( cd "$case_dir/wt" && exec sleep 300 ) &
|
|
2575
|
+
pid=$!
|
|
2576
|
+
disown
|
|
2577
|
+
sleep 0.3
|
|
2578
|
+
kill -0 "$pid" 2>/dev/null || fail "abort-then-reap-then-remove-order: setup sleeper did not start"
|
|
2579
|
+
|
|
2580
|
+
# A treehouse fake that snapshots, at the exact moment the destructive
|
|
2581
|
+
# worktree return runs, whether the run was already aborted and whether the
|
|
2582
|
+
# leaked process was already reaped - direct causal proof of ordering from
|
|
2583
|
+
# real observed state, not a source-text or line-number correlation.
|
|
2584
|
+
cat > "$case_dir/fakebin/treehouse" <<EOF
|
|
2585
|
+
#!/usr/bin/env bash
|
|
2586
|
+
if [ -s "$abort_log" ]; then echo "abort-already-happened" >> "$case_dir/order.log"; fi
|
|
2587
|
+
if ! kill -0 $pid 2>/dev/null; then echo "reap-already-happened" >> "$case_dir/order.log"; fi
|
|
2588
|
+
exit 0
|
|
2589
|
+
EOF
|
|
2590
|
+
chmod +x "$case_dir/fakebin/treehouse"
|
|
2591
|
+
|
|
2592
|
+
rc=0
|
|
2593
|
+
FM_FAKE_AXI_STATUS="$(parked_axi_status_toon fm/task-x1 "$head")" \
|
|
2594
|
+
FM_FAKE_NM_ABORT_LOG="$abort_log" \
|
|
2595
|
+
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr" || rc=$?
|
|
2596
|
+
expect_code 0 "$rc" "abort-then-reap-then-remove-order: teardown should still succeed"
|
|
2597
|
+
kill -0 "$pid" 2>/dev/null && { kill -KILL "$pid" 2>/dev/null || true; }
|
|
2598
|
+
|
|
2599
|
+
assert_present "$case_dir/order.log" \
|
|
2600
|
+
"abort-then-reap-then-remove-order: the destructive worktree return was never invoked"
|
|
2601
|
+
assert_grep "abort-already-happened" "$case_dir/order.log" \
|
|
2602
|
+
"abort-then-reap-then-remove-order: the run was not yet aborted when the worktree return ran"
|
|
2603
|
+
assert_grep "reap-already-happened" "$case_dir/order.log" \
|
|
2604
|
+
"abort-then-reap-then-remove-order: the leaked process was not yet reaped when the worktree return ran"
|
|
2605
|
+
pass "the run abort and the leaked-process reap both complete before the destructive worktree return"
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
test_local_only_fork_remote_allows
|
|
2609
|
+
test_teardown_closes_the_backlog_item_itself
|
|
2610
|
+
test_teardown_manual_backend_leaves_the_backlog_to_the_operator
|
|
2611
|
+
test_local_only_truly_unpushed_refuses
|
|
2612
|
+
test_local_only_merged_to_local_main_allows
|
|
2613
|
+
test_no_mistakes_origin_remote_allows
|
|
2614
|
+
test_no_mistakes_truly_unpushed_refuses
|
|
2615
|
+
test_local_only_force_overrides_unpushed
|
|
2616
|
+
test_teardown_missing_busy_sidecar_completes
|
|
2617
|
+
test_herdr_teardown_clears_escalation_marker
|
|
2618
|
+
test_herdr_flat_teardown_refuses_orphaning_records_then_retry_completes
|
|
2619
|
+
test_herdr_flat_teardown_refuses_records_on_unparseable_presence
|
|
2620
|
+
test_herdr_flat_teardown_preflight_refuses_before_changes
|
|
2621
|
+
test_forced_secondmate_herdr_child_preflight_refuses_before_changes
|
|
2622
|
+
test_forced_secondmate_teardown_holds_descendant_lifecycle_locks
|
|
2623
|
+
test_forced_secondmate_herdr_child_retains_records_when_close_unconfirmed
|
|
2624
|
+
test_forced_teardown_retains_nested_secondmate_home_when_grandchild_close_unconfirmed
|
|
2625
|
+
test_herdr_projection_teardown_retires_journal_only_after_confirmed_close
|
|
2626
|
+
test_herdr_projection_teardown_retains_journal_when_close_unconfirmed
|
|
2627
|
+
test_herdr_projection_teardown_surfaces_restore_failure_without_blocking_cleanup
|
|
2628
|
+
test_squash_merged_branch_deleted_allows
|
|
2629
|
+
test_squash_merged_pr_allows_when_head_ancestor_of_pr_head
|
|
2630
|
+
test_no_pr_recorded_discovers_merged_pr_by_branch_allows
|
|
2631
|
+
test_squash_merged_pr_allows_replayed_unpushed_patch
|
|
2632
|
+
test_merged_pr_with_later_local_commit_refuses
|
|
2633
|
+
test_pr_check_does_not_refresh_stale_pr_head
|
|
2634
|
+
test_pr_check_records_remote_head_when_local_lags
|
|
2635
|
+
test_content_in_default_fallback_allows
|
|
2636
|
+
test_content_fallback_refreshes_stale_origin_ref
|
|
2637
|
+
test_dirty_worktree_refuses
|
|
2638
|
+
test_gh_error_and_content_absent_refuses
|
|
2639
|
+
test_stale_index_lock_cleared_and_teardown_succeeds
|
|
2640
|
+
test_live_index_lock_is_never_removed_and_teardown_refuses
|
|
2641
|
+
test_lsof_error_never_clears_index_lock
|
|
2642
|
+
test_stale_index_lock_cleanup_rechecks_dirty_worktree
|
|
2643
|
+
test_non_linked_index_lock_path_is_checked_from_worktree
|
|
2644
|
+
test_index_lock_mtime_read_failure_refuses
|
|
2645
|
+
test_transient_index_lock_clears_after_first_attempt_and_retry_succeeds
|
|
2646
|
+
test_persistent_index_lock_exhausts_retries_and_refuses_loudly
|
|
2647
|
+
test_empty_retry_wait_uses_default_without_aborting
|
|
2648
|
+
test_fractional_legacy_retry_wait_refuses_without_arithmetic_error
|
|
2649
|
+
test_parked_own_run_is_aborted_before_teardown
|
|
2650
|
+
test_parked_own_run_refuses_when_abort_is_unconfirmed
|
|
2651
|
+
test_mismatched_run_after_abort_refuses_unconfirmed
|
|
2652
|
+
test_empty_status_after_abort_refuses_unconfirmed
|
|
2653
|
+
test_not_found_status_after_abort_confirms_completion
|
|
2654
|
+
test_another_branchs_parked_run_is_never_touched
|
|
2655
|
+
test_own_autonomous_run_is_left_alone
|
|
2656
|
+
test_leaked_worktree_process_is_reaped
|
|
2657
|
+
test_leaked_tasktmp_process_is_reaped
|
|
2658
|
+
test_lsof_absent_reaps_tmux_process_group
|
|
2659
|
+
test_lsof_error_refuses_before_removal
|
|
2660
|
+
test_reused_pid_identity_is_not_force_killed
|
|
2661
|
+
test_exec_changed_process_is_still_reaped
|
|
2662
|
+
test_process_spawned_during_grace_is_reaped_on_later_pass
|
|
2663
|
+
test_persistent_scan_refuses_after_bounded_retries
|
|
2664
|
+
test_process_exit_during_identity_lookup_does_not_refuse
|
|
2665
|
+
test_run_abort_precedes_process_reap_precedes_worktree_removal
|