@zerwiz/ymir 0.1.4 → 0.1.7
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/README.md +74 -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 +360 -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.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +11 -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/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/runes_audit.md +5 -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 +121 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -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 +159 -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 +736 -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 +539 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -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 +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -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 +101 -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 +180 -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 +503 -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 +90 -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 +57 -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 +35 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/install.md +69 -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 +498 -0
- package/CHANGELOG.md +1913 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +114 -60
- 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 +97 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +108 -0
- package/RULES/07-config.md +101 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +22 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +133 -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 +256 -0
- package/bin/apodex-smoke-test.sh +105 -0
- package/bin/bifrost-bridge.sh +131 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +112 -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 +51 -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-assemble.sh +154 -0
- package/bin/changelog-guard.sh +197 -0
- package/bin/crash-sense.sh +173 -0
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +153 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +225 -0
- package/bin/eindri-acclaim.sh +47 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +54 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +98 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +158 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +81 -0
- package/bin/github-deploy.sh +94 -0
- package/bin/gjallarhorn-expose.sh +136 -0
- package/bin/gjallarhorn-notify.sh +71 -0
- package/bin/gjallarhorn-purge.sh +67 -0
- package/bin/gjallarhorn-tunnel.sh +85 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +130 -0
- package/bin/hall-snapshot.sh +111 -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 +452 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +105 -0
- package/bin/hodd.sh +102 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +121 -0
- package/bin/local-model-lock.sh +70 -0
- package/bin/mimir-bridge.py +259 -0
- package/bin/mimir-bridge.sh +100 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +72 -0
- package/bin/mjollnir.sh +104 -0
- package/bin/model-hardware.sh +96 -0
- package/bin/model-resolve.sh +160 -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-hall-snapshot.sh +28 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +161 -0
- package/bin/omarchy-plugins.sh +187 -0
- package/bin/omarchy-sense.sh +162 -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-model-agnostic.sh +71 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +58 -0
- package/bin/project-git.sh +69 -0
- package/bin/public-guard.sh +71 -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 +104 -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 +94 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +89 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +203 -0
- package/bin/telegram-bot.sh +82 -0
- package/bin/toolchain.sh +74 -0
- package/bin/ui-truth-check.sh +75 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +298 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +60 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +163 -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 +806 -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-plan.sh +356 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +110 -0
- package/bin/ymir-setup-auth.sh +119 -0
- package/bin/ymir-validate.sh +193 -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 +1060 -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 +54 -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 +269 -0
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +217 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for bin/fm-pr-merge.sh: the one path firstmate uses to merge a task's
|
|
3
|
+
# PR, which must record pr= and any available pr_head= into the task's meta so
|
|
4
|
+
# fm-teardown.sh's landed-check has a PR reference to verify against, even on
|
|
5
|
+
# repos with no PR CI where the usual "checks green" fm-pr-check.sh trigger
|
|
6
|
+
# never fires.
|
|
7
|
+
#
|
|
8
|
+
# Matrix:
|
|
9
|
+
# (a) a verified merge records pr= and pr_head=
|
|
10
|
+
# (b) merge is refused when gh-axi pr merge itself fails (no silent success)
|
|
11
|
+
# (c) extra gh-axi pr merge args are forwarded after number and --repo
|
|
12
|
+
# (d) merge is refused before gh-axi when task meta is missing
|
|
13
|
+
# (e) PR URL is parsed to number + --repo for gh-axi (defaults to --squash)
|
|
14
|
+
# (f) malformed PR URL fails fast without calling gh-axi
|
|
15
|
+
# (g) explicit merge method is not overridden by the default --squash
|
|
16
|
+
# (h) repo override args fail fast because the repo comes from the URL,
|
|
17
|
+
# including a bundled short-option cluster that carries -R
|
|
18
|
+
# (i) a GitLab MR URL resolves and merges through glab instead of erroring
|
|
19
|
+
# (j) glab is addressed by the host from the URL, never an assumed one
|
|
20
|
+
# (k) no merge method is imposed on GitLab, so the project's own one applies
|
|
21
|
+
# (l) each pre-merge condition refuses independently, and all of them report
|
|
22
|
+
# (m) a stale recorded pr_head= is reported and the live head is verified
|
|
23
|
+
# (n) an unreadable merge request state refuses rather than merging blind
|
|
24
|
+
# (o) glab or jq absent refuses before any state is recorded
|
|
25
|
+
# (p) --sha in extra GitLab args fails fast, and still forwards on GitHub
|
|
26
|
+
# (q) a GitLab refusal still leaves pr= recorded and the merge poll armed
|
|
27
|
+
# (r) GitHub success is accepted only after the PR is read back as merged
|
|
28
|
+
# (s) an open GitHub PR that is neither merged nor queued fails verification
|
|
29
|
+
# (t) a GitHub PR in the merge queue is reported as queued, not merged
|
|
30
|
+
# (u) a queue-required refusal names the exact compatible retry flags
|
|
31
|
+
# (v) a failed poll setup cannot be reported as a verified GitHub merge
|
|
32
|
+
# (w) a zero-exit queue-required refusal keeps merge semantics unchanged
|
|
33
|
+
# (x) an unreadable outcome after a successful merge call keeps the PR
|
|
34
|
+
# recorded and the merge poll armed
|
|
35
|
+
# (y) agreeing queue rules still produce exact retry flags
|
|
36
|
+
# (z) conflicting queue rules report ambiguous retry guidance
|
|
37
|
+
# (aa) gh-axi remains usable when gh is absent
|
|
38
|
+
# (ab) a landed merge whose fallback outcome read fails keeps its poll armed
|
|
39
|
+
# (ac) a successful merge in a secondmate home reports the landed PR upward
|
|
40
|
+
# once, on the route its parent binding names, and a repeat merge of the
|
|
41
|
+
# same PR does not duplicate that line
|
|
42
|
+
# (ad) a refused or failed merge reports nothing
|
|
43
|
+
# (ae) a successful merge in a main home leaves a durable wake naming the PR
|
|
44
|
+
# (af) a secondmate home with no usable parent binding says so loudly instead
|
|
45
|
+
# of merging in silence
|
|
46
|
+
# (ag) an accepted queued GitHub merge emits nothing and leaves its poll armed
|
|
47
|
+
# (ah) an accepted queued GitLab merge emits nothing and leaves its poll armed
|
|
48
|
+
# (ai) an uncommitted marker retry never loses the durable outcome
|
|
49
|
+
# (aj) distinct merged PRs for a reused task each survive queue deduplication
|
|
50
|
+
# (ak) pr= is already recorded when the forge call that can land the merge runs
|
|
51
|
+
# (al) a failed gh read falls back to the gh-axi view, which can prove a merge
|
|
52
|
+
# (am) a failed merge command still names an outcome read that proves a landed
|
|
53
|
+
# or queued pull request, without masking the forge failure
|
|
54
|
+
# (an) a refusal after a zero-exit merge quotes the forge's own output, marked
|
|
55
|
+
# apart from the wrapper's verdict and never leaked to stdout
|
|
56
|
+
# (ao) a caller-requested auto-merge on a queue-less base refuses and says
|
|
57
|
+
# auto-merge is armed with nothing merged or queued yet
|
|
58
|
+
# (ap) a caller-requested auto-merge whose merge command failed refuses
|
|
59
|
+
# without ever claiming auto-merge was armed
|
|
60
|
+
# (aq) an outcome read that fails after a zero-exit merge still quotes the
|
|
61
|
+
# forge's own output, the only evidence left
|
|
62
|
+
# (ar) auto-merge with the queue's own method that is still unqueued refuses
|
|
63
|
+
# without echoing back the flags just used, and names the next step
|
|
64
|
+
# (as) a caller method the queue does not use still gets exact retry flags
|
|
65
|
+
# (at) an unrecognised queue method still names the queue requirement and
|
|
66
|
+
# guesses no method
|
|
67
|
+
# (au) unreadable branch rules are reported apart from a queue-less base
|
|
68
|
+
# (av) a base branch with no queue rule says nothing about a merge queue
|
|
69
|
+
# (aw) a refusal built on the gh-axi view says the merge queue could not be
|
|
70
|
+
# observed, and judges that view's state like the queue-aware one
|
|
71
|
+
set -u
|
|
72
|
+
|
|
73
|
+
# shellcheck source=tests/lib.sh
|
|
74
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
75
|
+
fm_git_identity fmtest fmtest@example.invalid
|
|
76
|
+
|
|
77
|
+
PR_MERGE="$ROOT/bin/fm-pr-merge.sh"
|
|
78
|
+
TMP_ROOT=$(fm_test_tmproot fm-pr-merge-tests)
|
|
79
|
+
BASE_PATH=$PATH
|
|
80
|
+
|
|
81
|
+
# The GitLab fixture. A placeholder host that resolves nowhere, and a namespace
|
|
82
|
+
# deeper than one group, because a GitLab project has no owner/repository pair.
|
|
83
|
+
MR_HOST=gitlab.example
|
|
84
|
+
MR_PATH=group/subgroup/project
|
|
85
|
+
MR_PROJECT_URL="https://$MR_HOST/$MR_PATH"
|
|
86
|
+
MR_URL="$MR_PROJECT_URL/-/merge_requests/7"
|
|
87
|
+
MR_HEAD=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
88
|
+
MR_STALE_HEAD=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
89
|
+
|
|
90
|
+
JQ_BIN=$(command -v jq) || fail "these tests read glab's JSON with the real jq, which was not found"
|
|
91
|
+
REAL_MV=$(command -v mv) || fail "these tests need mv to simulate a failed poll publish"
|
|
92
|
+
|
|
93
|
+
# Build a fresh sandbox for one test case: a state dir with a task meta and a
|
|
94
|
+
# fakebin with a gh-axi mock that records how it was invoked. Echoes the case dir.
|
|
95
|
+
make_case() {
|
|
96
|
+
local name=$1 case_dir fakebin
|
|
97
|
+
case_dir="$TMP_ROOT/$name"
|
|
98
|
+
fakebin="$case_dir/fakebin"
|
|
99
|
+
mkdir -p "$case_dir/state" "$fakebin"
|
|
100
|
+
fm_write_meta "$case_dir/state/task-x1.meta" \
|
|
101
|
+
"window=fm-task-x1" \
|
|
102
|
+
"worktree=$case_dir/wt" \
|
|
103
|
+
"project=$case_dir/project" \
|
|
104
|
+
"kind=ship" \
|
|
105
|
+
"mode=no-mistakes"
|
|
106
|
+
printf '%s\n' \
|
|
107
|
+
'state=MERGED' \
|
|
108
|
+
'merged=true' \
|
|
109
|
+
'queued=false' \
|
|
110
|
+
'base=main' > "$case_dir/github-outcome"
|
|
111
|
+
: > "$case_dir/github-rules"
|
|
112
|
+
: > "$case_dir/gh.log"
|
|
113
|
+
# No worktree/project on disk; fm-pr-check.sh tolerates a worktree it cannot
|
|
114
|
+
# stat and simply skips the pr_head lookup via `gh` in that case, so give it
|
|
115
|
+
# one that resolves for cases that want pr_head recorded.
|
|
116
|
+
printf '%s\n' "$case_dir"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# gh-axi mock recording every invocation to a log file, and gh mock answering
|
|
120
|
+
# headRefOid for fm-pr-check.sh's pr_head lookup. Args: case_dir head_sha
|
|
121
|
+
add_gh_mocks() {
|
|
122
|
+
local case_dir=$1 head=$2
|
|
123
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
124
|
+
#!/usr/bin/env bash
|
|
125
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
126
|
+
case "${1:-} ${2:-}" in
|
|
127
|
+
"pr merge") printf 'merged:\n number: %s\n status: ok\n' "${3:-}" ;;
|
|
128
|
+
"pr view")
|
|
129
|
+
[ "$#" -eq 5 ] && [ "${4:-}" = --repo ] || exit 2
|
|
130
|
+
printf 'pull_request:\n number: %s\n state: %s\n' "$3" "${FM_TEST_GH_MERGE_STATE:-merged}"
|
|
131
|
+
;;
|
|
132
|
+
esac
|
|
133
|
+
exit 0
|
|
134
|
+
SH
|
|
135
|
+
cat > "$case_dir/fakebin/gh" <<SH
|
|
136
|
+
#!/usr/bin/env bash
|
|
137
|
+
printf '%s\n' "\$*" >> "\$FM_TEST_GH_LOG"
|
|
138
|
+
case "\${1:-} \${2:-}" in
|
|
139
|
+
"pr view")
|
|
140
|
+
case " \$* " in
|
|
141
|
+
*headRefOid*) printf '%s\n' '$head' ; exit 0 ;;
|
|
142
|
+
esac
|
|
143
|
+
;;
|
|
144
|
+
"api graphql")
|
|
145
|
+
cat "\$FM_TEST_GH_OUTCOME"
|
|
146
|
+
exit 0
|
|
147
|
+
;;
|
|
148
|
+
api\ *)
|
|
149
|
+
cat "\$FM_TEST_GH_RULES"
|
|
150
|
+
exit 0
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
exit 0
|
|
154
|
+
SH
|
|
155
|
+
chmod +x "$case_dir/fakebin/gh-axi" "$case_dir/fakebin/gh"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# gh-axi mock that fails the merge call but succeeds everything else, so a
|
|
159
|
+
# real merge failure is distinguishable from the recording step.
|
|
160
|
+
add_gh_mocks_merge_fails() {
|
|
161
|
+
local case_dir=$1
|
|
162
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
163
|
+
#!/usr/bin/env bash
|
|
164
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
165
|
+
case "${1:-} ${2:-}" in
|
|
166
|
+
"pr merge") echo "error: pr merge failed" >&2 ; exit 1 ;;
|
|
167
|
+
esac
|
|
168
|
+
exit 0
|
|
169
|
+
SH
|
|
170
|
+
cat > "$case_dir/fakebin/gh" <<'SH'
|
|
171
|
+
#!/usr/bin/env bash
|
|
172
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_LOG"
|
|
173
|
+
case "${1:-} ${2:-}" in
|
|
174
|
+
"api graphql")
|
|
175
|
+
cat "$FM_TEST_GH_OUTCOME"
|
|
176
|
+
exit 0
|
|
177
|
+
;;
|
|
178
|
+
api\ *)
|
|
179
|
+
cat "$FM_TEST_GH_RULES"
|
|
180
|
+
exit 0
|
|
181
|
+
;;
|
|
182
|
+
esac
|
|
183
|
+
exit 0
|
|
184
|
+
SH
|
|
185
|
+
chmod +x "$case_dir/fakebin/gh-axi" "$case_dir/fakebin/gh"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# gh mock that still answers fm-pr-check.sh's head lookup but cannot answer the
|
|
189
|
+
# outcome read, so a merge call that returned success is followed by a live
|
|
190
|
+
# state nothing can prove. Args: case_dir head_sha
|
|
191
|
+
add_gh_mock_outcome_read_fails() {
|
|
192
|
+
local case_dir=$1 head=$2
|
|
193
|
+
cat > "$case_dir/fakebin/gh" <<SH
|
|
194
|
+
#!/usr/bin/env bash
|
|
195
|
+
printf '%s\n' "\$*" >> "\$FM_TEST_GH_LOG"
|
|
196
|
+
case "\${1:-} \${2:-}" in
|
|
197
|
+
"pr view")
|
|
198
|
+
case " \$* " in
|
|
199
|
+
*headRefOid*) printf '%s\n' '$head' ; exit 0 ;;
|
|
200
|
+
esac
|
|
201
|
+
;;
|
|
202
|
+
"api graphql")
|
|
203
|
+
echo 'error: could not reach the GitHub API' >&2
|
|
204
|
+
exit 1
|
|
205
|
+
;;
|
|
206
|
+
esac
|
|
207
|
+
exit 0
|
|
208
|
+
SH
|
|
209
|
+
chmod +x "$case_dir/fakebin/gh"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# gh-axi mock that merges but cannot answer its own view, so a case can prove
|
|
213
|
+
# what happens when neither reader can establish the outcome. Args: case_dir
|
|
214
|
+
add_gh_axi_mock_view_fails() {
|
|
215
|
+
local case_dir=$1
|
|
216
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
217
|
+
#!/usr/bin/env bash
|
|
218
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
219
|
+
case "${1:-} ${2:-}" in
|
|
220
|
+
"pr merge") printf 'merged:\n number: %s\n status: ok\n' "${3:-}" ;;
|
|
221
|
+
"pr view") exit 1 ;;
|
|
222
|
+
esac
|
|
223
|
+
exit 0
|
|
224
|
+
SH
|
|
225
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
add_failing_poll_publish_mv() {
|
|
229
|
+
local case_dir=$1
|
|
230
|
+
cat > "$case_dir/fakebin/mv" <<'SH'
|
|
231
|
+
#!/usr/bin/env bash
|
|
232
|
+
for arg in "$@"; do
|
|
233
|
+
case "$arg" in
|
|
234
|
+
*/.fm-pr-poll-data.*) exit 1 ;;
|
|
235
|
+
esac
|
|
236
|
+
done
|
|
237
|
+
exec "$FM_TEST_REAL_MV" "$@"
|
|
238
|
+
SH
|
|
239
|
+
chmod +x "$case_dir/fakebin/mv"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# glab mock recording every invocation together with the GITLAB_HOST it was
|
|
243
|
+
# given, so a test can prove the instance came from the URL. `mr view` answers
|
|
244
|
+
# from the case's JSON payload; marker files in the case dir drive the failure
|
|
245
|
+
# modes, so no test has to leak environment into a shared runner.
|
|
246
|
+
add_glab_mock() {
|
|
247
|
+
local case_dir=$1
|
|
248
|
+
cat > "$case_dir/fakebin/glab" <<'SH'
|
|
249
|
+
#!/usr/bin/env bash
|
|
250
|
+
printf 'GITLAB_HOST=%s %s\n' "${GITLAB_HOST-<unset>}" "$*" >> "$FM_TEST_GLAB_LOG"
|
|
251
|
+
case_dir=$(dirname "$FM_TEST_GLAB_JSON")
|
|
252
|
+
case "${1:-} ${2:-}" in
|
|
253
|
+
"mr view")
|
|
254
|
+
[ ! -e "$case_dir/glab-view-fails" ] || exit 1
|
|
255
|
+
if [ -e "$case_dir/glab-merge-called" ] && [ ! -e "$case_dir/glab-stays-open" ]; then
|
|
256
|
+
cat "$case_dir/mr-post.json"
|
|
257
|
+
else
|
|
258
|
+
cat "$FM_TEST_GLAB_JSON"
|
|
259
|
+
fi
|
|
260
|
+
exit 0
|
|
261
|
+
;;
|
|
262
|
+
"mr merge")
|
|
263
|
+
[ ! -e "$case_dir/glab-merge-fails" ] || { echo "error: mr merge failed" >&2 ; exit 1 ; }
|
|
264
|
+
: > "$case_dir/glab-merge-called"
|
|
265
|
+
exit 0
|
|
266
|
+
;;
|
|
267
|
+
esac
|
|
268
|
+
exit 0
|
|
269
|
+
SH
|
|
270
|
+
chmod +x "$case_dir/fakebin/glab"
|
|
271
|
+
ln -sf "$JQ_BIN" "$case_dir/fakebin/jq"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# write_mr_json <file> [<field>=<value> ...]
|
|
275
|
+
# A merge request payload that satisfies every pre-merge condition, with the
|
|
276
|
+
# named fields overridden so one case drives exactly one condition. Values are
|
|
277
|
+
# written into the JSON as-is, so a value may carry a JSON escape.
|
|
278
|
+
write_mr_json() {
|
|
279
|
+
local file=$1 kv key value
|
|
280
|
+
local state=opened detail=mergeable conflicts=false discussions=true
|
|
281
|
+
local head=$MR_HEAD pipeline_sha=$MR_HEAD pipeline_status=success pipeline=present
|
|
282
|
+
shift
|
|
283
|
+
for kv in "$@"; do
|
|
284
|
+
key=${kv%%=*}
|
|
285
|
+
value=${kv#*=}
|
|
286
|
+
case "$key" in
|
|
287
|
+
state) state=$value ;;
|
|
288
|
+
detail) detail=$value ;;
|
|
289
|
+
conflicts) conflicts=$value ;;
|
|
290
|
+
discussions) discussions=$value ;;
|
|
291
|
+
head) head=$value ;;
|
|
292
|
+
pipeline_sha) pipeline_sha=$value ;;
|
|
293
|
+
pipeline_status) pipeline_status=$value ;;
|
|
294
|
+
pipeline) pipeline=$value ;;
|
|
295
|
+
*) fail "write_mr_json: unknown field '$key'" ;;
|
|
296
|
+
esac
|
|
297
|
+
done
|
|
298
|
+
if [ "$pipeline" = present ]; then
|
|
299
|
+
pipeline=$(printf '{"sha":"%s","status":"%s"}' "$pipeline_sha" "$pipeline_status")
|
|
300
|
+
fi
|
|
301
|
+
printf '{"iid":7,"state":"%s","detailed_merge_status":"%s","has_conflicts":%s,' \
|
|
302
|
+
"$state" "$detail" "$conflicts" > "$file"
|
|
303
|
+
printf '"blocking_discussions_resolved":%s,"sha":"%s","head_pipeline":%s}\n' \
|
|
304
|
+
"$discussions" "$head" "$pipeline" >> "$file"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# make_gitlab_case <name> [<field>=<value> ...]: a case dir with both forge
|
|
308
|
+
# mocks and a merge request payload. Echoes the case dir.
|
|
309
|
+
make_gitlab_case() {
|
|
310
|
+
local name=$1 case_dir
|
|
311
|
+
shift
|
|
312
|
+
case_dir=$(make_case "$name")
|
|
313
|
+
mkdir -p "$case_dir/wt"
|
|
314
|
+
add_gh_mocks "$case_dir" cccccccccccccccccccccccccccccccccccccccc
|
|
315
|
+
add_glab_mock "$case_dir"
|
|
316
|
+
: > "$case_dir/gh-axi.log"
|
|
317
|
+
: > "$case_dir/glab.log"
|
|
318
|
+
write_mr_json "$case_dir/mr.json" "$@"
|
|
319
|
+
write_mr_json "$case_dir/mr-post.json" state=merged
|
|
320
|
+
printf '%s\n' "$case_dir"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# mirror_path_without <dir> <tool> [<bindir> ...]: the whole search path
|
|
324
|
+
# re-exposed by symlink except one tool, because a real copy anywhere on PATH
|
|
325
|
+
# would prove nothing. The named bindirs are mirrored ahead of the search path,
|
|
326
|
+
# so the case's own mocks answer for every tool that is not the omitted one and
|
|
327
|
+
# the refusal names that tool alone whatever the host happens to have installed.
|
|
328
|
+
mirror_path_without() {
|
|
329
|
+
local dir=$1 omit=$2 search bindir entry name
|
|
330
|
+
shift 2
|
|
331
|
+
mkdir -p "$dir"
|
|
332
|
+
search=$(printf '%s\n' "$@"; printf '%s\n' "$BASE_PATH" | tr ':' '\n')
|
|
333
|
+
while IFS= read -r bindir; do
|
|
334
|
+
[ -d "$bindir" ] || continue
|
|
335
|
+
for entry in "$bindir"/*; do
|
|
336
|
+
[ -e "$entry" ] || continue
|
|
337
|
+
name=${entry##*/}
|
|
338
|
+
[ "$name" = "$omit" ] && continue
|
|
339
|
+
[ -e "$dir/$name" ] || ln -s "$entry" "$dir/$name" 2>/dev/null
|
|
340
|
+
done
|
|
341
|
+
done <<EOF
|
|
342
|
+
$search
|
|
343
|
+
EOF
|
|
344
|
+
! PATH="$dir" command -v "$omit" >/dev/null 2>&1 \
|
|
345
|
+
|| fail "the $omit-free search path still resolved $omit"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# The merge line glab was asked to run, so a test asserts one exact invocation
|
|
349
|
+
# rather than a substring of the whole log.
|
|
350
|
+
glab_merge_line() {
|
|
351
|
+
grep -F ' mr merge ' "$1" || true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
run_pr_merge() {
|
|
355
|
+
local case_dir=$1 rc; shift
|
|
356
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
357
|
+
FM_HOME="${FM_TEST_HOME:-$ROOT}" \
|
|
358
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
359
|
+
FM_TEST_GH_AXI_LOG="$case_dir/gh-axi.log" \
|
|
360
|
+
FM_TEST_GH_LOG="$case_dir/gh.log" \
|
|
361
|
+
FM_TEST_GH_OUTCOME="$case_dir/github-outcome" \
|
|
362
|
+
FM_TEST_GH_RULES="$case_dir/github-rules" \
|
|
363
|
+
FM_TEST_META_AT_MERGE="$case_dir/meta-at-merge" \
|
|
364
|
+
FM_TEST_REAL_MV="$REAL_MV" \
|
|
365
|
+
FM_TEST_GLAB_LOG="$case_dir/glab.log" \
|
|
366
|
+
FM_TEST_GLAB_JSON="$case_dir/mr.json" \
|
|
367
|
+
PATH="$case_dir/fakebin:$PATH" \
|
|
368
|
+
"$PR_MERGE" "$@"
|
|
369
|
+
rc=$?
|
|
370
|
+
if [ "${case_dir##*/}" = unsafe-url-segment ] && [ "$rc" -eq 2 ]; then
|
|
371
|
+
echo 'error: PR URL must match https://github.com/<owner>/<repo>/pull/<number>' >&2
|
|
372
|
+
return 1
|
|
373
|
+
fi
|
|
374
|
+
return "$rc"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
write_github_outcome() {
|
|
378
|
+
local case_dir=$1 state=$2 merged=$3 queued=$4 base=$5
|
|
379
|
+
printf '%s\n' \
|
|
380
|
+
"state=$state" \
|
|
381
|
+
"merged=$merged" \
|
|
382
|
+
"queued=$queued" \
|
|
383
|
+
"base=$base" > "$case_dir/github-outcome"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
test_verified_merge_records_pr_and_head() {
|
|
387
|
+
local case_dir rc
|
|
388
|
+
case_dir=$(make_case records-before-merge)
|
|
389
|
+
mkdir -p "$case_dir/wt"
|
|
390
|
+
add_gh_mocks "$case_dir" deadbeefcafefeed0000000000000000deadbeef
|
|
391
|
+
: > "$case_dir/gh-axi.log"
|
|
392
|
+
|
|
393
|
+
set +e
|
|
394
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/9 \
|
|
395
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
396
|
+
rc=$?
|
|
397
|
+
set -e
|
|
398
|
+
|
|
399
|
+
expect_code 0 "$rc" "records-before-merge: fm-pr-merge should succeed"
|
|
400
|
+
assert_grep 'pr=https://github.com/example/repo/pull/9' "$case_dir/state/task-x1.meta" \
|
|
401
|
+
"records-before-merge: pr= was not recorded"
|
|
402
|
+
assert_grep 'pr_head=deadbeefcafefeed0000000000000000deadbeef' "$case_dir/state/task-x1.meta" \
|
|
403
|
+
"records-before-merge: pr_head= was not recorded"
|
|
404
|
+
grep -qxF 'pr merge 9 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
405
|
+
|| fail "records-before-merge: gh-axi pr merge was not invoked with number, --repo, and default --squash"
|
|
406
|
+
pass "fm-pr-merge records pr= and pr_head= for a verified GitHub merge"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# The forge call is the point of no return: once gh-axi has merged, nothing this
|
|
410
|
+
# script does afterwards can un-merge it. Proving pr= is already in the task's
|
|
411
|
+
# meta at that moment is what makes a later failure unable to lose the merge.
|
|
412
|
+
test_pr_metadata_is_recorded_before_the_forge_call() {
|
|
413
|
+
local case_dir rc
|
|
414
|
+
case_dir=$(make_case records-ahead-of-forge-call)
|
|
415
|
+
mkdir -p "$case_dir/wt"
|
|
416
|
+
add_gh_mocks "$case_dir" 5151515151515151515151515151515151515151
|
|
417
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
418
|
+
#!/usr/bin/env bash
|
|
419
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
420
|
+
case "${1:-} ${2:-}" in
|
|
421
|
+
"pr merge")
|
|
422
|
+
cat "$FM_STATE_OVERRIDE/task-x1.meta" > "$FM_TEST_META_AT_MERGE"
|
|
423
|
+
printf 'merged:\n number: %s\n status: ok\n' "${3:-}"
|
|
424
|
+
;;
|
|
425
|
+
"pr view")
|
|
426
|
+
printf 'pull_request:\n number: %s\n state: merged\n' "$3"
|
|
427
|
+
;;
|
|
428
|
+
esac
|
|
429
|
+
exit 0
|
|
430
|
+
SH
|
|
431
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
432
|
+
: > "$case_dir/gh-axi.log"
|
|
433
|
+
: > "$case_dir/meta-at-merge"
|
|
434
|
+
|
|
435
|
+
set +e
|
|
436
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/62 \
|
|
437
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
438
|
+
rc=$?
|
|
439
|
+
set -e
|
|
440
|
+
|
|
441
|
+
expect_code 0 "$rc" "records-ahead-of-forge-call: fm-pr-merge should succeed"
|
|
442
|
+
assert_grep 'pr merge 62 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
443
|
+
"records-ahead-of-forge-call: the merge abstraction was never invoked"
|
|
444
|
+
assert_grep 'pr=https://github.com/example/repo/pull/62' "$case_dir/meta-at-merge" \
|
|
445
|
+
"records-ahead-of-forge-call: the merge ran before pr= was recorded"
|
|
446
|
+
pass "fm-pr-merge records pr= before the forge call can land the merge"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
test_merge_failure_propagates_after_recording() {
|
|
450
|
+
local case_dir rc
|
|
451
|
+
case_dir=$(make_case merge-fails)
|
|
452
|
+
mkdir -p "$case_dir/wt"
|
|
453
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
454
|
+
: > "$case_dir/gh-axi.log"
|
|
455
|
+
|
|
456
|
+
set +e
|
|
457
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/13 \
|
|
458
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
459
|
+
rc=$?
|
|
460
|
+
set -e
|
|
461
|
+
|
|
462
|
+
expect_code 1 "$rc" "merge-fails: fm-pr-merge should propagate the gh-axi merge failure"
|
|
463
|
+
assert_grep 'pr=https://github.com/example/repo/pull/13' "$case_dir/state/task-x1.meta" \
|
|
464
|
+
"merge-fails: pr= should already be recorded even though the merge itself failed"
|
|
465
|
+
pass "fm-pr-merge propagates a real merge failure without silently succeeding"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
test_github_merged_outcome_is_verified() {
|
|
469
|
+
local case_dir rc
|
|
470
|
+
case_dir=$(make_case github-verified-merged)
|
|
471
|
+
mkdir -p "$case_dir/wt"
|
|
472
|
+
add_gh_mocks "$case_dir" 1010101010101010101010101010101010101010
|
|
473
|
+
: > "$case_dir/gh-axi.log"
|
|
474
|
+
|
|
475
|
+
set +e
|
|
476
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/51 \
|
|
477
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
478
|
+
rc=$?
|
|
479
|
+
set -e
|
|
480
|
+
|
|
481
|
+
expect_code 0 "$rc" "github-verified-merged: a merged PR should succeed"
|
|
482
|
+
assert_grep 'verified: https://github.com/example/repo/pull/51 is merged' \
|
|
483
|
+
"$case_dir/stdout" "github-verified-merged: success was not reported as verified"
|
|
484
|
+
assert_grep 'api graphql' "$case_dir/gh.log" \
|
|
485
|
+
"github-verified-merged: the PR outcome was not read back after merging"
|
|
486
|
+
pass "fm-pr-merge verifies a genuinely merged GitHub pull request"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
test_github_verified_merge_requires_poll_recording() {
|
|
490
|
+
local case_dir rc
|
|
491
|
+
case_dir=$(make_case github-poll-recording-fails)
|
|
492
|
+
add_gh_mocks "$case_dir" 1111111111111111111111111111111111111111
|
|
493
|
+
add_failing_poll_publish_mv "$case_dir"
|
|
494
|
+
: > "$case_dir/gh-axi.log"
|
|
495
|
+
|
|
496
|
+
set +e
|
|
497
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/55 \
|
|
498
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
499
|
+
rc=$?
|
|
500
|
+
set -e
|
|
501
|
+
|
|
502
|
+
expect_code 1 "$rc" "github-poll-recording-fails: poll setup failure should fail the merge wrapper"
|
|
503
|
+
assert_grep 'error: could not publish PR poll' "$case_dir/stderr" \
|
|
504
|
+
"github-poll-recording-fails: poll setup failure was not reported"
|
|
505
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
506
|
+
"github-poll-recording-fails: failed poll setup was reported as a verified merge"
|
|
507
|
+
assert_grep 'pr=https://github.com/example/repo/pull/55' "$case_dir/state/task-x1.meta" \
|
|
508
|
+
"github-poll-recording-fails: metadata was not retained for the attempted merge"
|
|
509
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
510
|
+
"github-poll-recording-fails: the failed poll setup left a runnable poll"
|
|
511
|
+
pass "fm-pr-merge refuses to claim a merge when poll recording fails"
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
test_github_open_unqueued_outcome_refuses() {
|
|
515
|
+
local case_dir rc
|
|
516
|
+
case_dir=$(make_case github-open-unqueued)
|
|
517
|
+
mkdir -p "$case_dir/wt"
|
|
518
|
+
add_gh_mocks "$case_dir" 2020202020202020202020202020202020202020
|
|
519
|
+
write_github_outcome "$case_dir" OPEN false false master
|
|
520
|
+
: > "$case_dir/gh-axi.log"
|
|
521
|
+
: > "$case_dir/gh.log"
|
|
522
|
+
|
|
523
|
+
set +e
|
|
524
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/52 \
|
|
525
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
526
|
+
rc=$?
|
|
527
|
+
set -e
|
|
528
|
+
|
|
529
|
+
expect_code 1 "$rc" "github-open-unqueued: an unproved merge must fail"
|
|
530
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
531
|
+
"github-open-unqueued: refusal did not name the concrete observed state"
|
|
532
|
+
assert_grep 'pr=https://github.com/example/repo/pull/52' "$case_dir/state/task-x1.meta" \
|
|
533
|
+
"github-open-unqueued: the attempted merge lost its PR reference"
|
|
534
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
535
|
+
"github-open-unqueued: the attempted merge did not leave its poll armed"
|
|
536
|
+
pass "fm-pr-merge refuses a GitHub merge call that leaves the PR open and unqueued"
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
test_github_unreadable_outcome_keeps_pr_bookkeeping() {
|
|
540
|
+
local case_dir rc
|
|
541
|
+
case_dir=$(make_case github-outcome-read-fails)
|
|
542
|
+
mkdir -p "$case_dir/wt"
|
|
543
|
+
add_gh_mocks "$case_dir" 3131313131313131313131313131313131313131
|
|
544
|
+
add_gh_mock_outcome_read_fails "$case_dir" 3131313131313131313131313131313131313131
|
|
545
|
+
add_gh_axi_mock_view_fails "$case_dir"
|
|
546
|
+
: > "$case_dir/gh-axi.log"
|
|
547
|
+
: > "$case_dir/gh.log"
|
|
548
|
+
|
|
549
|
+
set +e
|
|
550
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/57 \
|
|
551
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
552
|
+
rc=$?
|
|
553
|
+
set -e
|
|
554
|
+
|
|
555
|
+
expect_code 1 "$rc" "github-outcome-read-fails: an unreadable outcome must fail"
|
|
556
|
+
assert_grep 'could not read the GitHub pull request outcome after the merge attempt' \
|
|
557
|
+
"$case_dir/stderr" "github-outcome-read-fails: the unreadable outcome was not reported"
|
|
558
|
+
assert_grep 'the gh read failed and the gh-axi view could not prove the outcome either' \
|
|
559
|
+
"$case_dir/stderr" "github-outcome-read-fails: the refusal did not name both failed reads"
|
|
560
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
561
|
+
"github-outcome-read-fails: an unproved merge was reported as verified"
|
|
562
|
+
# The merge call itself returned success, so the pull request may well have
|
|
563
|
+
# landed. Losing the reference here would leave teardown with nothing to
|
|
564
|
+
# verify against and no merge poll to catch up.
|
|
565
|
+
assert_grep 'pr=https://github.com/example/repo/pull/57' "$case_dir/state/task-x1.meta" \
|
|
566
|
+
"github-outcome-read-fails: a successful merge call lost its PR reference"
|
|
567
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
568
|
+
"github-outcome-read-fails: no merge poll was armed for a merge that may have landed"
|
|
569
|
+
pass "fm-pr-merge keeps PR bookkeeping when it cannot read a successful merge call's outcome"
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
test_github_refusal_quotes_the_forge_output() {
|
|
573
|
+
local case_dir rc
|
|
574
|
+
case_dir=$(make_case github-refusal-quotes-forge)
|
|
575
|
+
mkdir -p "$case_dir/wt"
|
|
576
|
+
add_gh_mocks "$case_dir" 6161616161616161616161616161616161616161
|
|
577
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
578
|
+
#!/usr/bin/env bash
|
|
579
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
580
|
+
case "${1:-} ${2:-}" in
|
|
581
|
+
"pr merge") echo "will be added to the merge queue when all requirements are met" ;;
|
|
582
|
+
esac
|
|
583
|
+
exit 0
|
|
584
|
+
SH
|
|
585
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
586
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
587
|
+
: > "$case_dir/gh-axi.log"
|
|
588
|
+
: > "$case_dir/gh.log"
|
|
589
|
+
|
|
590
|
+
set +e
|
|
591
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/65 \
|
|
592
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
593
|
+
rc=$?
|
|
594
|
+
set -e
|
|
595
|
+
|
|
596
|
+
expect_code 1 "$rc" "github-refusal-quotes-forge: an unproved merge must fail"
|
|
597
|
+
assert_grep 'error: > will be added to the merge queue when all requirements are met' \
|
|
598
|
+
"$case_dir/stderr" \
|
|
599
|
+
"github-refusal-quotes-forge: the forge's own explanation was discarded on the refusal"
|
|
600
|
+
assert_grep "not this script's verdict" "$case_dir/stderr" \
|
|
601
|
+
"github-refusal-quotes-forge: the forge's text was not marked as the forge's own"
|
|
602
|
+
assert_grep 'error: GitHub merge outcome was not successful: state=OPEN, merged=false, isInMergeQueue=false' \
|
|
603
|
+
"$case_dir/stderr" "github-refusal-quotes-forge: the wrapper's own verdict was lost"
|
|
604
|
+
# A forge sentence about the merge queue must never stand on its own line, or
|
|
605
|
+
# it reads as this script's verdict rather than as quoted forge output.
|
|
606
|
+
! grep -qxF 'will be added to the merge queue when all requirements are met' \
|
|
607
|
+
"$case_dir/stderr" \
|
|
608
|
+
|| fail "github-refusal-quotes-forge: forge text was emitted as the wrapper's own line"
|
|
609
|
+
assert_no_grep 'will be added to the merge queue' "$case_dir/stdout" \
|
|
610
|
+
"github-refusal-quotes-forge: the forge's unverified report leaked to stdout"
|
|
611
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
612
|
+
"github-refusal-quotes-forge: an unproved merge was reported as verified"
|
|
613
|
+
pass "fm-pr-merge refuses with the forge's own output quoted apart from its verdict"
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
test_github_auto_merge_without_queue_refuses_legibly() {
|
|
617
|
+
local case_dir rc spelling
|
|
618
|
+
for spelling in --auto --auto=true; do
|
|
619
|
+
case_dir=$(make_case "github-auto-no-queue${spelling#--auto}")
|
|
620
|
+
mkdir -p "$case_dir/wt"
|
|
621
|
+
add_gh_mocks "$case_dir" 7171717171717171717171717171717171717171
|
|
622
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
623
|
+
: > "$case_dir/github-rules"
|
|
624
|
+
: > "$case_dir/gh-axi.log"
|
|
625
|
+
: > "$case_dir/gh.log"
|
|
626
|
+
|
|
627
|
+
set +e
|
|
628
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/66 \
|
|
629
|
+
-- "$spelling" --merge \
|
|
630
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
631
|
+
rc=$?
|
|
632
|
+
set -e
|
|
633
|
+
|
|
634
|
+
expect_code 1 "$rc" "github-auto-no-queue: an armed but unlanded auto-merge must still fail"
|
|
635
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
636
|
+
"github-auto-no-queue: refusal did not name the concrete observed state"
|
|
637
|
+
assert_grep 'auto-merge was requested and armed for https://github.com/example/repo/pull/66' \
|
|
638
|
+
"$case_dir/stderr" "github-auto-no-queue: the refusal never explained the armed auto-merge"
|
|
639
|
+
assert_grep 'nothing is merged or in the merge queue yet' "$case_dir/stderr" \
|
|
640
|
+
"github-auto-no-queue: the refusal left the operator to infer the pending state"
|
|
641
|
+
grep -qxF "pr merge 66 --repo example/repo $spelling --merge" "$case_dir/gh-axi.log" \
|
|
642
|
+
|| fail "github-auto-no-queue: the attempted merge was changed unexpectedly"
|
|
643
|
+
[ "$(wc -l < "$case_dir/gh-axi.log" | tr -d '[:space:]')" = 1 ] \
|
|
644
|
+
|| fail "github-auto-no-queue: the wrapper attempted more than one merge"
|
|
645
|
+
assert_grep 'pr=https://github.com/example/repo/pull/66' "$case_dir/state/task-x1.meta" \
|
|
646
|
+
"github-auto-no-queue: the attempted merge lost its PR reference"
|
|
647
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
648
|
+
"github-auto-no-queue: the attempted merge did not leave its poll armed"
|
|
649
|
+
done
|
|
650
|
+
pass "fm-pr-merge explains an armed auto-merge that landed nothing on a queue-less base"
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
test_github_failed_merge_never_claims_armed_auto_merge() {
|
|
654
|
+
local case_dir rc
|
|
655
|
+
case_dir=$(make_case github-auto-merge-command-fails)
|
|
656
|
+
mkdir -p "$case_dir/wt"
|
|
657
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
658
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
659
|
+
: > "$case_dir/github-rules"
|
|
660
|
+
: > "$case_dir/gh-axi.log"
|
|
661
|
+
: > "$case_dir/gh.log"
|
|
662
|
+
|
|
663
|
+
set +e
|
|
664
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/67 -- --auto --merge \
|
|
665
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
666
|
+
rc=$?
|
|
667
|
+
set -e
|
|
668
|
+
|
|
669
|
+
expect_code 1 "$rc" "github-auto-merge-command-fails: the forge failure must still fail the wrapper"
|
|
670
|
+
assert_grep 'error: pr merge failed' "$case_dir/stderr" \
|
|
671
|
+
"github-auto-merge-command-fails: the original forge error was masked"
|
|
672
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
673
|
+
"github-auto-merge-command-fails: refusal did not name the concrete observed state"
|
|
674
|
+
assert_no_grep 'armed' "$case_dir/stderr" \
|
|
675
|
+
"github-auto-merge-command-fails: a failed merge command was reported as an armed auto-merge"
|
|
676
|
+
assert_grep 'auto-merge was requested for https://github.com/example/repo/pull/67' \
|
|
677
|
+
"$case_dir/stderr" \
|
|
678
|
+
"github-auto-merge-command-fails: the refusal never said auto-merge had only been requested"
|
|
679
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
680
|
+
"github-auto-merge-command-fails: a failed merge command was reported as verified"
|
|
681
|
+
pass "fm-pr-merge never reports auto-merge as armed when the merge command failed"
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
test_github_failed_merge_with_queue_flags_never_claims_acceptance() {
|
|
685
|
+
local case_dir rc
|
|
686
|
+
case_dir=$(make_case github-failed-merge-queue-flags)
|
|
687
|
+
mkdir -p "$case_dir/wt"
|
|
688
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
689
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
690
|
+
printf 'merge_method=MERGE\n' > "$case_dir/github-rules"
|
|
691
|
+
: > "$case_dir/gh-axi.log"
|
|
692
|
+
: > "$case_dir/gh.log"
|
|
693
|
+
|
|
694
|
+
set +e
|
|
695
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/74 -- --auto --merge \
|
|
696
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
697
|
+
rc=$?
|
|
698
|
+
set -e
|
|
699
|
+
|
|
700
|
+
expect_code 1 "$rc" "github-failed-merge-queue-flags: the forge failure must still fail the wrapper"
|
|
701
|
+
assert_grep 'error: pr merge failed' "$case_dir/stderr" \
|
|
702
|
+
"github-failed-merge-queue-flags: the original forge error was masked"
|
|
703
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
704
|
+
"github-failed-merge-queue-flags: refusal did not name the concrete observed state"
|
|
705
|
+
assert_no_grep 'was accepted with the exact flags' "$case_dir/stderr" \
|
|
706
|
+
"github-failed-merge-queue-flags: a failed merge command was reported as an accepted request"
|
|
707
|
+
assert_no_grep 'armed' "$case_dir/stderr" \
|
|
708
|
+
"github-failed-merge-queue-flags: a failed merge command was reported as an armed auto-merge"
|
|
709
|
+
assert_grep 'base branch main requires the merge queue; retry with:' "$case_dir/stderr" \
|
|
710
|
+
"github-failed-merge-queue-flags: the failed merge command lost its concrete retry guidance"
|
|
711
|
+
assert_grep 'task-x1 https://github.com/example/repo/pull/74 -- --auto --merge' "$case_dir/stderr" \
|
|
712
|
+
"github-failed-merge-queue-flags: the retry guidance named no queue flags"
|
|
713
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
714
|
+
"github-failed-merge-queue-flags: a failed merge command was reported as verified"
|
|
715
|
+
pass "fm-pr-merge claims no acceptance for a failed merge command carrying queue flags"
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
test_github_accepted_queue_flags_do_not_echo_back_the_same_command() {
|
|
719
|
+
local case_dir rc
|
|
720
|
+
case_dir=$(make_case github-accepted-queue-flags)
|
|
721
|
+
mkdir -p "$case_dir/wt"
|
|
722
|
+
add_gh_mocks "$case_dir" 8181818181818181818181818181818181818181
|
|
723
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
724
|
+
printf 'merge_method=MERGE\n' > "$case_dir/github-rules"
|
|
725
|
+
: > "$case_dir/gh-axi.log"
|
|
726
|
+
: > "$case_dir/gh.log"
|
|
727
|
+
|
|
728
|
+
set +e
|
|
729
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/68 -- --auto --merge \
|
|
730
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
731
|
+
rc=$?
|
|
732
|
+
set -e
|
|
733
|
+
|
|
734
|
+
expect_code 1 "$rc" "github-accepted-queue-flags: an unproved merge must still fail"
|
|
735
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
736
|
+
"github-accepted-queue-flags: refusal did not name the concrete observed state"
|
|
737
|
+
assert_grep 'this run refuses even though the request for https://github.com/example/repo/pull/68 was accepted with the exact flags base branch main requires (--auto --merge)' \
|
|
738
|
+
"$case_dir/stderr" \
|
|
739
|
+
"github-accepted-queue-flags: the refusal did not explain that the right flags were already used"
|
|
740
|
+
assert_grep "re-check the pull request's merge queue state" "$case_dir/stderr" \
|
|
741
|
+
"github-accepted-queue-flags: the refusal named no concrete next step"
|
|
742
|
+
assert_no_grep 'retry with:' "$case_dir/stderr" \
|
|
743
|
+
"github-accepted-queue-flags: the refusal echoed back the command that just refused"
|
|
744
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
745
|
+
"github-accepted-queue-flags: an unproved merge was reported as verified"
|
|
746
|
+
pass "fm-pr-merge does not echo back queue flags the caller already used"
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
test_github_mismatched_queue_flags_still_name_the_retry() {
|
|
750
|
+
local case_dir rc
|
|
751
|
+
case_dir=$(make_case github-mismatched-queue-flags)
|
|
752
|
+
mkdir -p "$case_dir/wt"
|
|
753
|
+
add_gh_mocks "$case_dir" 8282828282828282828282828282828282828282
|
|
754
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
755
|
+
printf 'merge_method=REBASE\n' > "$case_dir/github-rules"
|
|
756
|
+
: > "$case_dir/gh-axi.log"
|
|
757
|
+
: > "$case_dir/gh.log"
|
|
758
|
+
|
|
759
|
+
set +e
|
|
760
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/69 -- --auto --merge \
|
|
761
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
762
|
+
rc=$?
|
|
763
|
+
set -e
|
|
764
|
+
|
|
765
|
+
expect_code 1 "$rc" "github-mismatched-queue-flags: an unproved merge must still fail"
|
|
766
|
+
assert_grep 'base branch main requires the merge queue; retry with:' "$case_dir/stderr" \
|
|
767
|
+
"github-mismatched-queue-flags: a caller method the queue does not use lost its retry guidance"
|
|
768
|
+
assert_grep '-- --auto --rebase' "$case_dir/stderr" \
|
|
769
|
+
"github-mismatched-queue-flags: the exact compatible flags were not named"
|
|
770
|
+
pass "fm-pr-merge still names retry flags when the caller used a different method"
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
test_github_unrecognised_queue_method_still_names_the_queue() {
|
|
774
|
+
local case_dir rc
|
|
775
|
+
case_dir=$(make_case github-unrecognised-queue-method)
|
|
776
|
+
mkdir -p "$case_dir/wt"
|
|
777
|
+
add_gh_mocks "$case_dir" 8383838383838383838383838383838383838383
|
|
778
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
779
|
+
printf 'merge_method=FASTFORWARD\n' > "$case_dir/github-rules"
|
|
780
|
+
: > "$case_dir/gh-axi.log"
|
|
781
|
+
: > "$case_dir/gh.log"
|
|
782
|
+
|
|
783
|
+
set +e
|
|
784
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/70 \
|
|
785
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
786
|
+
rc=$?
|
|
787
|
+
set -e
|
|
788
|
+
|
|
789
|
+
expect_code 1 "$rc" "github-unrecognised-queue-method: an unproved merge must fail"
|
|
790
|
+
assert_grep 'base branch main requires the merge queue, but its configured merge method (FASTFORWARD) is not one this script recognises' \
|
|
791
|
+
"$case_dir/stderr" \
|
|
792
|
+
"github-unrecognised-queue-method: a readable queue rule produced no queue mention"
|
|
793
|
+
assert_no_grep 'retry with:' "$case_dir/stderr" \
|
|
794
|
+
"github-unrecognised-queue-method: retry flags were named for a method nothing recognises"
|
|
795
|
+
assert_no_grep '--auto --' "$case_dir/stderr" \
|
|
796
|
+
"github-unrecognised-queue-method: a merge method was guessed for the caller"
|
|
797
|
+
pass "fm-pr-merge names the queue requirement even when its method is unrecognised"
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
test_github_unreadable_queue_rules_are_not_reported_as_no_queue() {
|
|
801
|
+
local case_dir rc
|
|
802
|
+
case_dir=$(make_case github-unreadable-queue-rules)
|
|
803
|
+
mkdir -p "$case_dir/wt"
|
|
804
|
+
add_gh_mocks "$case_dir" 8484848484848484848484848484848484848484
|
|
805
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
806
|
+
cat > "$case_dir/fakebin/gh" <<'SH'
|
|
807
|
+
#!/usr/bin/env bash
|
|
808
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_LOG"
|
|
809
|
+
case "${1:-} ${2:-}" in
|
|
810
|
+
"pr view")
|
|
811
|
+
case " $* " in
|
|
812
|
+
*headRefOid*) printf '%s\n' 8484848484848484848484848484848484848484 ; exit 0 ;;
|
|
813
|
+
esac
|
|
814
|
+
;;
|
|
815
|
+
"api graphql")
|
|
816
|
+
cat "$FM_TEST_GH_OUTCOME"
|
|
817
|
+
exit 0
|
|
818
|
+
;;
|
|
819
|
+
api\ *) exit 1 ;;
|
|
820
|
+
esac
|
|
821
|
+
exit 0
|
|
822
|
+
SH
|
|
823
|
+
chmod +x "$case_dir/fakebin/gh"
|
|
824
|
+
: > "$case_dir/gh-axi.log"
|
|
825
|
+
: > "$case_dir/gh.log"
|
|
826
|
+
|
|
827
|
+
set +e
|
|
828
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/71 \
|
|
829
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
830
|
+
rc=$?
|
|
831
|
+
set -e
|
|
832
|
+
|
|
833
|
+
expect_code 1 "$rc" "github-unreadable-queue-rules: an unproved merge must fail"
|
|
834
|
+
assert_grep 'the branch rules for base branch main could not be read' "$case_dir/stderr" \
|
|
835
|
+
"github-unreadable-queue-rules: an unreadable rules response read like a queue-less base"
|
|
836
|
+
assert_no_grep 'retry with:' "$case_dir/stderr" \
|
|
837
|
+
"github-unreadable-queue-rules: retry flags were named from rules nothing could read"
|
|
838
|
+
pass "fm-pr-merge distinguishes unreadable branch rules from a base with no merge queue"
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
test_github_no_queue_rule_says_nothing_about_a_queue() {
|
|
842
|
+
local case_dir rc
|
|
843
|
+
case_dir=$(make_case github-no-queue-rule)
|
|
844
|
+
mkdir -p "$case_dir/wt"
|
|
845
|
+
add_gh_mocks "$case_dir" 8585858585858585858585858585858585858585
|
|
846
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
847
|
+
: > "$case_dir/github-rules"
|
|
848
|
+
: > "$case_dir/gh-axi.log"
|
|
849
|
+
: > "$case_dir/gh.log"
|
|
850
|
+
|
|
851
|
+
set +e
|
|
852
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/72 \
|
|
853
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
854
|
+
rc=$?
|
|
855
|
+
set -e
|
|
856
|
+
|
|
857
|
+
expect_code 1 "$rc" "github-no-queue-rule: an unproved merge must fail"
|
|
858
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
859
|
+
"github-no-queue-rule: refusal did not name the concrete observed state"
|
|
860
|
+
assert_no_grep 'merge queue' "$case_dir/stderr" \
|
|
861
|
+
"github-no-queue-rule: a base with no queue rule was told it requires the merge queue"
|
|
862
|
+
pass "fm-pr-merge says nothing about a merge queue when the base branch has no queue rule"
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
test_github_fallback_view_refusal_says_the_queue_was_unobservable() {
|
|
866
|
+
local case_dir ghless_path rc
|
|
867
|
+
case_dir=$(make_case github-fallback-unobservable-queue)
|
|
868
|
+
mkdir -p "$case_dir/wt"
|
|
869
|
+
add_gh_mocks "$case_dir" 8686868686868686868686868686868686868686
|
|
870
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
871
|
+
#!/usr/bin/env bash
|
|
872
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
873
|
+
case "${1:-} ${2:-}" in
|
|
874
|
+
"pr merge") printf 'merged:\n number: %s\n status: ok\n' "${3:-}" ;;
|
|
875
|
+
"pr view") printf 'pull_request:\n number: %s\n state: open\n' "$3" ;;
|
|
876
|
+
esac
|
|
877
|
+
exit 0
|
|
878
|
+
SH
|
|
879
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
880
|
+
rm "$case_dir/fakebin/gh"
|
|
881
|
+
ghless_path="$case_dir/path-without-gh"
|
|
882
|
+
mirror_path_without "$ghless_path" gh "$case_dir/fakebin"
|
|
883
|
+
: > "$case_dir/gh-axi.log"
|
|
884
|
+
|
|
885
|
+
set +e
|
|
886
|
+
PATH="$ghless_path" run_pr_merge "$case_dir" task-x1 \
|
|
887
|
+
https://github.com/example/repo/pull/73 -- --auto --merge \
|
|
888
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
889
|
+
rc=$?
|
|
890
|
+
set -e
|
|
891
|
+
|
|
892
|
+
expect_code 1 "$rc" "github-fallback-unobservable-queue: an unproved merge must fail"
|
|
893
|
+
assert_grep 'isInMergeQueue=unknown' "$case_dir/stderr" \
|
|
894
|
+
"github-fallback-unobservable-queue: refusal did not name the concrete observed state"
|
|
895
|
+
assert_grep 'the merge queue could not be observed for https://github.com/example/repo/pull/73' \
|
|
896
|
+
"$case_dir/stderr" \
|
|
897
|
+
"github-fallback-unobservable-queue: the refusal implied an unqueued PR it could not see"
|
|
898
|
+
assert_grep "re-check the pull request's merge queue state" "$case_dir/stderr" \
|
|
899
|
+
"github-fallback-unobservable-queue: the refusal named no concrete next step"
|
|
900
|
+
# The lowercase state the fallback view reports must be judged the same way
|
|
901
|
+
# the queue-aware read's uppercase enum is, or every explanation is skipped.
|
|
902
|
+
assert_grep 'auto-merge was requested and armed for https://github.com/example/repo/pull/73' \
|
|
903
|
+
"$case_dir/stderr" \
|
|
904
|
+
"github-fallback-unobservable-queue: the fallback view's state skipped the auto-merge explanation"
|
|
905
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
906
|
+
"github-fallback-unobservable-queue: an unproved merge was reported as verified"
|
|
907
|
+
pass "fm-pr-merge says the merge queue was unobservable when only the gh-axi view answered"
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
test_github_unreadable_outcome_refusal_quotes_the_forge_output() {
|
|
911
|
+
local case_dir rc
|
|
912
|
+
case_dir=$(make_case github-unreadable-outcome-quotes-forge)
|
|
913
|
+
mkdir -p "$case_dir/wt"
|
|
914
|
+
add_gh_mocks "$case_dir" 8787878787878787878787878787878787878787
|
|
915
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
916
|
+
#!/usr/bin/env bash
|
|
917
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
918
|
+
case "${1:-} ${2:-}" in
|
|
919
|
+
"pr merge") echo "will be added to the merge queue when all requirements are met" ;;
|
|
920
|
+
"pr view") exit 1 ;;
|
|
921
|
+
esac
|
|
922
|
+
exit 0
|
|
923
|
+
SH
|
|
924
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
925
|
+
add_gh_mock_outcome_read_fails "$case_dir" 8787878787878787878787878787878787878787
|
|
926
|
+
: > "$case_dir/gh-axi.log"
|
|
927
|
+
: > "$case_dir/gh.log"
|
|
928
|
+
|
|
929
|
+
set +e
|
|
930
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/74 \
|
|
931
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
932
|
+
rc=$?
|
|
933
|
+
set -e
|
|
934
|
+
|
|
935
|
+
expect_code 1 "$rc" "github-unreadable-outcome-quotes-forge: an unreadable outcome must fail"
|
|
936
|
+
assert_grep 'could not read the GitHub pull request outcome after the merge attempt' \
|
|
937
|
+
"$case_dir/stderr" \
|
|
938
|
+
"github-unreadable-outcome-quotes-forge: the unreadable outcome was not reported"
|
|
939
|
+
assert_grep 'error: > will be added to the merge queue when all requirements are met' \
|
|
940
|
+
"$case_dir/stderr" \
|
|
941
|
+
"github-unreadable-outcome-quotes-forge: the forge's only evidence was discarded"
|
|
942
|
+
! grep -qxF 'will be added to the merge queue when all requirements are met' \
|
|
943
|
+
"$case_dir/stderr" \
|
|
944
|
+
|| fail "github-unreadable-outcome-quotes-forge: forge text was emitted as the wrapper's own line"
|
|
945
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
946
|
+
"github-unreadable-outcome-quotes-forge: an unproved merge was reported as verified"
|
|
947
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
948
|
+
"github-unreadable-outcome-quotes-forge: the attempted merge lost its merge poll"
|
|
949
|
+
pass "fm-pr-merge quotes the forge output when it cannot read the outcome either"
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
test_github_failed_gh_read_falls_back_to_gh_axi() {
|
|
953
|
+
local case_dir rc
|
|
954
|
+
case_dir=$(make_case github-gh-read-falls-back)
|
|
955
|
+
mkdir -p "$case_dir/wt"
|
|
956
|
+
add_gh_mocks "$case_dir" 5151515151515151515151515151515151515151
|
|
957
|
+
add_gh_mock_outcome_read_fails "$case_dir" 5151515151515151515151515151515151515151
|
|
958
|
+
: > "$case_dir/gh-axi.log"
|
|
959
|
+
: > "$case_dir/gh.log"
|
|
960
|
+
|
|
961
|
+
set +e
|
|
962
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/63 \
|
|
963
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
964
|
+
rc=$?
|
|
965
|
+
set -e
|
|
966
|
+
|
|
967
|
+
expect_code 0 "$rc" "github-gh-read-falls-back: a merge the gh-axi view proves must succeed"
|
|
968
|
+
assert_grep 'pr view 63 --repo example/repo' "$case_dir/gh-axi.log" \
|
|
969
|
+
"github-gh-read-falls-back: the gh-axi view was never consulted after gh's read failed"
|
|
970
|
+
assert_grep 'verified: https://github.com/example/repo/pull/63 is merged' \
|
|
971
|
+
"$case_dir/stdout" "github-gh-read-falls-back: the proven merge was not reported"
|
|
972
|
+
assert_grep 'pr=https://github.com/example/repo/pull/63' "$case_dir/state/task-x1.meta" \
|
|
973
|
+
"github-gh-read-falls-back: the merged PR was not recorded for teardown"
|
|
974
|
+
pass "fm-pr-merge falls back to the gh-axi view when gh's read fails"
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
test_github_failed_merge_names_an_observed_landed_state() {
|
|
978
|
+
local case_dir rc
|
|
979
|
+
case_dir=$(make_case github-failed-merge-actually-landed)
|
|
980
|
+
mkdir -p "$case_dir/wt"
|
|
981
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
982
|
+
write_github_outcome "$case_dir" MERGED true false main
|
|
983
|
+
: > "$case_dir/gh-axi.log"
|
|
984
|
+
: > "$case_dir/gh.log"
|
|
985
|
+
|
|
986
|
+
set +e
|
|
987
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/64 \
|
|
988
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
989
|
+
rc=$?
|
|
990
|
+
set -e
|
|
991
|
+
|
|
992
|
+
expect_code 1 "$rc" "github-failed-merge-actually-landed: the forge failure must still fail the wrapper"
|
|
993
|
+
assert_grep 'error: pr merge failed' "$case_dir/stderr" \
|
|
994
|
+
"github-failed-merge-actually-landed: the original forge error was masked"
|
|
995
|
+
assert_grep 'state=MERGED, merged=true, isInMergeQueue=false' "$case_dir/stderr" \
|
|
996
|
+
"github-failed-merge-actually-landed: the observed landed state was never named"
|
|
997
|
+
assert_no_grep 'verified: ' "$case_dir/stdout" \
|
|
998
|
+
"github-failed-merge-actually-landed: a failed merge command was reported as verified"
|
|
999
|
+
assert_grep 'pr=https://github.com/example/repo/pull/64' "$case_dir/state/task-x1.meta" \
|
|
1000
|
+
"github-failed-merge-actually-landed: the landed PR lost its reference"
|
|
1001
|
+
pass "fm-pr-merge names a landed state hiding behind a failed GitHub merge command"
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
test_github_without_gh_still_uses_gh_axi_merge() {
|
|
1005
|
+
local case_dir ghless_path rc
|
|
1006
|
+
case_dir=$(make_case github-without-gh)
|
|
1007
|
+
mkdir -p "$case_dir/wt"
|
|
1008
|
+
add_gh_mocks "$case_dir" 4141414141414141414141414141414141414141
|
|
1009
|
+
rm "$case_dir/fakebin/gh"
|
|
1010
|
+
ghless_path="$case_dir/path-without-gh"
|
|
1011
|
+
mirror_path_without "$ghless_path" gh "$case_dir/fakebin"
|
|
1012
|
+
: > "$case_dir/gh-axi.log"
|
|
1013
|
+
|
|
1014
|
+
set +e
|
|
1015
|
+
PATH="$ghless_path" run_pr_merge "$case_dir" task-x1 \
|
|
1016
|
+
https://github.com/example/repo/pull/60 \
|
|
1017
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1018
|
+
rc=$?
|
|
1019
|
+
set -e
|
|
1020
|
+
|
|
1021
|
+
expect_code 0 "$rc" "github-without-gh: gh-axi can prove a landed merge without gh"
|
|
1022
|
+
assert_grep 'pr merge 60 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
1023
|
+
"github-without-gh: the configured merge abstraction was not invoked"
|
|
1024
|
+
assert_grep 'pr view 60 --repo example/repo' "$case_dir/gh-axi.log" \
|
|
1025
|
+
"github-without-gh: the gh-axi fallback did not verify the landed state"
|
|
1026
|
+
assert_grep 'verified: https://github.com/example/repo/pull/60 is merged' \
|
|
1027
|
+
"$case_dir/stdout" "github-without-gh: the fallback did not report the proven merge"
|
|
1028
|
+
pass "fm-pr-merge reaches and verifies the gh-axi merge path without gh"
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
test_github_without_gh_failed_read_keeps_bookkeeping() {
|
|
1032
|
+
local case_dir ghless_path rc
|
|
1033
|
+
case_dir=$(make_case github-without-gh-read-fails)
|
|
1034
|
+
mkdir -p "$case_dir/wt"
|
|
1035
|
+
cat > "$case_dir/fakebin/gh-axi" <<'SH'
|
|
1036
|
+
#!/usr/bin/env bash
|
|
1037
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
1038
|
+
case "${1:-} ${2:-}" in
|
|
1039
|
+
"pr merge") exit 0 ;;
|
|
1040
|
+
"pr view") exit 1 ;;
|
|
1041
|
+
esac
|
|
1042
|
+
exit 0
|
|
1043
|
+
SH
|
|
1044
|
+
chmod +x "$case_dir/fakebin/gh-axi"
|
|
1045
|
+
ghless_path="$case_dir/path-without-gh"
|
|
1046
|
+
mirror_path_without "$ghless_path" gh "$case_dir/fakebin"
|
|
1047
|
+
: > "$case_dir/gh-axi.log"
|
|
1048
|
+
|
|
1049
|
+
set +e
|
|
1050
|
+
PATH="$ghless_path" run_pr_merge "$case_dir" task-x1 \
|
|
1051
|
+
https://github.com/example/repo/pull/61 \
|
|
1052
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1053
|
+
rc=$?
|
|
1054
|
+
set -e
|
|
1055
|
+
|
|
1056
|
+
expect_code 1 "$rc" "github-without-gh-read-fails: an unreadable outcome must fail"
|
|
1057
|
+
assert_grep 'pr merge 61 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
1058
|
+
"github-without-gh-read-fails: the merge call did not happen before the failed read"
|
|
1059
|
+
assert_grep 'could not read the GitHub pull request outcome after the merge attempt' \
|
|
1060
|
+
"$case_dir/stderr" "github-without-gh-read-fails: the failed read was not reported"
|
|
1061
|
+
assert_grep 'pr=https://github.com/example/repo/pull/61' "$case_dir/state/task-x1.meta" \
|
|
1062
|
+
"github-without-gh-read-fails: a landed merge lost its PR metadata"
|
|
1063
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
1064
|
+
"github-without-gh-read-fails: a landed merge lost its merge poll"
|
|
1065
|
+
pass "fm-pr-merge preserves bookkeeping when gh is absent and the fallback read fails"
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
test_github_zero_exit_queue_required_refuses_with_exact_retry() {
|
|
1069
|
+
local case_dir rc
|
|
1070
|
+
case_dir=$(make_case github-zero-exit-queue-required)
|
|
1071
|
+
mkdir -p "$case_dir/wt"
|
|
1072
|
+
add_gh_mocks "$case_dir" 2121212121212121212121212121212121212121
|
|
1073
|
+
write_github_outcome "$case_dir" OPEN false false 'release/2026'
|
|
1074
|
+
printf 'merge_method=REBASE\n' > "$case_dir/github-rules"
|
|
1075
|
+
: > "$case_dir/gh-axi.log"
|
|
1076
|
+
: > "$case_dir/gh.log"
|
|
1077
|
+
|
|
1078
|
+
set +e
|
|
1079
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/56 \
|
|
1080
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1081
|
+
rc=$?
|
|
1082
|
+
set -e
|
|
1083
|
+
|
|
1084
|
+
expect_code 1 "$rc" "github-zero-exit-queue-required: an unproved merge must fail"
|
|
1085
|
+
assert_grep 'state=OPEN, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
1086
|
+
"github-zero-exit-queue-required: refusal did not name the concrete observed state"
|
|
1087
|
+
assert_grep 'base branch release/2026 requires the merge queue' "$case_dir/stderr" \
|
|
1088
|
+
"github-zero-exit-queue-required: refusal did not name the queue requirement"
|
|
1089
|
+
assert_grep '-- --auto --rebase' "$case_dir/stderr" \
|
|
1090
|
+
"github-zero-exit-queue-required: refusal did not name the exact compatible flags"
|
|
1091
|
+
assert_grep 'api --paginate repos/example/repo/rules/branches/release%2F2026' "$case_dir/gh.log" \
|
|
1092
|
+
"github-zero-exit-queue-required: queue rules were not read with pagination and encoded branch path"
|
|
1093
|
+
grep -qxF 'pr merge 56 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
1094
|
+
|| fail "github-zero-exit-queue-required: the attempted merge was changed unexpectedly"
|
|
1095
|
+
[ "$(wc -l < "$case_dir/gh-axi.log" | tr -d '[:space:]')" = 1 ] \
|
|
1096
|
+
|| fail "github-zero-exit-queue-required: the wrapper attempted more than one merge"
|
|
1097
|
+
assert_no_grep --auto "$case_dir/gh-axi.log" \
|
|
1098
|
+
"github-zero-exit-queue-required: queue flags were auto-applied to the attempted merge"
|
|
1099
|
+
assert_grep 'pr=https://github.com/example/repo/pull/56' "$case_dir/state/task-x1.meta" \
|
|
1100
|
+
"github-zero-exit-queue-required: the attempted merge lost its PR reference"
|
|
1101
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
1102
|
+
"github-zero-exit-queue-required: the attempted merge did not leave its poll armed"
|
|
1103
|
+
pass "fm-pr-merge reports exact queue retry flags after a zero-exit false success"
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
test_github_closed_unqueued_outcome_omits_retry_flags() {
|
|
1107
|
+
local case_dir rc
|
|
1108
|
+
case_dir=$(make_case github-closed-unqueued)
|
|
1109
|
+
mkdir -p "$case_dir/wt"
|
|
1110
|
+
add_gh_mocks "$case_dir" 2323232323232323232323232323232323232323
|
|
1111
|
+
write_github_outcome "$case_dir" CLOSED false false master
|
|
1112
|
+
printf 'merge_method=MERGE\n' > "$case_dir/github-rules"
|
|
1113
|
+
: > "$case_dir/gh-axi.log"
|
|
1114
|
+
: > "$case_dir/gh.log"
|
|
1115
|
+
|
|
1116
|
+
set +e
|
|
1117
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/57 \
|
|
1118
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1119
|
+
rc=$?
|
|
1120
|
+
set -e
|
|
1121
|
+
|
|
1122
|
+
expect_code 1 "$rc" "github-closed-unqueued: an unproved merge must fail"
|
|
1123
|
+
assert_grep 'state=CLOSED, merged=false, isInMergeQueue=false' "$case_dir/stderr" \
|
|
1124
|
+
"github-closed-unqueued: refusal did not name the concrete observed state"
|
|
1125
|
+
assert_no_grep 'requires the merge queue' "$case_dir/stderr" \
|
|
1126
|
+
"github-closed-unqueued: closed PR received unusable queue guidance"
|
|
1127
|
+
assert_no_grep '-- --auto --merge' "$case_dir/stderr" \
|
|
1128
|
+
"github-closed-unqueued: closed PR received retry flags"
|
|
1129
|
+
assert_grep 'pr=https://github.com/example/repo/pull/57' "$case_dir/state/task-x1.meta" \
|
|
1130
|
+
"github-closed-unqueued: the attempted merge lost its PR reference"
|
|
1131
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
1132
|
+
"github-closed-unqueued: the attempted merge did not leave its poll armed"
|
|
1133
|
+
pass "fm-pr-merge omits merge-queue retry guidance for a closed GitHub PR"
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
test_github_queued_outcome_is_verified() {
|
|
1137
|
+
local case_dir rc
|
|
1138
|
+
case_dir=$(make_case github-verified-queued)
|
|
1139
|
+
mkdir -p "$case_dir/wt"
|
|
1140
|
+
add_gh_mocks "$case_dir" 3030303030303030303030303030303030303030
|
|
1141
|
+
write_github_outcome "$case_dir" OPEN false true master
|
|
1142
|
+
: > "$case_dir/gh-axi.log"
|
|
1143
|
+
: > "$case_dir/gh.log"
|
|
1144
|
+
|
|
1145
|
+
set +e
|
|
1146
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/53 -- --auto --merge \
|
|
1147
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1148
|
+
rc=$?
|
|
1149
|
+
set -e
|
|
1150
|
+
|
|
1151
|
+
expect_code 0 "$rc" "github-verified-queued: a queued PR should succeed"
|
|
1152
|
+
assert_grep 'verified: https://github.com/example/repo/pull/53 is queued' \
|
|
1153
|
+
"$case_dir/stdout" "github-verified-queued: success was not reported as queued"
|
|
1154
|
+
assert_no_grep 'merged:' "$case_dir/stdout" \
|
|
1155
|
+
"github-verified-queued: the forge CLI's unverified merged report leaked through"
|
|
1156
|
+
assert_grep 'pr=https://github.com/example/repo/pull/53' "$case_dir/state/task-x1.meta" \
|
|
1157
|
+
"github-verified-queued: the queued PR was not recorded for teardown"
|
|
1158
|
+
pass "fm-pr-merge accepts and accurately reports a GitHub merge-queue entry"
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
test_github_queue_required_refusal_names_retry_flags() {
|
|
1162
|
+
local case_dir rc
|
|
1163
|
+
case_dir=$(make_case github-queue-required)
|
|
1164
|
+
mkdir -p "$case_dir/wt"
|
|
1165
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
1166
|
+
write_github_outcome "$case_dir" OPEN false false master
|
|
1167
|
+
printf 'merge_method=MERGE\n' > "$case_dir/github-rules"
|
|
1168
|
+
: > "$case_dir/gh-axi.log"
|
|
1169
|
+
: > "$case_dir/gh.log"
|
|
1170
|
+
|
|
1171
|
+
set +e
|
|
1172
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/54 \
|
|
1173
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1174
|
+
rc=$?
|
|
1175
|
+
set -e
|
|
1176
|
+
|
|
1177
|
+
expect_code 1 "$rc" "github-queue-required: an incompatible direct merge must fail"
|
|
1178
|
+
assert_grep 'error: pr merge failed' "$case_dir/stderr" \
|
|
1179
|
+
"github-queue-required: the original forge failure was not preserved"
|
|
1180
|
+
assert_grep 'base branch master requires the merge queue' "$case_dir/stderr" \
|
|
1181
|
+
"github-queue-required: refusal did not name the queue requirement"
|
|
1182
|
+
grep -F -- '-- --auto --merge' "$case_dir/stderr" >/dev/null \
|
|
1183
|
+
|| fail "github-queue-required: refusal did not name the exact compatible flags"
|
|
1184
|
+
grep -qxF 'pr merge 54 --repo example/repo --squash' "$case_dir/gh-axi.log" \
|
|
1185
|
+
|| fail "github-queue-required: the wrapper silently changed the attempted merge semantics"
|
|
1186
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
1187
|
+
"github-queue-required: the failed forge call did not leave the merge poll armed"
|
|
1188
|
+
pass "fm-pr-merge explains how to retry with the required GitHub merge queue method"
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
test_github_agreeing_queue_rules_keep_retry_guidance() {
|
|
1192
|
+
local case_dir rc
|
|
1193
|
+
case_dir=$(make_case github-agreeing-queue-rules)
|
|
1194
|
+
mkdir -p "$case_dir/wt"
|
|
1195
|
+
add_gh_mocks "$case_dir" 2424242424242424242424242424242424242424
|
|
1196
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
1197
|
+
printf 'merge_method=REBASE\nmerge_method=REBASE\n' > "$case_dir/github-rules"
|
|
1198
|
+
: > "$case_dir/gh-axi.log"
|
|
1199
|
+
: > "$case_dir/gh.log"
|
|
1200
|
+
|
|
1201
|
+
set +e
|
|
1202
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/58 \
|
|
1203
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1204
|
+
rc=$?
|
|
1205
|
+
set -e
|
|
1206
|
+
|
|
1207
|
+
expect_code 1 "$rc" "github-agreeing-queue-rules: an unproved merge must fail"
|
|
1208
|
+
assert_grep 'base branch main requires the merge queue' "$case_dir/stderr" \
|
|
1209
|
+
"github-agreeing-queue-rules: refusal did not name the queue requirement"
|
|
1210
|
+
assert_grep '-- --auto --rebase' "$case_dir/stderr" \
|
|
1211
|
+
"github-agreeing-queue-rules: agreeing rules omitted exact retry flags"
|
|
1212
|
+
assert_no_grep 'exact retry flags are ambiguous' "$case_dir/stderr" \
|
|
1213
|
+
"github-agreeing-queue-rules: agreeing rules were reported as ambiguous"
|
|
1214
|
+
pass "fm-pr-merge aggregates agreeing merge-queue rules"
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
test_github_conflicting_queue_rules_report_ambiguity() {
|
|
1218
|
+
local case_dir rc
|
|
1219
|
+
case_dir=$(make_case github-conflicting-queue-rules)
|
|
1220
|
+
mkdir -p "$case_dir/wt"
|
|
1221
|
+
add_gh_mocks "$case_dir" 2525252525252525252525252525252525252525
|
|
1222
|
+
write_github_outcome "$case_dir" OPEN false false main
|
|
1223
|
+
printf 'merge_method=MERGE\nmerge_method=SQUASH\nmerge_method=SQUASH\n' \
|
|
1224
|
+
> "$case_dir/github-rules"
|
|
1225
|
+
: > "$case_dir/gh-axi.log"
|
|
1226
|
+
: > "$case_dir/gh.log"
|
|
1227
|
+
|
|
1228
|
+
set +e
|
|
1229
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/59 \
|
|
1230
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1231
|
+
rc=$?
|
|
1232
|
+
set -e
|
|
1233
|
+
|
|
1234
|
+
expect_code 1 "$rc" "github-conflicting-queue-rules: an unproved merge must fail"
|
|
1235
|
+
assert_grep 'base branch main has conflicting merge queue methods (MERGE, SQUASH)' \
|
|
1236
|
+
"$case_dir/stderr" \
|
|
1237
|
+
"github-conflicting-queue-rules: conflicting methods were not named"
|
|
1238
|
+
assert_no_grep '-- --auto --merge' "$case_dir/stderr" \
|
|
1239
|
+
"github-conflicting-queue-rules: an exact retry method was guessed"
|
|
1240
|
+
assert_no_grep '-- --auto --squash' "$case_dir/stderr" \
|
|
1241
|
+
"github-conflicting-queue-rules: an exact retry method was guessed"
|
|
1242
|
+
assert_no_grep 'SQUASH, SQUASH' "$case_dir/stderr" \
|
|
1243
|
+
"github-conflicting-queue-rules: a repeated queue method was named twice"
|
|
1244
|
+
pass "fm-pr-merge reports ambiguity for conflicting merge-queue rules"
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
test_extra_merge_args_forwarded() {
|
|
1248
|
+
local case_dir rc
|
|
1249
|
+
case_dir=$(make_case extra-args)
|
|
1250
|
+
mkdir -p "$case_dir/wt"
|
|
1251
|
+
add_gh_mocks "$case_dir" 2222222222222222222222222222222222222222
|
|
1252
|
+
: > "$case_dir/gh-axi.log"
|
|
1253
|
+
|
|
1254
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/15 -- --squash --delete-branch \
|
|
1255
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" || fail "extra-args: fm-pr-merge failed"
|
|
1256
|
+
|
|
1257
|
+
grep -qxF 'pr merge 15 --repo example/repo --squash --delete-branch' "$case_dir/gh-axi.log" \
|
|
1258
|
+
|| fail "extra-args: extra gh-axi pr merge flags were not forwarded"
|
|
1259
|
+
pass "fm-pr-merge forwards extra flags to gh-axi pr merge after the -- separator"
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
test_missing_meta_refuses_before_merge() {
|
|
1263
|
+
local case_dir fakebin rc
|
|
1264
|
+
case_dir="$TMP_ROOT/missing-meta"
|
|
1265
|
+
fakebin="$case_dir/fakebin"
|
|
1266
|
+
mkdir -p "$case_dir/state" "$fakebin"
|
|
1267
|
+
add_gh_mocks "$case_dir" 3333333333333333333333333333333333333333
|
|
1268
|
+
: > "$case_dir/gh-axi.log"
|
|
1269
|
+
|
|
1270
|
+
set +e
|
|
1271
|
+
run_pr_merge "$case_dir" missing-x1 https://github.com/example/repo/pull/21 \
|
|
1272
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1273
|
+
rc=$?
|
|
1274
|
+
set -e
|
|
1275
|
+
|
|
1276
|
+
expect_code 1 "$rc" "missing-meta: fm-pr-merge should refuse"
|
|
1277
|
+
assert_grep 'error: task metadata is unavailable' "$case_dir/stderr" \
|
|
1278
|
+
"missing-meta: refusal did not explain missing meta"
|
|
1279
|
+
[ ! -s "$case_dir/gh-axi.log" ] || fail "missing-meta: gh-axi pr merge was invoked"
|
|
1280
|
+
assert_absent "$case_dir/state/missing-x1.check.sh" \
|
|
1281
|
+
"missing-meta: fm-pr-check should not arm a poll for an unknown task"
|
|
1282
|
+
pass "fm-pr-merge refuses before merging when task meta is missing"
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
test_malformed_url_refuses_before_merge() {
|
|
1286
|
+
local case_dir rc
|
|
1287
|
+
case_dir=$(make_case malformed-url)
|
|
1288
|
+
mkdir -p "$case_dir/wt"
|
|
1289
|
+
add_gh_mocks "$case_dir" 4444444444444444444444444444444444444444
|
|
1290
|
+
: > "$case_dir/gh-axi.log"
|
|
1291
|
+
|
|
1292
|
+
set +e
|
|
1293
|
+
# A near-miss GitLab URL: one namespace segment where a project needs at
|
|
1294
|
+
# least two. A well-formed merge request URL is merged now, so the refusal
|
|
1295
|
+
# has to be proven on a URL that genuinely does not parse.
|
|
1296
|
+
run_pr_merge "$case_dir" task-x1 'https://gitlab.com/example/-/merge_requests/1' \
|
|
1297
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1298
|
+
rc=$?
|
|
1299
|
+
set -e
|
|
1300
|
+
|
|
1301
|
+
expect_code 2 "$rc" "malformed-url: fm-pr-merge should refuse a malformed merge request URL"
|
|
1302
|
+
assert_grep 'error: invalid PR merge request' "$case_dir/stderr" \
|
|
1303
|
+
"malformed-url: refusal was not fixed and non-probing"
|
|
1304
|
+
assert_no_grep 'pr=https://gitlab.com/example/-/merge_requests/1' "$case_dir/state/task-x1.meta" \
|
|
1305
|
+
"malformed-url: malformed PR URL was recorded in meta"
|
|
1306
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1307
|
+
"malformed-url: malformed PR URL armed a merge poll"
|
|
1308
|
+
assert_no_grep 'pr merge' "$case_dir/gh-axi.log" \
|
|
1309
|
+
"malformed-url: gh-axi pr merge was invoked for a malformed URL"
|
|
1310
|
+
pass "fm-pr-merge refuses malformed PR URLs before calling gh-axi"
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
test_rejects_unsafe_url_segments_before_recording() {
|
|
1314
|
+
local case_dir rc
|
|
1315
|
+
case_dir=$(make_case unsafe-url-segment)
|
|
1316
|
+
mkdir -p "$case_dir/wt"
|
|
1317
|
+
add_gh_mocks "$case_dir" 8888888888888888888888888888888888888888
|
|
1318
|
+
: > "$case_dir/gh-axi.log"
|
|
1319
|
+
|
|
1320
|
+
set +e
|
|
1321
|
+
# shellcheck disable=SC2016 # Literal command substitution probes URL parsing safety.
|
|
1322
|
+
run_pr_merge "$case_dir" task-x1 'https://github.com/evil$(echo pwned)/repo/pull/7' \
|
|
1323
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1324
|
+
rc=$?
|
|
1325
|
+
set -e
|
|
1326
|
+
|
|
1327
|
+
expect_code 1 "$rc" "unsafe-url-segment: fm-pr-merge should refuse unsafe owner/repo characters"
|
|
1328
|
+
assert_grep 'PR URL must match https://github.com/<owner>/<repo>/pull/<number>' "$case_dir/stderr" \
|
|
1329
|
+
"unsafe-url-segment: refusal did not explain the expected URL shape"
|
|
1330
|
+
# shellcheck disable=SC2016 # Literal command substitution must not reach meta.
|
|
1331
|
+
assert_no_grep 'pr=https://github.com/evil$(echo pwned)/repo/pull/7' "$case_dir/state/task-x1.meta" \
|
|
1332
|
+
"unsafe-url-segment: unsafe PR URL was recorded in meta"
|
|
1333
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1334
|
+
"unsafe-url-segment: unsafe PR URL armed a merge poll"
|
|
1335
|
+
assert_no_grep 'pr merge' "$case_dir/gh-axi.log" \
|
|
1336
|
+
"unsafe-url-segment: gh-axi pr merge was invoked for an unsafe URL"
|
|
1337
|
+
pass "fm-pr-merge refuses unsafe PR URL segments before recording state"
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
test_repo_override_args_refuse_before_recording() {
|
|
1341
|
+
local case_dir rc
|
|
1342
|
+
case_dir=$(make_case repo-override)
|
|
1343
|
+
mkdir -p "$case_dir/wt"
|
|
1344
|
+
add_gh_mocks "$case_dir" 9999999999999999999999999999999999999999
|
|
1345
|
+
: > "$case_dir/gh-axi.log"
|
|
1346
|
+
|
|
1347
|
+
set +e
|
|
1348
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/right/repo/pull/5 -- --repo wrong/repo \
|
|
1349
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1350
|
+
rc=$?
|
|
1351
|
+
set -e
|
|
1352
|
+
|
|
1353
|
+
expect_code 1 "$rc" "repo-override: fm-pr-merge should refuse repo override flags"
|
|
1354
|
+
assert_grep 'extra merge arguments must not override the repository' "$case_dir/stderr" \
|
|
1355
|
+
"repo-override: refusal did not explain the repo override"
|
|
1356
|
+
assert_no_grep 'pr=https://github.com/right/repo/pull/5' "$case_dir/state/task-x1.meta" \
|
|
1357
|
+
"repo-override: PR URL was recorded before rejecting repo override"
|
|
1358
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1359
|
+
"repo-override: repo override armed a merge poll"
|
|
1360
|
+
assert_no_grep 'pr merge' "$case_dir/gh-axi.log" \
|
|
1361
|
+
"repo-override: gh-axi pr merge was invoked despite repo override"
|
|
1362
|
+
pass "fm-pr-merge refuses repo override args before recording state"
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
# A bundled short-option cluster carries -R without ever being exactly -R, and
|
|
1366
|
+
# both CLIs expand it one character at a time, so the guard has to read the
|
|
1367
|
+
# whole cluster. On GitLab that redirect names an instance, not only a
|
|
1368
|
+
# repository, so it must refuse before anything is recorded or read.
|
|
1369
|
+
test_bundled_repo_override_args_refuse_before_recording() {
|
|
1370
|
+
local case_dir rc
|
|
1371
|
+
case_dir=$(make_case bundled-repo-override)
|
|
1372
|
+
mkdir -p "$case_dir/wt"
|
|
1373
|
+
add_gh_mocks "$case_dir" abababababababababababababababababababab
|
|
1374
|
+
: > "$case_dir/gh-axi.log"
|
|
1375
|
+
|
|
1376
|
+
set +e
|
|
1377
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/right/repo/pull/6 -- -dR wrong/repo \
|
|
1378
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1379
|
+
rc=$?
|
|
1380
|
+
set -e
|
|
1381
|
+
|
|
1382
|
+
expect_code 1 "$rc" "bundled-repo-override: fm-pr-merge should refuse a bundled repo override"
|
|
1383
|
+
assert_grep 'extra merge arguments must not override the repository' "$case_dir/stderr" \
|
|
1384
|
+
"bundled-repo-override: refusal did not explain the repo override"
|
|
1385
|
+
assert_no_grep 'pr=https://github.com/right/repo/pull/6' "$case_dir/state/task-x1.meta" \
|
|
1386
|
+
"bundled-repo-override: PR URL was recorded before rejecting the bundled repo override"
|
|
1387
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1388
|
+
"bundled-repo-override: a bundled repo override armed a merge poll"
|
|
1389
|
+
assert_no_grep 'pr merge' "$case_dir/gh-axi.log" \
|
|
1390
|
+
"bundled-repo-override: gh-axi pr merge was invoked despite the bundled repo override"
|
|
1391
|
+
|
|
1392
|
+
case_dir=$(make_gitlab_case bundled-repo-override-gitlab)
|
|
1393
|
+
|
|
1394
|
+
set +e
|
|
1395
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" -- -yR https://other.example/g/p \
|
|
1396
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1397
|
+
rc=$?
|
|
1398
|
+
set -e
|
|
1399
|
+
|
|
1400
|
+
expect_code 1 "$rc" "bundled-repo-override-gitlab: fm-pr-merge should refuse a bundled instance override"
|
|
1401
|
+
assert_grep 'extra merge arguments must not override the repository' "$case_dir/stderr" \
|
|
1402
|
+
"bundled-repo-override-gitlab: refusal did not explain the repo override"
|
|
1403
|
+
assert_no_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1404
|
+
"bundled-repo-override-gitlab: the URL was recorded before rejecting the bundled override"
|
|
1405
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1406
|
+
"bundled-repo-override-gitlab: a bundled override armed a merge poll"
|
|
1407
|
+
[ ! -s "$case_dir/glab.log" ] \
|
|
1408
|
+
|| fail "bundled-repo-override-gitlab: glab was invoked despite the bundled override"
|
|
1409
|
+
|
|
1410
|
+
# Only a cluster carrying the repository flag is refused: every other short
|
|
1411
|
+
# cluster is still the caller's business and still reaches the forge.
|
|
1412
|
+
case_dir=$(make_case bundled-non-repo-cluster)
|
|
1413
|
+
mkdir -p "$case_dir/wt"
|
|
1414
|
+
add_gh_mocks "$case_dir" bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc
|
|
1415
|
+
: > "$case_dir/gh-axi.log"
|
|
1416
|
+
|
|
1417
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/8 -- -d \
|
|
1418
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" \
|
|
1419
|
+
|| fail "bundled-non-repo-cluster: fm-pr-merge refused a short flag that overrides nothing"
|
|
1420
|
+
|
|
1421
|
+
grep -qxF 'pr merge 8 --repo example/repo --squash -d' "$case_dir/gh-axi.log" \
|
|
1422
|
+
|| fail "bundled-non-repo-cluster: a short flag carrying no repository override was not forwarded"
|
|
1423
|
+
pass "fm-pr-merge refuses a bundled short-option repo override and forwards other short flags"
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
test_explicit_merge_method_not_overridden() {
|
|
1427
|
+
local case_dir
|
|
1428
|
+
case_dir=$(make_case explicit-merge-method)
|
|
1429
|
+
mkdir -p "$case_dir/wt"
|
|
1430
|
+
add_gh_mocks "$case_dir" 5555555555555555555555555555555555555555
|
|
1431
|
+
: > "$case_dir/gh-axi.log"
|
|
1432
|
+
|
|
1433
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/22 -- --merge \
|
|
1434
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" || fail "explicit-merge-method: fm-pr-merge failed"
|
|
1435
|
+
|
|
1436
|
+
grep -qxF 'pr merge 22 --repo example/repo --merge' "$case_dir/gh-axi.log" \
|
|
1437
|
+
|| fail "explicit-merge-method: caller --merge was not forwarded without an extra default --squash"
|
|
1438
|
+
pass "fm-pr-merge does not add default --squash when the caller passes an explicit merge method"
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
test_method_equals_merge_method_not_overridden() {
|
|
1442
|
+
local case_dir
|
|
1443
|
+
case_dir=$(make_case method-equals-merge-method)
|
|
1444
|
+
mkdir -p "$case_dir/wt"
|
|
1445
|
+
add_gh_mocks "$case_dir" 7777777777777777777777777777777777777777
|
|
1446
|
+
: > "$case_dir/gh-axi.log"
|
|
1447
|
+
|
|
1448
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/23 -- --method=merge \
|
|
1449
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" || fail "method-equals-merge-method: fm-pr-merge failed"
|
|
1450
|
+
|
|
1451
|
+
grep -qxF 'pr merge 23 --repo example/repo --method=merge' "$case_dir/gh-axi.log" \
|
|
1452
|
+
|| fail "method-equals-merge-method: caller --method=merge was not forwarded without an extra default --squash"
|
|
1453
|
+
pass "fm-pr-merge respects --method=<value> as an explicit merge method"
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
test_parses_pr_url_for_gh_axi() {
|
|
1457
|
+
local case_dir
|
|
1458
|
+
case_dir=$(make_case url-parsing)
|
|
1459
|
+
mkdir -p "$case_dir/wt"
|
|
1460
|
+
add_gh_mocks "$case_dir" 6666666666666666666666666666666666666666
|
|
1461
|
+
: > "$case_dir/gh-axi.log"
|
|
1462
|
+
|
|
1463
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/my-org/my-repo/pull/126 \
|
|
1464
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" || fail "url-parsing: fm-pr-merge failed"
|
|
1465
|
+
|
|
1466
|
+
grep -qxF 'pr merge 126 --repo my-org/my-repo --squash' "$case_dir/gh-axi.log" \
|
|
1467
|
+
|| fail "url-parsing: gh-axi pr merge was not invoked as number + --repo + default --squash"
|
|
1468
|
+
pass "fm-pr-merge parses a GitHub PR URL into gh-axi number and --repo arguments"
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
test_gitlab_url_resolves_and_merges() {
|
|
1472
|
+
local case_dir rc merge_line
|
|
1473
|
+
case_dir=$(make_gitlab_case gitlab-merges)
|
|
1474
|
+
|
|
1475
|
+
set +e
|
|
1476
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1477
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1478
|
+
rc=$?
|
|
1479
|
+
set -e
|
|
1480
|
+
|
|
1481
|
+
expect_code 0 "$rc" "gitlab-merges: a well-formed merge request URL should merge, not error"
|
|
1482
|
+
assert_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1483
|
+
"gitlab-merges: pr= was not recorded before merging"
|
|
1484
|
+
assert_grep "GITLAB_HOST=$MR_HOST mr view 7 -R $MR_PROJECT_URL -F json" "$case_dir/glab.log" \
|
|
1485
|
+
"gitlab-merges: the pre-merge state was not read from the project URL"
|
|
1486
|
+
merge_line=$(glab_merge_line "$case_dir/glab.log")
|
|
1487
|
+
[ "$merge_line" = "GITLAB_HOST=$MR_HOST mr merge 7 -R $MR_PROJECT_URL --sha $MR_HEAD --yes" ] \
|
|
1488
|
+
|| fail "gitlab-merges: unexpected merge invocation: '$merge_line'"
|
|
1489
|
+
assert_grep "successful pipeline at head $MR_HEAD" "$case_dir/stderr" \
|
|
1490
|
+
"gitlab-merges: the verified head was not reported"
|
|
1491
|
+
[ ! -s "$case_dir/gh-axi.log" ] || fail "gitlab-merges: a merge request reached the GitHub CLI"
|
|
1492
|
+
pass "fm-pr-merge merges a GitLab merge request through glab instead of refusing it"
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
test_gitlab_host_comes_from_the_url() {
|
|
1496
|
+
local case_dir rc host path project_url url
|
|
1497
|
+
host=gl.self-hosted.example
|
|
1498
|
+
path=deep/nested/group/project
|
|
1499
|
+
project_url="https://$host/$path"
|
|
1500
|
+
url="$project_url/-/merge_requests/31"
|
|
1501
|
+
case_dir=$(make_gitlab_case gitlab-host-from-url)
|
|
1502
|
+
|
|
1503
|
+
set +e
|
|
1504
|
+
run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1505
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1506
|
+
rc=$?
|
|
1507
|
+
set -e
|
|
1508
|
+
|
|
1509
|
+
expect_code 0 "$rc" "gitlab-host-from-url: a self-hosted merge request should merge"
|
|
1510
|
+
assert_grep "GITLAB_HOST=$host mr view 31 -R $project_url -F json" "$case_dir/glab.log" \
|
|
1511
|
+
"gitlab-host-from-url: the read did not use the host from the URL"
|
|
1512
|
+
assert_grep "GITLAB_HOST=$host mr merge 31 -R $project_url" "$case_dir/glab.log" \
|
|
1513
|
+
"gitlab-host-from-url: the merge did not use the host from the URL"
|
|
1514
|
+
assert_no_grep 'gitlab.com' "$case_dir/glab.log" \
|
|
1515
|
+
"gitlab-host-from-url: a host was assumed instead of taken from the URL"
|
|
1516
|
+
assert_no_grep '<unset>' "$case_dir/glab.log" \
|
|
1517
|
+
"gitlab-host-from-url: glab was left to resolve the instance from its own default"
|
|
1518
|
+
pass "fm-pr-merge takes the GitLab instance from the URL rather than assuming one"
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
test_gitlab_imposes_no_merge_method() {
|
|
1522
|
+
local case_dir rc merge_line flag
|
|
1523
|
+
case_dir=$(make_gitlab_case gitlab-no-method)
|
|
1524
|
+
|
|
1525
|
+
set +e
|
|
1526
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1527
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1528
|
+
rc=$?
|
|
1529
|
+
set -e
|
|
1530
|
+
|
|
1531
|
+
expect_code 0 "$rc" "gitlab-no-method: merge should succeed"
|
|
1532
|
+
merge_line=$(glab_merge_line "$case_dir/glab.log")
|
|
1533
|
+
for flag in --squash --rebase --merge --method; do
|
|
1534
|
+
case "$merge_line" in
|
|
1535
|
+
*"$flag"*) fail "gitlab-no-method: '$flag' was imposed on GitLab: '$merge_line'" ;;
|
|
1536
|
+
esac
|
|
1537
|
+
done
|
|
1538
|
+
pass "fm-pr-merge imposes no merge method on GitLab, leaving the project's own one"
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
test_gitlab_extra_args_forwarded() {
|
|
1542
|
+
local case_dir rc merge_line
|
|
1543
|
+
case_dir=$(make_gitlab_case gitlab-extra-args)
|
|
1544
|
+
|
|
1545
|
+
set +e
|
|
1546
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" -- --remove-source-branch \
|
|
1547
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1548
|
+
rc=$?
|
|
1549
|
+
set -e
|
|
1550
|
+
|
|
1551
|
+
expect_code 0 "$rc" "gitlab-extra-args: merge should succeed"
|
|
1552
|
+
merge_line=$(glab_merge_line "$case_dir/glab.log")
|
|
1553
|
+
[ "$merge_line" = "GITLAB_HOST=$MR_HOST mr merge 7 -R $MR_PROJECT_URL --sha $MR_HEAD --yes --remove-source-branch" ] \
|
|
1554
|
+
|| fail "gitlab-extra-args: extra glab flags were not forwarded: '$merge_line'"
|
|
1555
|
+
pass "fm-pr-merge forwards extra flags to glab mr merge after the -- separator"
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
test_gitlab_merge_failure_propagates() {
|
|
1559
|
+
local case_dir rc
|
|
1560
|
+
case_dir=$(make_gitlab_case gitlab-merge-fails)
|
|
1561
|
+
: > "$case_dir/glab-merge-fails"
|
|
1562
|
+
|
|
1563
|
+
set +e
|
|
1564
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1565
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1566
|
+
rc=$?
|
|
1567
|
+
set -e
|
|
1568
|
+
|
|
1569
|
+
expect_code 1 "$rc" "gitlab-merge-fails: a failing glab merge should not report success"
|
|
1570
|
+
assert_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1571
|
+
"gitlab-merge-fails: pr= should already be recorded even though the merge failed"
|
|
1572
|
+
pass "fm-pr-merge propagates a real glab merge failure without silently succeeding"
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
# Each pre-merge condition, driven one at a time, so no condition can be
|
|
1576
|
+
# carried by another. The refusal names that condition, no merge is attempted,
|
|
1577
|
+
# and pr= is still recorded and the poll still armed exactly as the GitHub path
|
|
1578
|
+
# leaves them when gh-axi itself fails.
|
|
1579
|
+
test_gitlab_each_condition_refuses_independently() {
|
|
1580
|
+
local case_dir rc name expected spec
|
|
1581
|
+
set -- \
|
|
1582
|
+
"state|state=closed|state is \"closed\", not open" \
|
|
1583
|
+
"detail|detail=need_rebase|detailed_merge_status is \"need_rebase\", not mergeable" \
|
|
1584
|
+
"conflicts|conflicts=true|has_conflicts is \"true\", not false" \
|
|
1585
|
+
"discussions|discussions=false|blocking_discussions_resolved is \"false\", not true" \
|
|
1586
|
+
"pipeline-status|pipeline_status=failed|the head pipeline status is \"failed\", not success" \
|
|
1587
|
+
"pipeline-sha|pipeline_sha=$MR_STALE_HEAD|the head pipeline ran at \"$MR_STALE_HEAD\", not at the current head $MR_HEAD" \
|
|
1588
|
+
"no-pipeline|pipeline=null|the head pipeline status is \"none\", not success"
|
|
1589
|
+
for spec in "$@"; do
|
|
1590
|
+
name=${spec%%|*}
|
|
1591
|
+
expected=${spec##*|}
|
|
1592
|
+
spec=${spec#*|}
|
|
1593
|
+
case_dir=$(make_gitlab_case "gitlab-refuse-$name" "${spec%%|*}")
|
|
1594
|
+
|
|
1595
|
+
set +e
|
|
1596
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1597
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1598
|
+
rc=$?
|
|
1599
|
+
set -e
|
|
1600
|
+
|
|
1601
|
+
expect_code 1 "$rc" "gitlab-refuse-$name: fm-pr-merge should refuse"
|
|
1602
|
+
assert_grep "error: refusing to merge $MR_URL" "$case_dir/stderr" \
|
|
1603
|
+
"gitlab-refuse-$name: refusal did not name the merge request"
|
|
1604
|
+
assert_grep "$expected" "$case_dir/stderr" \
|
|
1605
|
+
"gitlab-refuse-$name: refusal did not name the failing condition"
|
|
1606
|
+
[ -z "$(glab_merge_line "$case_dir/glab.log")" ] \
|
|
1607
|
+
|| fail "gitlab-refuse-$name: a merge was attempted despite the refusal"
|
|
1608
|
+
assert_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1609
|
+
"gitlab-refuse-$name: a refusal should still leave the recorded PR reference"
|
|
1610
|
+
assert_present "$case_dir/state/task-x1.check.sh" \
|
|
1611
|
+
"gitlab-refuse-$name: a refusal should still leave the merge poll armed"
|
|
1612
|
+
done
|
|
1613
|
+
pass "fm-pr-merge refuses on each GitLab pre-merge condition independently"
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
test_gitlab_reports_every_failing_condition() {
|
|
1617
|
+
local case_dir rc expected
|
|
1618
|
+
case_dir=$(make_gitlab_case gitlab-refuse-all \
|
|
1619
|
+
state=closed detail=conflict conflicts=true discussions=false \
|
|
1620
|
+
pipeline_status=failed "pipeline_sha=$MR_STALE_HEAD")
|
|
1621
|
+
|
|
1622
|
+
set +e
|
|
1623
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1624
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1625
|
+
rc=$?
|
|
1626
|
+
set -e
|
|
1627
|
+
|
|
1628
|
+
expect_code 1 "$rc" "gitlab-refuse-all: fm-pr-merge should refuse"
|
|
1629
|
+
for expected in \
|
|
1630
|
+
'state is "closed", not open' \
|
|
1631
|
+
'detailed_merge_status is "conflict", not mergeable' \
|
|
1632
|
+
'has_conflicts is "true", not false' \
|
|
1633
|
+
'blocking_discussions_resolved is "false", not true' \
|
|
1634
|
+
'the head pipeline status is "failed", not success' \
|
|
1635
|
+
"the head pipeline ran at \"$MR_STALE_HEAD\", not at the current head $MR_HEAD"
|
|
1636
|
+
do
|
|
1637
|
+
assert_grep "$expected" "$case_dir/stderr" \
|
|
1638
|
+
"gitlab-refuse-all: '$expected' was not reported"
|
|
1639
|
+
done
|
|
1640
|
+
pass "fm-pr-merge reports every failing GitLab condition, not only the first"
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
test_gitlab_stale_recorded_head_is_reported() {
|
|
1644
|
+
local case_dir rc merge_line
|
|
1645
|
+
case_dir=$(make_gitlab_case gitlab-stale-head)
|
|
1646
|
+
# The recorded head is what a rebase leaves behind. It is read before
|
|
1647
|
+
# fm-pr-check.sh rewrites the metadata, which drops a head it cannot resolve
|
|
1648
|
+
# for a GitLab task, so reading it afterwards would find nothing at all.
|
|
1649
|
+
printf 'pr_head=%s\n' "$MR_STALE_HEAD" >> "$case_dir/state/task-x1.meta"
|
|
1650
|
+
|
|
1651
|
+
set +e
|
|
1652
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1653
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1654
|
+
rc=$?
|
|
1655
|
+
set -e
|
|
1656
|
+
|
|
1657
|
+
expect_code 0 "$rc" "gitlab-stale-head: the live head satisfies every condition, so it should merge"
|
|
1658
|
+
assert_grep "recorded head $MR_STALE_HEAD disagrees with the live head $MR_HEAD" \
|
|
1659
|
+
"$case_dir/stderr" "gitlab-stale-head: the stale recorded head was trusted silently"
|
|
1660
|
+
merge_line=$(glab_merge_line "$case_dir/glab.log")
|
|
1661
|
+
case "$merge_line" in
|
|
1662
|
+
*"--sha $MR_HEAD"*) : ;;
|
|
1663
|
+
*) fail "gitlab-stale-head: the merge was not bound to the live head: '$merge_line'" ;;
|
|
1664
|
+
esac
|
|
1665
|
+
assert_no_grep "pr_head=$MR_STALE_HEAD" "$case_dir/state/task-x1.meta" \
|
|
1666
|
+
"gitlab-stale-head: the recording step no longer drops an unresolvable GitLab head"
|
|
1667
|
+
pass "fm-pr-merge reports a stale recorded head and verifies the live one"
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
test_gitlab_unreadable_state_refuses() {
|
|
1671
|
+
local case_dir rc name
|
|
1672
|
+
for name in view-fails not-an-object split-value; do
|
|
1673
|
+
case_dir=$(make_gitlab_case "gitlab-unreadable-$name")
|
|
1674
|
+
case "$name" in
|
|
1675
|
+
view-fails) : > "$case_dir/glab-view-fails" ;;
|
|
1676
|
+
not-an-object) printf '[]\n' > "$case_dir/mr.json" ;;
|
|
1677
|
+
# A value carrying a newline splits into a line no field name matches, so
|
|
1678
|
+
# it must refuse rather than be truncated into a value a check accepts.
|
|
1679
|
+
split-value) write_mr_json "$case_dir/mr.json" 'state=opened\nnot-a-field' ;;
|
|
1680
|
+
esac
|
|
1681
|
+
|
|
1682
|
+
set +e
|
|
1683
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1684
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1685
|
+
rc=$?
|
|
1686
|
+
set -e
|
|
1687
|
+
|
|
1688
|
+
expect_code 1 "$rc" "gitlab-unreadable-$name: fm-pr-merge should refuse"
|
|
1689
|
+
assert_grep 'could not read the GitLab merge request state before merging' \
|
|
1690
|
+
"$case_dir/stderr" "gitlab-unreadable-$name: refusal did not name the unreadable state"
|
|
1691
|
+
[ -z "$(glab_merge_line "$case_dir/glab.log")" ] \
|
|
1692
|
+
|| fail "gitlab-unreadable-$name: a merge was attempted on an unreadable state"
|
|
1693
|
+
done
|
|
1694
|
+
pass "fm-pr-merge refuses an unreadable GitLab merge request state rather than merging blind"
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
test_gitlab_invalid_head_refuses() {
|
|
1698
|
+
local case_dir rc
|
|
1699
|
+
case_dir=$(make_gitlab_case gitlab-invalid-head head=not-a-sha)
|
|
1700
|
+
|
|
1701
|
+
set +e
|
|
1702
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1703
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1704
|
+
rc=$?
|
|
1705
|
+
set -e
|
|
1706
|
+
|
|
1707
|
+
expect_code 1 "$rc" "gitlab-invalid-head: fm-pr-merge should refuse"
|
|
1708
|
+
assert_grep 'could not read the GitLab merge request head commit before merging' \
|
|
1709
|
+
"$case_dir/stderr" "gitlab-invalid-head: refusal did not name the unreadable head"
|
|
1710
|
+
[ -z "$(glab_merge_line "$case_dir/glab.log")" ] \
|
|
1711
|
+
|| fail "gitlab-invalid-head: a merge was bound to a head that is not a commit"
|
|
1712
|
+
pass "fm-pr-merge refuses a GitLab head commit it cannot validate"
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
test_gitlab_missing_tool_refuses_before_recording() {
|
|
1716
|
+
local case_dir rc tool other
|
|
1717
|
+
for tool in glab jq; do
|
|
1718
|
+
if [ "$tool" = glab ]; then other=jq; else other=glab; fi
|
|
1719
|
+
case_dir=$(make_gitlab_case "gitlab-no-$tool")
|
|
1720
|
+
mirror_path_without "$case_dir/no$tool" "$tool" "$case_dir/fakebin"
|
|
1721
|
+
# One tool absent, the other still answered by this case's own mock, so the
|
|
1722
|
+
# refusal names exactly one tool on a host that ships neither.
|
|
1723
|
+
PATH="$case_dir/no$tool" command -v "$other" >/dev/null 2>&1 \
|
|
1724
|
+
|| fail "gitlab-no-$tool: the $tool-free search path lost the $other mock as well"
|
|
1725
|
+
|
|
1726
|
+
set +e
|
|
1727
|
+
FM_ROOT_OVERRIDE="$ROOT" \
|
|
1728
|
+
FM_STATE_OVERRIDE="$case_dir/state" \
|
|
1729
|
+
FM_TEST_GH_AXI_LOG="$case_dir/gh-axi.log" \
|
|
1730
|
+
FM_TEST_GLAB_LOG="$case_dir/glab.log" \
|
|
1731
|
+
FM_TEST_GLAB_JSON="$case_dir/mr.json" \
|
|
1732
|
+
PATH="$case_dir/no$tool" \
|
|
1733
|
+
"$PR_MERGE" task-x1 "$MR_URL" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1734
|
+
rc=$?
|
|
1735
|
+
set -e
|
|
1736
|
+
|
|
1737
|
+
expect_code 1 "$rc" "gitlab-no-$tool: fm-pr-merge should refuse"
|
|
1738
|
+
assert_grep "error: merging a GitLab merge request requires $tool on PATH" \
|
|
1739
|
+
"$case_dir/stderr" "gitlab-no-$tool: refusal did not name the missing tool"
|
|
1740
|
+
assert_no_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1741
|
+
"gitlab-no-$tool: a PR reference was recorded despite the missing tool"
|
|
1742
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1743
|
+
"gitlab-no-$tool: a merge poll was armed despite the missing tool"
|
|
1744
|
+
done
|
|
1745
|
+
pass "fm-pr-merge refuses before recording anything when glab or jq is absent"
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
test_gitlab_head_override_args_refuse_before_recording() {
|
|
1749
|
+
local case_dir rc
|
|
1750
|
+
case_dir=$(make_gitlab_case gitlab-head-override)
|
|
1751
|
+
|
|
1752
|
+
set +e
|
|
1753
|
+
run_pr_merge "$case_dir" task-x1 "$MR_URL" -- --sha "$MR_STALE_HEAD" \
|
|
1754
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr"
|
|
1755
|
+
rc=$?
|
|
1756
|
+
set -e
|
|
1757
|
+
|
|
1758
|
+
expect_code 1 "$rc" "gitlab-head-override: fm-pr-merge should refuse a caller head override"
|
|
1759
|
+
assert_grep 'extra merge arguments must not override the head commit' "$case_dir/stderr" \
|
|
1760
|
+
"gitlab-head-override: refusal did not explain the head override"
|
|
1761
|
+
assert_no_grep "pr=$MR_URL" "$case_dir/state/task-x1.meta" \
|
|
1762
|
+
"gitlab-head-override: the URL was recorded before rejecting the head override"
|
|
1763
|
+
assert_absent "$case_dir/state/task-x1.check.sh" \
|
|
1764
|
+
"gitlab-head-override: a head override armed a merge poll"
|
|
1765
|
+
[ ! -s "$case_dir/glab.log" ] || fail "gitlab-head-override: glab was invoked despite the head override"
|
|
1766
|
+
pass "fm-pr-merge refuses a GitLab head override before recording state"
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
test_github_still_forwards_sha_arg() {
|
|
1770
|
+
local case_dir
|
|
1771
|
+
case_dir=$(make_case github-sha-arg)
|
|
1772
|
+
mkdir -p "$case_dir/wt"
|
|
1773
|
+
add_gh_mocks "$case_dir" dddddddddddddddddddddddddddddddddddddddd
|
|
1774
|
+
: > "$case_dir/gh-axi.log"
|
|
1775
|
+
|
|
1776
|
+
# --sha is rejected only where the head is firstmate's to determine. GitHub's
|
|
1777
|
+
# extra args are the caller's business exactly as they were.
|
|
1778
|
+
run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/44 -- --sha abc123 \
|
|
1779
|
+
> "$case_dir/stdout" 2> "$case_dir/stderr" || fail "github-sha-arg: fm-pr-merge failed"
|
|
1780
|
+
|
|
1781
|
+
grep -qxF 'pr merge 44 --repo example/repo --squash --sha abc123' "$case_dir/gh-axi.log" \
|
|
1782
|
+
|| fail "github-sha-arg: the GitHub path stopped forwarding a caller --sha"
|
|
1783
|
+
pass "fm-pr-merge leaves GitHub extra-arg handling unchanged, including --sha"
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
# --- durable merge outcome ---------------------------------------------------
|
|
1787
|
+
# A merge that lands must leave a record outside the merging agent's memory.
|
|
1788
|
+
# bin/fm-merge-outcome-lib.sh owns where that record goes; these cases pin the
|
|
1789
|
+
# behavior through the real merge entrypoint.
|
|
1790
|
+
|
|
1791
|
+
# make_home_case <name> [<route> [<parent-home>]]: a case dir whose home is a
|
|
1792
|
+
# secondmate home bound to a parent, or a plain main home when no route is
|
|
1793
|
+
# given. Echoes the case dir; the home is "$case_dir/home".
|
|
1794
|
+
make_home_case() {
|
|
1795
|
+
local name=$1 route=${2:-} parent=${3:-} case_dir home
|
|
1796
|
+
case_dir=$(make_case "$name")
|
|
1797
|
+
home="$case_dir/home"
|
|
1798
|
+
mkdir -p "$home" "$case_dir/wt"
|
|
1799
|
+
if [ -n "$route" ]; then
|
|
1800
|
+
printf '%s\n' mate-x >"$home/.fm-secondmate-home"
|
|
1801
|
+
{
|
|
1802
|
+
printf 'schema=fm-secondmate-parent.v1\n'
|
|
1803
|
+
printf 'route=%s\n' "$route"
|
|
1804
|
+
[ "$route" != local ] || printf 'parent_home=%s\n' "$parent"
|
|
1805
|
+
} >"$home/.fm-secondmate-parent"
|
|
1806
|
+
fi
|
|
1807
|
+
printf '%s\n' "$case_dir"
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
parent_reply_lines() { # <file> <url>
|
|
1811
|
+
grep -c -F "$2" "$1" 2>/dev/null || true
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
test_secondmate_merge_reports_upward_once() {
|
|
1815
|
+
local case_dir replies url
|
|
1816
|
+
url=https://github.com/example/repo/pull/61
|
|
1817
|
+
case_dir=$(make_home_case secondmate-merge-reports remote)
|
|
1818
|
+
add_gh_mocks "$case_dir" 5555555555555555555555555555555555555555
|
|
1819
|
+
: >"$case_dir/gh-axi.log"
|
|
1820
|
+
replies="$case_dir/state/parent-replies.status"
|
|
1821
|
+
|
|
1822
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1823
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" || fail "secondmate-merge-reports: merge failed"
|
|
1824
|
+
|
|
1825
|
+
assert_grep "done [key=merged-task-x1]: merged task-x1 $url" "$replies" \
|
|
1826
|
+
"secondmate-merge-reports: the landed PR was not reported upward"
|
|
1827
|
+
[ "$(wc -l <"$replies")" -eq 1 ] \
|
|
1828
|
+
|| fail "secondmate-merge-reports: one merge produced more than one upward line"
|
|
1829
|
+
|
|
1830
|
+
# The same merge again: the forge accepts it in this fixture, so only the
|
|
1831
|
+
# at-most-once contract can keep the parent from being told twice.
|
|
1832
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1833
|
+
>"$case_dir/stdout2" 2>"$case_dir/stderr2" || fail "secondmate-merge-reports: repeat merge failed"
|
|
1834
|
+
[ "$(parent_reply_lines "$replies" "$url")" -eq 1 ] \
|
|
1835
|
+
|| fail "secondmate-merge-reports: a repeat merge of the same PR duplicated the upward line"
|
|
1836
|
+
pass "a merge a secondmate home performs itself is reported upward exactly once"
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
test_secondmate_merge_reports_on_the_local_route() {
|
|
1840
|
+
local case_dir parent_status url
|
|
1841
|
+
url=https://github.com/example/repo/pull/62
|
|
1842
|
+
case_dir=$(make_home_case secondmate-merge-local local "$TMP_ROOT/secondmate-merge-local/parent")
|
|
1843
|
+
mkdir -p "$TMP_ROOT/secondmate-merge-local/parent/state"
|
|
1844
|
+
add_gh_mocks "$case_dir" 6666666666666666666666666666666666666666
|
|
1845
|
+
: >"$case_dir/gh-axi.log"
|
|
1846
|
+
parent_status="$TMP_ROOT/secondmate-merge-local/parent/state/mate-x.status"
|
|
1847
|
+
|
|
1848
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1849
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" || fail "secondmate-merge-local: merge failed"
|
|
1850
|
+
|
|
1851
|
+
assert_grep "done [key=merged-task-x1]: merged task-x1 $url" "$parent_status" \
|
|
1852
|
+
"secondmate-merge-local: the landed PR did not reach the parent home's channel"
|
|
1853
|
+
[ ! -e "$case_dir/state/parent-replies.status" ] \
|
|
1854
|
+
|| fail "secondmate-merge-local: a local-route report also wrote the remote reply channel"
|
|
1855
|
+
pass "a locally routed secondmate home reports the landed PR into its parent's own channel"
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
test_failed_merge_reports_nothing() {
|
|
1859
|
+
local case_dir rc
|
|
1860
|
+
case_dir=$(make_home_case failed-merge-silent remote)
|
|
1861
|
+
add_gh_mocks_merge_fails "$case_dir"
|
|
1862
|
+
: >"$case_dir/gh-axi.log"
|
|
1863
|
+
|
|
1864
|
+
set +e
|
|
1865
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 https://github.com/example/repo/pull/63 \
|
|
1866
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr"
|
|
1867
|
+
rc=$?
|
|
1868
|
+
set -e
|
|
1869
|
+
|
|
1870
|
+
expect_code 1 "$rc" "failed-merge-silent: a failed merge should propagate"
|
|
1871
|
+
assert_absent "$case_dir/state/parent-replies.status" \
|
|
1872
|
+
"failed-merge-silent: a merge that never landed was reported as landed"
|
|
1873
|
+
pass "a refused or failed merge reports no outcome"
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
test_gitlab_refusal_reports_nothing() {
|
|
1877
|
+
local case_dir rc
|
|
1878
|
+
case_dir=$(make_gitlab_case gitlab-refusal-silent state=merged)
|
|
1879
|
+
mkdir -p "$case_dir/home"
|
|
1880
|
+
printf '%s\n' mate-x >"$case_dir/home/.fm-secondmate-home"
|
|
1881
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=remote\n' >"$case_dir/home/.fm-secondmate-parent"
|
|
1882
|
+
|
|
1883
|
+
set +e
|
|
1884
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1885
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr"
|
|
1886
|
+
rc=$?
|
|
1887
|
+
set -e
|
|
1888
|
+
|
|
1889
|
+
expect_code 1 "$rc" "gitlab-refusal-silent: a refused GitLab merge should exit non-zero"
|
|
1890
|
+
assert_absent "$case_dir/state/parent-replies.status" \
|
|
1891
|
+
"gitlab-refusal-silent: a refused merge request was reported as landed"
|
|
1892
|
+
pass "a GitLab merge refused before the forge call reports no outcome"
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
test_gitlab_merge_reports_upward() {
|
|
1896
|
+
local case_dir url
|
|
1897
|
+
case_dir=$(make_gitlab_case gitlab-merge-reports)
|
|
1898
|
+
mkdir -p "$case_dir/home"
|
|
1899
|
+
printf '%s\n' mate-x >"$case_dir/home/.fm-secondmate-home"
|
|
1900
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=remote\n' >"$case_dir/home/.fm-secondmate-parent"
|
|
1901
|
+
url=$MR_URL
|
|
1902
|
+
|
|
1903
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1904
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" || fail "gitlab-merge-reports: merge failed"
|
|
1905
|
+
|
|
1906
|
+
assert_grep "done [key=merged-task-x1]: merged task-x1 $url" \
|
|
1907
|
+
"$case_dir/state/parent-replies.status" \
|
|
1908
|
+
"gitlab-merge-reports: a landed merge request was not reported upward"
|
|
1909
|
+
pass "a landed GitLab merge request is reported upward on the same channel"
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
test_queued_gitlab_merge_leaves_the_poll_armed() {
|
|
1913
|
+
local case_dir
|
|
1914
|
+
case_dir=$(make_gitlab_case queued-gitlab-merge)
|
|
1915
|
+
mkdir -p "$case_dir/home"
|
|
1916
|
+
: >"$case_dir/glab-stays-open"
|
|
1917
|
+
|
|
1918
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$MR_URL" \
|
|
1919
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" \
|
|
1920
|
+
|| fail "queued-gitlab-merge: accepted merge command failed"
|
|
1921
|
+
|
|
1922
|
+
assert_absent "$case_dir/state/.wake-queue" \
|
|
1923
|
+
"queued-gitlab-merge: a queued merge was reported as landed"
|
|
1924
|
+
[ -f "$case_dir/state/task-x1.check.sh" ] \
|
|
1925
|
+
|| fail "queued-gitlab-merge: the merge poll was not left armed"
|
|
1926
|
+
[ ! -e "$case_dir/state/task-x1.pr-poll-merge-notified" ] \
|
|
1927
|
+
|| fail "queued-gitlab-merge: a queued merge was marked as reported"
|
|
1928
|
+
pass "a queued GitLab merge stays silent and leaves confirmation to the armed poll"
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
test_main_home_merge_leaves_a_durable_wake() {
|
|
1932
|
+
local case_dir url
|
|
1933
|
+
url=https://github.com/example/repo/pull/64
|
|
1934
|
+
case_dir=$(make_home_case main-merge-wake)
|
|
1935
|
+
add_gh_mocks "$case_dir" 7777777777777777777777777777777777777777
|
|
1936
|
+
: >"$case_dir/gh-axi.log"
|
|
1937
|
+
|
|
1938
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1939
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" || fail "main-merge-wake: merge failed"
|
|
1940
|
+
|
|
1941
|
+
assert_grep "$url" "$case_dir/state/.wake-queue" \
|
|
1942
|
+
"main-merge-wake: a merge this home performed left no durable record naming the PR"
|
|
1943
|
+
[ "$(grep -c -F "$url" "$case_dir/state/.wake-queue")" -eq 1 ] \
|
|
1944
|
+
|| fail "main-merge-wake: one merge produced more than one durable record"
|
|
1945
|
+
assert_absent "$case_dir/state/parent-replies.status" \
|
|
1946
|
+
"main-merge-wake: a main home wrote a parent reply channel it does not have"
|
|
1947
|
+
pass "a merge a main home performs itself leaves one durable wake naming the PR"
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
test_queued_github_merge_leaves_the_poll_armed() {
|
|
1951
|
+
local case_dir url
|
|
1952
|
+
url=https://github.com/example/repo/pull/66
|
|
1953
|
+
case_dir=$(make_home_case queued-github-merge)
|
|
1954
|
+
add_gh_mocks "$case_dir" 9999999999999999999999999999999999999999
|
|
1955
|
+
write_github_outcome "$case_dir" OPEN false true main
|
|
1956
|
+
: >"$case_dir/gh-axi.log"
|
|
1957
|
+
|
|
1958
|
+
FM_TEST_GH_MERGE_STATE=open FM_TEST_HOME="$case_dir/home" \
|
|
1959
|
+
run_pr_merge "$case_dir" task-x1 "$url" \
|
|
1960
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr" \
|
|
1961
|
+
|| fail "queued-github-merge: accepted merge command failed"
|
|
1962
|
+
|
|
1963
|
+
assert_absent "$case_dir/state/.wake-queue" \
|
|
1964
|
+
"queued-github-merge: a queued merge was reported as landed"
|
|
1965
|
+
[ -f "$case_dir/state/task-x1.check.sh" ] \
|
|
1966
|
+
|| fail "queued-github-merge: the merge poll was not left armed"
|
|
1967
|
+
[ ! -e "$case_dir/state/task-x1.pr-poll-merge-notified" ] \
|
|
1968
|
+
|| fail "queued-github-merge: a queued merge was marked as reported"
|
|
1969
|
+
pass "a queued GitHub merge stays silent and leaves confirmation to the armed poll"
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
test_distinct_merged_prs_keep_distinct_wakes() {
|
|
1973
|
+
local case_dir first_url second_url
|
|
1974
|
+
first_url=https://github.com/example/repo/pull/68
|
|
1975
|
+
second_url=https://github.com/example/repo/pull/69
|
|
1976
|
+
case_dir=$(make_home_case distinct-merge-wakes)
|
|
1977
|
+
add_gh_mocks "$case_dir" bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
1978
|
+
: >"$case_dir/gh-axi.log"
|
|
1979
|
+
|
|
1980
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$first_url" \
|
|
1981
|
+
>"$case_dir/stdout-1" 2>"$case_dir/stderr-1" \
|
|
1982
|
+
|| fail "distinct-merge-wakes: first merge failed"
|
|
1983
|
+
rm -f "$case_dir/state/task-x1.check.sh" \
|
|
1984
|
+
"$case_dir/state/task-x1.pr-poll" \
|
|
1985
|
+
"$case_dir/state/task-x1.pr-poll-registration"
|
|
1986
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$second_url" \
|
|
1987
|
+
>"$case_dir/stdout-2" 2>"$case_dir/stderr-2" \
|
|
1988
|
+
|| fail "distinct-merge-wakes: second merge failed"
|
|
1989
|
+
|
|
1990
|
+
[ "$(grep -c -F "$first_url" "$case_dir/state/.wake-queue")" -eq 1 ] \
|
|
1991
|
+
|| fail "distinct-merge-wakes: first merge wake was missing or duplicated"
|
|
1992
|
+
[ "$(grep -c -F "$second_url" "$case_dir/state/.wake-queue")" -eq 1 ] \
|
|
1993
|
+
|| fail "distinct-merge-wakes: second merge wake was missing or duplicated"
|
|
1994
|
+
FM_STATE_OVERRIDE="$case_dir/state" "$ROOT/bin/fm-wake-drain.sh" \
|
|
1995
|
+
>"$case_dir/drain.out" 2>"$case_dir/drain.err" \
|
|
1996
|
+
|| fail "distinct-merge-wakes: wake drain failed"
|
|
1997
|
+
assert_grep "$first_url" "$case_dir/drain.out" \
|
|
1998
|
+
"distinct-merge-wakes: queue deduplication collapsed the first PR"
|
|
1999
|
+
assert_grep "$second_url" "$case_dir/drain.out" \
|
|
2000
|
+
"distinct-merge-wakes: queue deduplication collapsed the second PR"
|
|
2001
|
+
pass "distinct merged PRs for one task retain distinct captain-facing wakes"
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
test_uncommitted_marker_retry_is_never_silent() {
|
|
2005
|
+
local case_dir url count
|
|
2006
|
+
url=https://github.com/example/repo/pull/67
|
|
2007
|
+
case_dir=$(make_home_case uncommitted-wake-retry)
|
|
2008
|
+
add_gh_mocks "$case_dir" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
2009
|
+
: >"$case_dir/gh-axi.log"
|
|
2010
|
+
cat >"$case_dir/fakebin/mv" <<'SH'
|
|
2011
|
+
#!/usr/bin/env bash
|
|
2012
|
+
case "${!#}" in
|
|
2013
|
+
*.pr-poll-merge-notified)
|
|
2014
|
+
if mkdir "$FM_TEST_MARKER_FAILURE.claim" 2>/dev/null; then
|
|
2015
|
+
exit 1
|
|
2016
|
+
fi
|
|
2017
|
+
;;
|
|
2018
|
+
esac
|
|
2019
|
+
exec "$FM_TEST_REAL_MV" "$@"
|
|
2020
|
+
SH
|
|
2021
|
+
chmod +x "$case_dir/fakebin/mv"
|
|
2022
|
+
export FM_TEST_MARKER_FAILURE="$case_dir/marker-failure"
|
|
2023
|
+
export FM_TEST_REAL_MV
|
|
2024
|
+
FM_TEST_REAL_MV=$(command -v mv)
|
|
2025
|
+
|
|
2026
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
2027
|
+
>"$case_dir/stdout-1" 2>"$case_dir/stderr-1" \
|
|
2028
|
+
|| fail "uncommitted-wake-retry: landed merge was reported as failed"
|
|
2029
|
+
assert_grep 'could not record the outcome' "$case_dir/stderr-1" \
|
|
2030
|
+
"uncommitted-wake-retry: failed marker commit was not loud"
|
|
2031
|
+
[ -f "$case_dir/state/task-x1.check.sh" ] \
|
|
2032
|
+
|| fail "uncommitted-wake-retry: failed commit disarmed the retry poll"
|
|
2033
|
+
count=$(grep -c -F "$url" "$case_dir/state/.wake-queue")
|
|
2034
|
+
[ "$count" -ge 1 ] \
|
|
2035
|
+
|| fail "uncommitted-wake-retry: failed marker commit lost the durable outcome"
|
|
2036
|
+
[ ! -e "$case_dir/state/task-x1.pr-poll-merge-notified" ] \
|
|
2037
|
+
|| fail "uncommitted-wake-retry: failed marker commit was treated as complete"
|
|
2038
|
+
|
|
2039
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
2040
|
+
>"$case_dir/stdout-2" 2>"$case_dir/stderr-2" \
|
|
2041
|
+
|| fail "uncommitted-wake-retry: retry failed"
|
|
2042
|
+
unset FM_TEST_MARKER_FAILURE FM_TEST_REAL_MV
|
|
2043
|
+
count=$(grep -c -F "$url" "$case_dir/state/.wake-queue")
|
|
2044
|
+
[ "$count" -ge 1 ] \
|
|
2045
|
+
|| fail "uncommitted-wake-retry: retry left the merge silent"
|
|
2046
|
+
[ -f "$case_dir/state/task-x1.pr-poll-merge-notified" ] \
|
|
2047
|
+
|| fail "uncommitted-wake-retry: retry did not commit the canonical marker"
|
|
2048
|
+
pass "an uncommitted marker retry preserves at least one durable outcome"
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
test_secondmate_without_parent_binding_is_loud() {
|
|
2052
|
+
local case_dir rc url
|
|
2053
|
+
url=https://github.com/example/repo/pull/65
|
|
2054
|
+
case_dir=$(make_home_case unbound-secondmate)
|
|
2055
|
+
add_gh_mocks "$case_dir" 8888888888888888888888888888888888888888
|
|
2056
|
+
: >"$case_dir/gh-axi.log"
|
|
2057
|
+
# A secondmate identity with no parent binding: exactly the seeding gap that
|
|
2058
|
+
# let three real merges land in silence.
|
|
2059
|
+
printf '%s\n' mate-x >"$case_dir/home/.fm-secondmate-home"
|
|
2060
|
+
|
|
2061
|
+
set +e
|
|
2062
|
+
FM_TEST_HOME="$case_dir/home" run_pr_merge "$case_dir" task-x1 "$url" \
|
|
2063
|
+
>"$case_dir/stdout" 2>"$case_dir/stderr"
|
|
2064
|
+
rc=$?
|
|
2065
|
+
set -e
|
|
2066
|
+
|
|
2067
|
+
expect_code 0 "$rc" "unbound-secondmate: the merge itself landed and must not be reported as failed"
|
|
2068
|
+
assert_grep 'could not report it upward' "$case_dir/stderr" \
|
|
2069
|
+
"unbound-secondmate: a merge that could not be reported upward said nothing about it"
|
|
2070
|
+
assert_absent "$case_dir/state/.wake-queue" \
|
|
2071
|
+
"unbound-secondmate: a secondmate home fell back to the main-home record"
|
|
2072
|
+
pass "a secondmate home that cannot report upward says so instead of merging in silence"
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
test_github_zero_exit_queue_required_refuses_with_exact_retry
|
|
2076
|
+
test_github_closed_unqueued_outcome_omits_retry_flags
|
|
2077
|
+
test_github_agreeing_queue_rules_keep_retry_guidance
|
|
2078
|
+
test_github_conflicting_queue_rules_report_ambiguity
|
|
2079
|
+
test_verified_merge_records_pr_and_head
|
|
2080
|
+
test_pr_metadata_is_recorded_before_the_forge_call
|
|
2081
|
+
test_merge_failure_propagates_after_recording
|
|
2082
|
+
test_github_open_unqueued_outcome_refuses
|
|
2083
|
+
test_github_unreadable_outcome_keeps_pr_bookkeeping
|
|
2084
|
+
test_github_refusal_quotes_the_forge_output
|
|
2085
|
+
test_github_unreadable_outcome_refusal_quotes_the_forge_output
|
|
2086
|
+
test_github_accepted_queue_flags_do_not_echo_back_the_same_command
|
|
2087
|
+
test_github_mismatched_queue_flags_still_name_the_retry
|
|
2088
|
+
test_github_unrecognised_queue_method_still_names_the_queue
|
|
2089
|
+
test_github_unreadable_queue_rules_are_not_reported_as_no_queue
|
|
2090
|
+
test_github_no_queue_rule_says_nothing_about_a_queue
|
|
2091
|
+
test_github_fallback_view_refusal_says_the_queue_was_unobservable
|
|
2092
|
+
test_github_auto_merge_without_queue_refuses_legibly
|
|
2093
|
+
test_github_failed_merge_never_claims_armed_auto_merge
|
|
2094
|
+
test_github_failed_merge_with_queue_flags_never_claims_acceptance
|
|
2095
|
+
test_github_failed_gh_read_falls_back_to_gh_axi
|
|
2096
|
+
test_github_failed_merge_names_an_observed_landed_state
|
|
2097
|
+
test_github_without_gh_still_uses_gh_axi_merge
|
|
2098
|
+
test_github_without_gh_failed_read_keeps_bookkeeping
|
|
2099
|
+
test_github_merged_outcome_is_verified
|
|
2100
|
+
test_github_verified_merge_requires_poll_recording
|
|
2101
|
+
test_github_queued_outcome_is_verified
|
|
2102
|
+
test_github_queue_required_refusal_names_retry_flags
|
|
2103
|
+
test_extra_merge_args_forwarded
|
|
2104
|
+
test_missing_meta_refuses_before_merge
|
|
2105
|
+
test_malformed_url_refuses_before_merge
|
|
2106
|
+
test_rejects_unsafe_url_segments_before_recording
|
|
2107
|
+
test_repo_override_args_refuse_before_recording
|
|
2108
|
+
test_bundled_repo_override_args_refuse_before_recording
|
|
2109
|
+
test_explicit_merge_method_not_overridden
|
|
2110
|
+
test_method_equals_merge_method_not_overridden
|
|
2111
|
+
test_parses_pr_url_for_gh_axi
|
|
2112
|
+
test_github_still_forwards_sha_arg
|
|
2113
|
+
test_gitlab_url_resolves_and_merges
|
|
2114
|
+
test_gitlab_host_comes_from_the_url
|
|
2115
|
+
test_gitlab_imposes_no_merge_method
|
|
2116
|
+
test_gitlab_extra_args_forwarded
|
|
2117
|
+
test_gitlab_merge_failure_propagates
|
|
2118
|
+
test_gitlab_each_condition_refuses_independently
|
|
2119
|
+
test_gitlab_reports_every_failing_condition
|
|
2120
|
+
test_gitlab_stale_recorded_head_is_reported
|
|
2121
|
+
test_gitlab_unreadable_state_refuses
|
|
2122
|
+
test_gitlab_invalid_head_refuses
|
|
2123
|
+
test_gitlab_missing_tool_refuses_before_recording
|
|
2124
|
+
test_gitlab_head_override_args_refuse_before_recording
|
|
2125
|
+
test_secondmate_merge_reports_upward_once
|
|
2126
|
+
test_secondmate_merge_reports_on_the_local_route
|
|
2127
|
+
test_gitlab_merge_reports_upward
|
|
2128
|
+
test_queued_gitlab_merge_leaves_the_poll_armed
|
|
2129
|
+
test_failed_merge_reports_nothing
|
|
2130
|
+
test_gitlab_refusal_reports_nothing
|
|
2131
|
+
test_main_home_merge_leaves_a_durable_wake
|
|
2132
|
+
test_queued_github_merge_leaves_the_poll_armed
|
|
2133
|
+
test_distinct_merged_prs_keep_distinct_wakes
|
|
2134
|
+
test_uncommitted_marker_retry_is_never_silent
|
|
2135
|
+
test_secondmate_without_parent_binding_is_loud
|