@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,2140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Security and regression tests for canonical PR parsing, static merge polls,
|
|
3
|
+
# private atomic artifacts, authenticated custom checks, and teardown cleanup.
|
|
4
|
+
set -u
|
|
5
|
+
|
|
6
|
+
# shellcheck source=tests/lib.sh disable=SC1091
|
|
7
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
8
|
+
# shellcheck source=/dev/null
|
|
9
|
+
. "$ROOT/bin/fm-pr-lib.sh"
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
. "$ROOT/bin/fm-check-lib.sh"
|
|
12
|
+
|
|
13
|
+
PR_CHECK="$ROOT/bin/fm-pr-check.sh"
|
|
14
|
+
PR_MERGE="$ROOT/bin/fm-pr-merge.sh"
|
|
15
|
+
POLL="$ROOT/bin/fm-pr-poll.sh"
|
|
16
|
+
WATCH="$ROOT/bin/fm-watch.sh"
|
|
17
|
+
TEARDOWN="$ROOT/bin/fm-teardown.sh"
|
|
18
|
+
REGISTER="$ROOT/bin/fm-check-register.sh"
|
|
19
|
+
TMP_ROOT=$(fm_test_tmproot fm-pr-check-security)
|
|
20
|
+
BASE_PATH=${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}
|
|
21
|
+
REAL_CP=$(command -v cp)
|
|
22
|
+
REAL_MV=$(command -v mv)
|
|
23
|
+
REAL_STAT=$(command -v stat)
|
|
24
|
+
REAL_CHMOD=$(command -v chmod)
|
|
25
|
+
# The merge path reads a merge request's JSON with the real jq, and BASE_PATH is
|
|
26
|
+
# deliberately restricted, so a case that needs jq exposes this one rather than
|
|
27
|
+
# depending on the host keeping jq in one of those four directories.
|
|
28
|
+
REAL_JQ=$(command -v jq) || fail "these tests read glab's JSON with the real jq, which was not found"
|
|
29
|
+
|
|
30
|
+
ack_watcher_cycle() { # <state>
|
|
31
|
+
local state=$1 err sequence generation
|
|
32
|
+
err="$state/.test-wake-drain.err"
|
|
33
|
+
FM_STATE_OVERRIDE="$state" "$ROOT/bin/fm-wake-drain.sh" >/dev/null 2> "$err" || return 1
|
|
34
|
+
sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
|
|
35
|
+
generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
|
|
36
|
+
rm -f "$err"
|
|
37
|
+
[ -n "$sequence" ] && [ -n "$generation" ] || return 1
|
|
38
|
+
FM_STATE_OVERRIDE="$state" "$ROOT/bin/fm-wake-drain.sh" --ack-through "$sequence" \
|
|
39
|
+
--recovery-generation "$generation"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
file_mode() {
|
|
43
|
+
if [ "$(uname)" = Darwin ]; then
|
|
44
|
+
stat -f %Lp "$1"
|
|
45
|
+
else
|
|
46
|
+
stat -c %a "$1"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
LINK_KIND=
|
|
51
|
+
LINK_TARGET=
|
|
52
|
+
LINK_CONTENT=
|
|
53
|
+
LINK_MODE=
|
|
54
|
+
make_private_symlink() {
|
|
55
|
+
local base=$1 destination=$2 kind=$3
|
|
56
|
+
LINK_KIND=$kind
|
|
57
|
+
LINK_TARGET="$base/target-$kind"
|
|
58
|
+
LINK_CONTENT=
|
|
59
|
+
LINK_MODE=
|
|
60
|
+
case "$kind" in
|
|
61
|
+
regular)
|
|
62
|
+
LINK_CONTENT='external sentinel'
|
|
63
|
+
printf '%s\n' "$LINK_CONTENT" > "$LINK_TARGET"
|
|
64
|
+
chmod 0644 "$LINK_TARGET"
|
|
65
|
+
LINK_MODE=644
|
|
66
|
+
;;
|
|
67
|
+
dangling)
|
|
68
|
+
rm -f "$LINK_TARGET"
|
|
69
|
+
;;
|
|
70
|
+
directory)
|
|
71
|
+
mkdir "$LINK_TARGET"
|
|
72
|
+
printf 'outside\n' > "$LINK_TARGET/keep"
|
|
73
|
+
chmod 0755 "$LINK_TARGET"
|
|
74
|
+
LINK_MODE=755
|
|
75
|
+
;;
|
|
76
|
+
*) fail "unknown symlink fixture kind" ;;
|
|
77
|
+
esac
|
|
78
|
+
ln -s "$LINK_TARGET" "$destination"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
assert_private_symlink_unchanged() {
|
|
82
|
+
local link=$1
|
|
83
|
+
[ -L "$link" ] || fail "private destination symlink was replaced"
|
|
84
|
+
case "$LINK_KIND" in
|
|
85
|
+
regular)
|
|
86
|
+
[ "$(cat "$LINK_TARGET")" = "$LINK_CONTENT" ] || fail "external regular target content changed"
|
|
87
|
+
[ "$(file_mode "$LINK_TARGET")" = "$LINK_MODE" ] || fail "external regular target mode changed"
|
|
88
|
+
;;
|
|
89
|
+
dangling)
|
|
90
|
+
[ ! -e "$LINK_TARGET" ] || fail "dangling target was created"
|
|
91
|
+
;;
|
|
92
|
+
directory)
|
|
93
|
+
[ -f "$LINK_TARGET/keep" ] || fail "external directory target contents changed"
|
|
94
|
+
[ "$(file_mode "$LINK_TARGET")" = "$LINK_MODE" ] || fail "external directory target mode changed"
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
state_snapshot() {
|
|
100
|
+
local state=$1 file
|
|
101
|
+
(
|
|
102
|
+
cd "$state" || exit 1
|
|
103
|
+
find . \( -type f -o -type l \) -print | LC_ALL=C sort | while IFS= read -r file; do
|
|
104
|
+
if [ -L "$file" ]; then
|
|
105
|
+
printf 'link %s %s\n' "$file" "$(readlink "$file")"
|
|
106
|
+
else
|
|
107
|
+
printf 'file %s %s ' "$file" "$(file_mode "$file")"
|
|
108
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
make_case() {
|
|
115
|
+
local name=$1 dir fakebin fake_root
|
|
116
|
+
dir="$TMP_ROOT/$name"
|
|
117
|
+
fakebin="$dir/fakebin"
|
|
118
|
+
fake_root="$dir/root"
|
|
119
|
+
mkdir -p "$dir/home/state" "$dir/home/data" "$dir/home/config" "$dir/wt" "$fakebin" "$fake_root/bin"
|
|
120
|
+
cat > "$fake_root/bin/fm-guard.sh" <<'SH'
|
|
121
|
+
#!/usr/bin/env bash
|
|
122
|
+
printf 'guard\n' >> "$FM_TEST_GUARD_LOG"
|
|
123
|
+
SH
|
|
124
|
+
chmod +x "$fake_root/bin/fm-guard.sh"
|
|
125
|
+
cat > "$fakebin/gh" <<'SH'
|
|
126
|
+
#!/usr/bin/env bash
|
|
127
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_LOG"
|
|
128
|
+
case "${1:-} ${2:-}" in
|
|
129
|
+
"api graphql")
|
|
130
|
+
printf '%s\n' \
|
|
131
|
+
'state=MERGED' \
|
|
132
|
+
'merged=true' \
|
|
133
|
+
'queued=false' \
|
|
134
|
+
'base=main'
|
|
135
|
+
exit 0
|
|
136
|
+
;;
|
|
137
|
+
esac
|
|
138
|
+
case " $* " in
|
|
139
|
+
*" headRefOid "*) printf '%s\n' "${FM_TEST_GH_HEAD:-0123456789abcdef0123456789abcdef01234567}" ;;
|
|
140
|
+
*" state "*)
|
|
141
|
+
[ "${FM_TEST_GH_FAIL:-0}" = 0 ] || exit 1
|
|
142
|
+
[ "${FM_TEST_GH_SLEEP:-0}" = 0 ] || sleep "$FM_TEST_GH_SLEEP"
|
|
143
|
+
printf '%s\n' "${FM_TEST_GH_STATE:-OPEN}"
|
|
144
|
+
;;
|
|
145
|
+
esac
|
|
146
|
+
SH
|
|
147
|
+
cat > "$fakebin/gh-axi" <<'SH'
|
|
148
|
+
#!/usr/bin/env bash
|
|
149
|
+
printf '%s\n' "$*" >> "$FM_TEST_GH_AXI_LOG"
|
|
150
|
+
case "${1:-} ${2:-}" in
|
|
151
|
+
"pr view")
|
|
152
|
+
[ "$#" -eq 5 ] && [ "${4:-}" = --repo ] || exit 2
|
|
153
|
+
printf 'pull_request:\n number: %s\n state: %s\n' "$3" "${FM_TEST_GH_MERGE_STATE:-merged}"
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
156
|
+
exit "${FM_TEST_GH_AXI_RC:-0}"
|
|
157
|
+
SH
|
|
158
|
+
# Plain glab, reproducing the real CLI's contract: its field output on stdout
|
|
159
|
+
# and exit 0 on success, and a non-zero exit with no stdout on any failure.
|
|
160
|
+
cat > "$fakebin/glab" <<'SH'
|
|
161
|
+
#!/usr/bin/env bash
|
|
162
|
+
printf '%s\n' "$*" >> "$FM_TEST_GLAB_LOG"
|
|
163
|
+
[ "${FM_TEST_GLAB_FAIL:-0}" = 0 ] || exit 1
|
|
164
|
+
[ "${FM_TEST_GLAB_SLEEP:-0}" = 0 ] || sleep "$FM_TEST_GLAB_SLEEP"
|
|
165
|
+
printf 'title:\tfixture merge request\nstate:\t%s\nauthor:\tsomeone\n' "${FM_TEST_GLAB_STATE:-opened}"
|
|
166
|
+
SH
|
|
167
|
+
chmod +x "$fakebin/gh" "$fakebin/gh-axi" "$fakebin/glab"
|
|
168
|
+
: > "$dir/gh.log"
|
|
169
|
+
: > "$dir/gh-axi.log"
|
|
170
|
+
: > "$dir/glab.log"
|
|
171
|
+
: > "$dir/guard.log"
|
|
172
|
+
printf '%s\n' "$dir"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
write_task_meta() {
|
|
176
|
+
local dir=$1 id=${2:-task-a}
|
|
177
|
+
fm_write_meta "$dir/home/state/$id.meta" \
|
|
178
|
+
"window=firstmate:fm-$id" \
|
|
179
|
+
"endpoint_task_id=$id" \
|
|
180
|
+
"worktree=$dir/wt" \
|
|
181
|
+
"project=$dir/project" \
|
|
182
|
+
"kind=ship" \
|
|
183
|
+
"mode=no-mistakes"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
write_poll_meta() {
|
|
187
|
+
local state=$1 id=$2 url=$3
|
|
188
|
+
fm_write_meta "$state/$id.meta" \
|
|
189
|
+
"window=fm-$id" \
|
|
190
|
+
"pr=$url"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
run_check_entry() {
|
|
195
|
+
local dir=$1
|
|
196
|
+
shift
|
|
197
|
+
FM_ROOT_OVERRIDE="$dir/root" FM_HOME="$dir/home" \
|
|
198
|
+
FM_TEST_GUARD_LOG="$dir/guard.log" FM_TEST_GH_LOG="$dir/gh.log" \
|
|
199
|
+
FM_TEST_GH_AXI_LOG="$dir/gh-axi.log" FM_TEST_GLAB_LOG="$dir/glab.log" \
|
|
200
|
+
PATH="$dir/fakebin:$BASE_PATH" \
|
|
201
|
+
"$PR_CHECK" "$@"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
run_merge_entry() {
|
|
205
|
+
local dir=$1
|
|
206
|
+
shift
|
|
207
|
+
FM_ROOT_OVERRIDE="$dir/root" FM_HOME="$dir/home" \
|
|
208
|
+
FM_TEST_GUARD_LOG="$dir/guard.log" FM_TEST_GH_LOG="$dir/gh.log" \
|
|
209
|
+
FM_TEST_GH_AXI_LOG="$dir/gh-axi.log" FM_TEST_GLAB_LOG="$dir/glab.log" \
|
|
210
|
+
PATH="$dir/fakebin:$BASE_PATH" \
|
|
211
|
+
"$PR_MERGE" "$@"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# shellcheck disable=SC2016 # Literal rejected URL bytes are parser test data.
|
|
215
|
+
INVALID_URLS=(
|
|
216
|
+
'https://gitlab.com/single/-/merge_requests/1'
|
|
217
|
+
'https://gitlab.com/g/p/-/merge_requests/0'
|
|
218
|
+
'https://gitlab.com/g/p/-/merge_requests/01'
|
|
219
|
+
'https://GitLab.com/g/p/-/merge_requests/1'
|
|
220
|
+
'https://gitlab.com:443/g/p/-/merge_requests/1'
|
|
221
|
+
'https://user@gitlab.com/g/p/-/merge_requests/1'
|
|
222
|
+
'https://gitlab.com/g/p/-/merge_requests/1/'
|
|
223
|
+
'https://gitlab.com/-/p/-/merge_requests/1'
|
|
224
|
+
'https://gitlab.com/g/p.git/-/merge_requests/1'
|
|
225
|
+
'https://gitlab.com/g/p.atom/-/merge_requests/1'
|
|
226
|
+
'https://gitlab.com/g/p/-/merge_requests/1?x=1'
|
|
227
|
+
'https://gitlab.com/g/p/-/merge_requests/1#note'
|
|
228
|
+
'https://gitlab.com/g/p/-/issues/1'
|
|
229
|
+
'https://gitlab.com//p/-/merge_requests/1'
|
|
230
|
+
'https://.gitlab.com/g/p/-/merge_requests/1'
|
|
231
|
+
'https://gitlab.com./g/p/-/merge_requests/1'
|
|
232
|
+
'http://gitlab.com/g/p/-/merge_requests/1'
|
|
233
|
+
'https://github.com/o/r/pull/1/'
|
|
234
|
+
' https://github.com/o/r/pull/1'
|
|
235
|
+
'https://github.com/o/r/pull/1 '
|
|
236
|
+
'https://github.com/o /r/pull/1'
|
|
237
|
+
$'https://github.com/o/r/pull/1\t'
|
|
238
|
+
$'https://github.com/o/r/pull/1\r'
|
|
239
|
+
$'https://github.com/o/r/pull/1\nnext'
|
|
240
|
+
$'https://github.com/o/r/pull/1\r\nnext'
|
|
241
|
+
$'https://github.com/o/r/pull/1\001'
|
|
242
|
+
$'https://github.com/o/r/pull/1\033'
|
|
243
|
+
$'https://github.com/o/r/pull/1\177'
|
|
244
|
+
'https://user@github.com/o/r/pull/1'
|
|
245
|
+
'https://user:pass@github.com/o/r/pull/1'
|
|
246
|
+
'https://github.com:443/o/r/pull/1'
|
|
247
|
+
'https://github.com/o%2Fr/pull/1'
|
|
248
|
+
'https://github.com/o/r%2Fz/pull/1'
|
|
249
|
+
'https://github.com/o/r/pull/1%3Fq'
|
|
250
|
+
'https://github.com/o/r/pull/1%23f'
|
|
251
|
+
'https://github.com/o/r/pull/1%24x'
|
|
252
|
+
'https://github.com/o/r/pull/1%28x%29'
|
|
253
|
+
'https://github.com/o/r/pull/1%60x'
|
|
254
|
+
'https://github.com/o/r/pull/1%0D'
|
|
255
|
+
'https://github.com/o/r/pull/1%0A'
|
|
256
|
+
'https://github.com/o/r/pull/1%252Fz'
|
|
257
|
+
'https://github.com//r/pull/1'
|
|
258
|
+
'https://github.com/o//pull/1'
|
|
259
|
+
'https://github.com/o/r//1'
|
|
260
|
+
'https://github.com/o/r/1'
|
|
261
|
+
'https://github.com/o/r/pull/'
|
|
262
|
+
'https://github.com/-owner/r/pull/1'
|
|
263
|
+
'https://github.com/owner-/r/pull/1'
|
|
264
|
+
'https://github.com/owner--name/r/pull/1'
|
|
265
|
+
'https://github.com/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/r/pull/1'
|
|
266
|
+
'https://github.com/o/./pull/1'
|
|
267
|
+
'https://github.com/o/../pull/1'
|
|
268
|
+
'https://github.com/o/r+z/pull/1'
|
|
269
|
+
'https://github.com/o/r/pull/+1'
|
|
270
|
+
'https://github.com/o/r/pull/0'
|
|
271
|
+
'https://github.com/o/r/pull/-1'
|
|
272
|
+
'https://github.com/o/r/pull/01'
|
|
273
|
+
'https://github.com/o/r/pull/0x1'
|
|
274
|
+
'https://github.com/o/r/pull/1e2'
|
|
275
|
+
'https://github.com/o/r/pull/1.0'
|
|
276
|
+
'https://github.com/o/r/issues/1'
|
|
277
|
+
'https://github.com/o/r/x/pull/1'
|
|
278
|
+
'https://github.com/o/r/pull/1/files'
|
|
279
|
+
'https://github.com/o/r/pull/1?q=x'
|
|
280
|
+
'https://github.com/o/r/pull/1#f'
|
|
281
|
+
'https://github.com.evil/o/r/pull/1'
|
|
282
|
+
'https://evilgithub.com/o/r/pull/1'
|
|
283
|
+
'https://gıthub.com/o/r/pull/1'
|
|
284
|
+
'https://xn--gthub-3va.com/o/r/pull/1'
|
|
285
|
+
'http://github.com/o/r/pull/1'
|
|
286
|
+
'ssh://github.com/o/r/pull/1'
|
|
287
|
+
'git://github.com/o/r/pull/1'
|
|
288
|
+
'file://github.com/o/r/pull/1'
|
|
289
|
+
'//github.com/o/r/pull/1'
|
|
290
|
+
'HTTPS://github.com/o/r/pull/1'
|
|
291
|
+
'https://GitHub.com/o/r/pull/1'
|
|
292
|
+
'https://github.com/o$/r/pull/1'
|
|
293
|
+
'https://github.com/o(/r/pull/1'
|
|
294
|
+
'https://github.com/o)/r/pull/1'
|
|
295
|
+
'https://github.com/o`/r/pull/1'
|
|
296
|
+
'https://github.com/o/r`/pull/1'
|
|
297
|
+
'https://github.com/o/r/pull/1`'
|
|
298
|
+
"https://github.com/o/'r'/pull/1"
|
|
299
|
+
'https://github.com/o/"r"/pull/1'
|
|
300
|
+
'https://github.com/o/'\''"r"'\''/pull/1'
|
|
301
|
+
"https://github.com/o/r/pull/1'"
|
|
302
|
+
'https://github.com/o/r/pull/1"'
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# shellcheck disable=SC2016 # Literal shell syntax is task-ID test data.
|
|
306
|
+
INVALID_IDS=(
|
|
307
|
+
'../escape'
|
|
308
|
+
'a/b'
|
|
309
|
+
'.'
|
|
310
|
+
'..'
|
|
311
|
+
'.task'
|
|
312
|
+
'task a'
|
|
313
|
+
$'task\ta'
|
|
314
|
+
$'task\na'
|
|
315
|
+
'task*'
|
|
316
|
+
"task'a"
|
|
317
|
+
'task"a'
|
|
318
|
+
'task;a'
|
|
319
|
+
'task$a'
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# shellcheck disable=SC2016 # Literal shell syntax is task-ID test data.
|
|
323
|
+
UNSAFE_LIFECYCLE_IDS=(
|
|
324
|
+
'../escape'
|
|
325
|
+
'a/b'
|
|
326
|
+
'.'
|
|
327
|
+
'..'
|
|
328
|
+
'.task'
|
|
329
|
+
'task a'
|
|
330
|
+
$'task\ta'
|
|
331
|
+
$'task\na'
|
|
332
|
+
'task*'
|
|
333
|
+
"task'a"
|
|
334
|
+
'task"a'
|
|
335
|
+
'task;a'
|
|
336
|
+
'task$a'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
test_parser_matrix() {
|
|
340
|
+
local id row url owner repo number
|
|
341
|
+
while IFS='|' read -r url owner repo number; do
|
|
342
|
+
[ -n "$url" ] || continue
|
|
343
|
+
fm_pr_url_parse "$url" || fail "parser rejected canonical URL"
|
|
344
|
+
[ "$FM_PR_URL" = "$url" ] || fail "parser changed canonical URL"
|
|
345
|
+
[ "$FM_PR_OWNER" = "$owner" ] || fail "parser returned wrong owner"
|
|
346
|
+
[ "$FM_PR_REPO" = "$repo" ] || fail "parser returned wrong repository"
|
|
347
|
+
[ "$FM_PR_NUMBER" = "$number" ] || fail "parser returned wrong PR number"
|
|
348
|
+
done <<'EOF'
|
|
349
|
+
https://github.com/a/b/pull/1|a|b|1
|
|
350
|
+
https://github.com/my-org/repo/pull/42|my-org|repo|42
|
|
351
|
+
https://github.com/Owner/repo-name_with.parts/pull/123456|Owner|repo-name_with.parts|123456
|
|
352
|
+
EOF
|
|
353
|
+
while IFS='|' read -r url host path number; do
|
|
354
|
+
[ -n "$url" ] || continue
|
|
355
|
+
fm_pr_url_parse "$url" || fail "parser rejected a canonical merge request URL"
|
|
356
|
+
[ "$FM_PR_PROVIDER" = gitlab ] || fail "parser did not tag a merge request URL as gitlab"
|
|
357
|
+
[ "$FM_PR_URL" = "$url" ] || fail "parser changed a canonical merge request URL"
|
|
358
|
+
[ "$FM_PR_HOST" = "$host" ] || fail "parser returned wrong GitLab host"
|
|
359
|
+
[ "$FM_PR_PATH" = "$path" ] || fail "parser returned wrong GitLab project path"
|
|
360
|
+
[ "$FM_PR_NUMBER" = "$number" ] || fail "parser returned wrong merge request number"
|
|
361
|
+
[ -z "$FM_PR_OWNER" ] && [ -z "$FM_PR_REPO" ] \
|
|
362
|
+
|| fail "parser set GitHub owner/repository for a merge request URL"
|
|
363
|
+
done <<'EOF'
|
|
364
|
+
https://gitlab.com/group/project/-/merge_requests/1|gitlab.com|group/project|1
|
|
365
|
+
https://gitlab.com/group/sub/deep/project/-/merge_requests/42|gitlab.com|group/sub/deep/project|42
|
|
366
|
+
https://gitlab.example.co.uk/g/p/-/merge_requests/7|gitlab.example.co.uk|g/p|7
|
|
367
|
+
https://code.internal/team/tools/ci-runner/-/merge_requests/123456|code.internal|team/tools/ci-runner|123456
|
|
368
|
+
EOF
|
|
369
|
+
fm_pr_url_parse https://github.com/a/b/pull/1 || fail "parser rejected canonical URL"
|
|
370
|
+
[ "$FM_PR_PROVIDER" = github ] || fail "parser did not tag a pull request URL as github"
|
|
371
|
+
[ "$FM_PR_HOST" = github.com ] || fail "parser returned wrong GitHub host"
|
|
372
|
+
[ "$FM_PR_PATH" = a/b ] || fail "parser returned wrong GitHub project path"
|
|
373
|
+
for row in "${INVALID_URLS[@]}"; do
|
|
374
|
+
! fm_pr_url_parse "$row" || fail "parser accepted a rejected raw-byte URL class"
|
|
375
|
+
done
|
|
376
|
+
for id in -task task- task--a Task-a task_a task.a; do
|
|
377
|
+
fm_pr_task_id_valid "$id" || fail "task ID validator rejected a safe lifecycle-compatible slug"
|
|
378
|
+
done
|
|
379
|
+
fm_task_id_creation_valid _noncanonical \
|
|
380
|
+
|| fail "creation validator rejected a task ID after its reserved namespace moved"
|
|
381
|
+
id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
382
|
+
fm_pr_task_id_valid "$id" || fail "operational validator rejected a path-safe legacy task ID"
|
|
383
|
+
! fm_task_id_creation_valid "$id" || fail "creation validator accepted an overlong task ID"
|
|
384
|
+
pass "raw-byte parser accepts canonical URLs and rejects the complete adversarial matrix"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
test_invalid_entrypoints_have_zero_side_effects() {
|
|
388
|
+
local dir before after value rc
|
|
389
|
+
dir=$(make_case invalid-entrypoints)
|
|
390
|
+
write_task_meta "$dir"
|
|
391
|
+
printf 'existing-check\n' > "$dir/home/state/task-a.check.sh"
|
|
392
|
+
printf 'existing-data\n' > "$dir/home/state/task-a.pr-poll"
|
|
393
|
+
chmod 0600 "$dir/home/state/task-a.check.sh" "$dir/home/state/task-a.pr-poll"
|
|
394
|
+
|
|
395
|
+
for value in "${INVALID_URLS[@]}"; do
|
|
396
|
+
before=$(state_snapshot "$dir/home/state")
|
|
397
|
+
set +e
|
|
398
|
+
run_check_entry "$dir" task-a "$value" > "$dir/stdout" 2> "$dir/stderr"
|
|
399
|
+
rc=$?
|
|
400
|
+
set -e
|
|
401
|
+
[ "$rc" -ne 0 ] || fail "direct entrypoint accepted invalid URL"
|
|
402
|
+
[ "$(cat "$dir/stderr")" = 'error: invalid PR check request' ] || fail "direct invalid URL diagnostic was not fixed"
|
|
403
|
+
after=$(state_snapshot "$dir/home/state")
|
|
404
|
+
[ "$after" = "$before" ] || fail "direct invalid URL changed prior state"
|
|
405
|
+
done
|
|
406
|
+
|
|
407
|
+
for value in "${INVALID_IDS[@]}"; do
|
|
408
|
+
before=$(state_snapshot "$dir/home/state")
|
|
409
|
+
set +e
|
|
410
|
+
run_check_entry "$dir" "$value" https://github.com/o/r/pull/1 > "$dir/stdout" 2> "$dir/stderr"
|
|
411
|
+
rc=$?
|
|
412
|
+
set -e
|
|
413
|
+
[ "$rc" -ne 0 ] || fail "direct entrypoint accepted invalid task ID"
|
|
414
|
+
after=$(state_snapshot "$dir/home/state")
|
|
415
|
+
[ "$after" = "$before" ] || fail "invalid task ID changed state or traversed a path"
|
|
416
|
+
done
|
|
417
|
+
|
|
418
|
+
for value in "${INVALID_URLS[@]}"; do
|
|
419
|
+
before=$(state_snapshot "$dir/home/state")
|
|
420
|
+
set +e
|
|
421
|
+
run_merge_entry "$dir" task-a "$value" > "$dir/stdout" 2> "$dir/stderr"
|
|
422
|
+
rc=$?
|
|
423
|
+
set -e
|
|
424
|
+
[ "$rc" -ne 0 ] || fail "merge entrypoint accepted invalid URL"
|
|
425
|
+
[ "$(cat "$dir/stderr")" = 'error: invalid PR merge request' ] || fail "merge invalid URL diagnostic was not fixed"
|
|
426
|
+
after=$(state_snapshot "$dir/home/state")
|
|
427
|
+
[ "$after" = "$before" ] || fail "merge invalid URL changed prior state"
|
|
428
|
+
done
|
|
429
|
+
|
|
430
|
+
for value in "${INVALID_IDS[@]}"; do
|
|
431
|
+
before=$(state_snapshot "$dir/home/state")
|
|
432
|
+
set +e
|
|
433
|
+
run_merge_entry "$dir" "$value" https://github.com/o/r/pull/1 > "$dir/stdout" 2> "$dir/stderr"
|
|
434
|
+
rc=$?
|
|
435
|
+
set -e
|
|
436
|
+
[ "$rc" -ne 0 ] || fail "merge entrypoint accepted invalid task ID"
|
|
437
|
+
after=$(state_snapshot "$dir/home/state")
|
|
438
|
+
[ "$after" = "$before" ] || fail "merge invalid task ID changed state"
|
|
439
|
+
done
|
|
440
|
+
|
|
441
|
+
for value in "${UNSAFE_LIFECYCLE_IDS[@]}"; do
|
|
442
|
+
before=$(state_snapshot "$dir/home/state")
|
|
443
|
+
set +e
|
|
444
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$dir/root" FM_TEST_GUARD_LOG="$dir/guard.log" \
|
|
445
|
+
"$TEARDOWN" "$value" --force > "$dir/stdout" 2> "$dir/stderr"
|
|
446
|
+
rc=$?
|
|
447
|
+
set -e
|
|
448
|
+
[ "$rc" -ne 0 ] || fail "teardown accepted invalid task ID"
|
|
449
|
+
[ "$(cat "$dir/stderr")" = 'error: invalid teardown request' ] \
|
|
450
|
+
|| fail "teardown invalid task ID diagnostic was not fixed"
|
|
451
|
+
after=$(state_snapshot "$dir/home/state")
|
|
452
|
+
[ "$after" = "$before" ] || fail "teardown invalid task ID changed state"
|
|
453
|
+
done
|
|
454
|
+
|
|
455
|
+
set +e
|
|
456
|
+
run_check_entry "$dir" > /dev/null 2> "$dir/stderr"; rc=$?
|
|
457
|
+
set -e
|
|
458
|
+
[ "$rc" -ne 0 ] || fail "direct entrypoint accepted zero arguments"
|
|
459
|
+
set +e
|
|
460
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/1 extra > /dev/null 2> "$dir/stderr"; rc=$?
|
|
461
|
+
set -e
|
|
462
|
+
[ "$rc" -ne 0 ] || fail "direct entrypoint accepted extra arguments"
|
|
463
|
+
set +e
|
|
464
|
+
run_merge_entry "$dir" > /dev/null 2> "$dir/stderr"; rc=$?
|
|
465
|
+
set -e
|
|
466
|
+
[ "$rc" -ne 0 ] || fail "merge entrypoint accepted zero arguments"
|
|
467
|
+
|
|
468
|
+
[ ! -s "$dir/gh.log" ] || fail "invalid direct or merge data called gh"
|
|
469
|
+
[ ! -s "$dir/gh-axi.log" ] || fail "invalid direct or merge data called gh-axi"
|
|
470
|
+
[ ! -s "$dir/guard.log" ] || fail "invalid direct or merge data called the guard"
|
|
471
|
+
[ ! -e "$TMP_ROOT/escape.check.sh" ] || fail "task traversal wrote outside state"
|
|
472
|
+
pass "PR and teardown entrypoints reject invalid arguments before every side effect"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
test_valid_recording_and_merge_derivation() {
|
|
476
|
+
local dir expected sidecar count rc
|
|
477
|
+
dir=$(make_case valid-recording)
|
|
478
|
+
write_task_meta "$dir"
|
|
479
|
+
expected=0123456789abcdef0123456789abcdef01234567
|
|
480
|
+
FM_TEST_GH_HEAD=$expected run_check_entry "$dir" task-a https://github.com/my-org/repo_name.with-dots/pull/37 \
|
|
481
|
+
> "$dir/stdout" 2> "$dir/stderr" || fail "valid direct check failed"
|
|
482
|
+
|
|
483
|
+
grep -qxF 'pr=https://github.com/my-org/repo_name.with-dots/pull/37' "$dir/home/state/task-a.meta" \
|
|
484
|
+
|| fail "canonical pr metadata was not exact"
|
|
485
|
+
grep -qxF "pr_head=$expected" "$dir/home/state/task-a.meta" || fail "PR head metadata was not exact"
|
|
486
|
+
cmp -s "$POLL" "$dir/home/state/task-a.check.sh" || fail "published check was not byte-for-byte static"
|
|
487
|
+
[ "$(file_mode "$dir/home/state/task-a.check.sh")" = 600 ] || fail "published check mode was not 0600"
|
|
488
|
+
[ "$(file_mode "$dir/home/state/task-a.pr-poll")" = 600 ] || fail "published sidecar mode was not 0600"
|
|
489
|
+
[ "$(file_mode "$dir/home/state/task-a.pr-poll-registration")" = 600 ] \
|
|
490
|
+
|| fail "published registration mode was not 0600"
|
|
491
|
+
[ "$(fm_pr_file_link_count "$dir/home/state/task-a.check.sh")" = 1 ] \
|
|
492
|
+
&& [ "$(fm_pr_file_link_count "$dir/home/state/task-a.pr-poll")" = 1 ] \
|
|
493
|
+
&& [ "$(fm_pr_file_link_count "$dir/home/state/task-a.pr-poll-registration")" = 1 ] \
|
|
494
|
+
|| fail "published poll artifacts were not single-link files"
|
|
495
|
+
fm_pr_poll_artifacts_valid "$dir/home/state" task-a "$POLL" \
|
|
496
|
+
|| fail "published poll provenance or metadata binding was invalid"
|
|
497
|
+
sidecar=$(cat "$dir/home/state/task-a.pr-poll")
|
|
498
|
+
[ "$sidecar" = $'github\nhttps://github.com/my-org/repo_name.with-dots/pull/37\ngithub.com\nmy-org/repo_name.with-dots\n37' ] \
|
|
499
|
+
|| fail "published sidecar bytes were not exact"
|
|
500
|
+
|
|
501
|
+
FM_TEST_GH_HEAD=$expected run_check_entry "$dir" task-a https://github.com/my-org/repo_name.with-dots/pull/37 \
|
|
502
|
+
>/dev/null 2>/dev/null || fail "valid duplicate check failed"
|
|
503
|
+
count=$(grep -c '^pr=' "$dir/home/state/task-a.meta")
|
|
504
|
+
[ "$count" -eq 1 ] || fail "duplicate pr metadata was appended"
|
|
505
|
+
count=$(grep -c '^pr_head=' "$dir/home/state/task-a.meta")
|
|
506
|
+
[ "$count" -eq 1 ] || fail "duplicate pr_head metadata was appended"
|
|
507
|
+
|
|
508
|
+
: > "$dir/gh-axi.log"
|
|
509
|
+
run_merge_entry "$dir" task-a https://github.com/my-org/repo_name.with-dots/pull/37 -- --merge \
|
|
510
|
+
>/dev/null 2>/dev/null || fail "valid merge wrapper failed"
|
|
511
|
+
grep -qxF 'pr merge 37 --repo my-org/repo_name.with-dots --merge' "$dir/gh-axi.log" \
|
|
512
|
+
|| fail "merge wrapper did not preserve repository derivation and method"
|
|
513
|
+
# A merge this home performed leaves its own durable outcome, so the poll's
|
|
514
|
+
# confirmation is no longer the first the captain hears of it. Acknowledge that
|
|
515
|
+
# record before the watcher cycle below, which is what still retires the poll.
|
|
516
|
+
assert_grep 'https://github.com/my-org/repo_name.with-dots/pull/37' "$dir/home/state/.wake-queue" \
|
|
517
|
+
"a merge this home performed left no durable outcome"
|
|
518
|
+
ack_watcher_cycle "$dir/home/state" || fail "merge outcome acknowledgement failed"
|
|
519
|
+
# With the merge already reported, the poll's own detection is a duplicate the
|
|
520
|
+
# watcher absorbs, so this cycle needs its own reason to end.
|
|
521
|
+
add_stop_custom_check "$dir"
|
|
522
|
+
set +e
|
|
523
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/merged-watch.out" 2> "$dir/merged-watch.err"
|
|
524
|
+
rc=$?
|
|
525
|
+
set -e
|
|
526
|
+
[ "$rc" -eq 0 ] || fail "guarded merge poll retirement failed: $(cat "$dir/merged-watch.err")"
|
|
527
|
+
assert_poll_absent "$dir/home/state" task-a
|
|
528
|
+
assert_no_grep "merged-task-a" "$dir/home/state/.wake-queue" \
|
|
529
|
+
"the drained self-merge outcome was republished by its poll"
|
|
530
|
+
grep -qxF 'pr=https://github.com/my-org/repo_name.with-dots/pull/37' "$dir/home/state/task-a.meta" \
|
|
531
|
+
|| fail "guarded merge retirement removed pr metadata"
|
|
532
|
+
grep -qxF "pr_head=$expected" "$dir/home/state/task-a.meta" \
|
|
533
|
+
|| fail "guarded merge retirement removed pr_head metadata"
|
|
534
|
+
|
|
535
|
+
dir=$(make_case newline-head)
|
|
536
|
+
write_task_meta "$dir"
|
|
537
|
+
FM_TEST_GH_HEAD=$'0123456789abcdef0123456789abcdef01234567\nwindow=unexpected' \
|
|
538
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/2 >/dev/null 2>/dev/null \
|
|
539
|
+
|| fail "valid check with malformed remote head failed"
|
|
540
|
+
assert_no_grep 'pr_head=' "$dir/home/state/task-a.meta" "multiline PR head reached metadata"
|
|
541
|
+
assert_no_grep 'window=unexpected' "$dir/home/state/task-a.meta" "newline metadata key was injected"
|
|
542
|
+
|
|
543
|
+
dir=$(make_case lifecycle-compatible-id)
|
|
544
|
+
write_task_meta "$dir" Task_A.1
|
|
545
|
+
run_merge_entry "$dir" Task_A.1 https://github.com/o/r/pull/3 \
|
|
546
|
+
> "$dir/stdout" 2> "$dir/stderr" \
|
|
547
|
+
|| fail "safe lifecycle-compatible task ID could not use the PR merge flow"
|
|
548
|
+
fm_pr_poll_artifacts_valid "$dir/home/state" Task_A.1 "$POLL" \
|
|
549
|
+
|| fail "safe lifecycle-compatible task ID did not publish an authenticated poll"
|
|
550
|
+
rm -rf "$dir/wt"
|
|
551
|
+
cat > "$dir/fakebin/tmux" <<'SH'
|
|
552
|
+
#!/usr/bin/env bash
|
|
553
|
+
exit 0
|
|
554
|
+
SH
|
|
555
|
+
chmod 0700 "$dir/fakebin/tmux"
|
|
556
|
+
touch "$dir/home/state/.last-watcher-beat"
|
|
557
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" PATH="$dir/fakebin:$BASE_PATH" \
|
|
558
|
+
"$TEARDOWN" Task_A.1 --force > "$dir/teardown.out" 2> "$dir/teardown.err" \
|
|
559
|
+
|| fail "safe lifecycle-compatible task ID could not be torn down"
|
|
560
|
+
[ ! -e "$dir/home/state/Task_A.1.meta" ] \
|
|
561
|
+
|| fail "safe lifecycle-compatible task teardown retained metadata"
|
|
562
|
+
|
|
563
|
+
for id in _noncanonical aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
|
|
564
|
+
dir=$(make_case "legacy-teardown-${id:0:12}")
|
|
565
|
+
fm_write_meta "$dir/home/state/$id.meta" \
|
|
566
|
+
"window=firstmate:fm-$id" \
|
|
567
|
+
"endpoint_task_id=$id" \
|
|
568
|
+
"worktree=$dir/missing-worktree" \
|
|
569
|
+
"project=$dir/project" \
|
|
570
|
+
'kind=ship' \
|
|
571
|
+
'mode=local-only'
|
|
572
|
+
cat > "$dir/fakebin/tmux" <<'SH'
|
|
573
|
+
#!/usr/bin/env bash
|
|
574
|
+
exit 0
|
|
575
|
+
SH
|
|
576
|
+
chmod 0700 "$dir/fakebin/tmux"
|
|
577
|
+
touch "$dir/home/state/.last-watcher-beat"
|
|
578
|
+
mkdir "$dir/home/state/$id.check.sh"
|
|
579
|
+
set +e
|
|
580
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" PATH="$dir/fakebin:$BASE_PATH" \
|
|
581
|
+
"$TEARDOWN" "$id" --force > "$dir/unsafe-teardown.out" 2> "$dir/unsafe-teardown.err"
|
|
582
|
+
rc=$?
|
|
583
|
+
set -e
|
|
584
|
+
[ "$rc" -ne 0 ] || fail "legacy task teardown accepted an unsafe direct artifact"
|
|
585
|
+
[ -e "$dir/home/state/$id.meta" ] \
|
|
586
|
+
|| fail "legacy task teardown mutated lifecycle state before artifact refusal"
|
|
587
|
+
[ -d "$dir/home/state/$id.check.sh" ] \
|
|
588
|
+
|| fail "legacy task teardown changed the unsafe direct artifact"
|
|
589
|
+
rmdir "$dir/home/state/$id.check.sh"
|
|
590
|
+
FM_HOME="$dir/home" "$ROOT/bin/fm-x-link.sh" "$id" req-legacy \
|
|
591
|
+
--carry-count 0 --carry-ts 1700000000 --carry-platform x --carry-max 280 \
|
|
592
|
+
> "$dir/x-link.out" 2> "$dir/x-link.err" \
|
|
593
|
+
|| fail "path-safe legacy task ID could not link an X request"
|
|
594
|
+
run_merge_entry "$dir" "$id" https://github.com/o/r/pull/4 \
|
|
595
|
+
> "$dir/merge.out" 2> "$dir/merge.err" \
|
|
596
|
+
|| fail "path-safe legacy task ID could not use the PR merge flow"
|
|
597
|
+
fm_pr_poll_artifacts_valid "$dir/home/state" "$id" "$POLL" \
|
|
598
|
+
|| fail "path-safe legacy task ID did not publish an authenticated poll"
|
|
599
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" PATH="$dir/fakebin:$BASE_PATH" \
|
|
600
|
+
"$TEARDOWN" "$id" --force > "$dir/teardown.out" 2> "$dir/teardown.err" \
|
|
601
|
+
|| fail "legacy path-safe task ID could not be torn down"
|
|
602
|
+
[ ! -e "$dir/home/state/$id.meta" ] || fail "legacy task teardown retained metadata"
|
|
603
|
+
done
|
|
604
|
+
pass "valid direct and merge flows record exact metadata and reject multiline head metadata"
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
run_watcher_bounded() {
|
|
608
|
+
local home=$1 fakebin=$2 check_interval=${FM_TEST_CHECK_INTERVAL:-0} watch_root=${FM_TEST_WATCH_ROOT:-$ROOT}
|
|
609
|
+
shift 2
|
|
610
|
+
perl -e 'my $pid=fork; die unless defined $pid; if (!$pid) { exec @ARGV } local $SIG{ALRM}=sub { kill "TERM", $pid; waitpid $pid, 0; exit 124 }; alarm 10; waitpid $pid, 0; alarm 0; exit($? >> 8)' \
|
|
611
|
+
env FM_HOME="$home" FM_ROOT_OVERRIDE="$watch_root" FM_CHECK_INTERVAL="$check_interval" FM_CHECK_TIMEOUT=1 \
|
|
612
|
+
FM_POLL=0.02 FM_HEARTBEAT=999999 FM_SIGNAL_GRACE=0 PATH="$fakebin:$BASE_PATH" "$WATCH" "$@"
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
test_rejected_metacharacter_bytes_are_inert() {
|
|
616
|
+
local dir family rc before after
|
|
617
|
+
dir=$(make_case rejected-metacharacters)
|
|
618
|
+
write_task_meta "$dir"
|
|
619
|
+
write_poll_meta "$dir/home/state" safe-check https://github.com/o/r/pull/99
|
|
620
|
+
families=(
|
|
621
|
+
'https://github.com/o$/r/pull/1'
|
|
622
|
+
'https://github.com/o(/r/pull/1'
|
|
623
|
+
'https://github.com/o)/r/pull/1'
|
|
624
|
+
'https://github.com/o`/r/pull/1'
|
|
625
|
+
)
|
|
626
|
+
for family in "${families[@]}"; do
|
|
627
|
+
rm -f "$dir/home/state/task-a.check.sh" "$dir/home/state/task-a.pr-poll"
|
|
628
|
+
set +e
|
|
629
|
+
run_check_entry "$dir" task-a "$family" > /dev/null 2> "$dir/stderr"
|
|
630
|
+
rc=$?
|
|
631
|
+
set -e
|
|
632
|
+
[ "$rc" -ne 0 ] || fail "rejected metacharacter byte was accepted"
|
|
633
|
+
[ ! -e "$dir/home/state/task-a.check.sh" ] || fail "rejected input left a runnable task check"
|
|
634
|
+
[ ! -e "$dir/home/state/task-a.pr-poll" ] || fail "rejected input left a sidecar"
|
|
635
|
+
fm_pr_poll_prepare "$dir/home/state" safe-check github https://github.com/o/r/pull/99 github.com o/r 99 "$POLL" \
|
|
636
|
+
|| fail "could not prepare bounded watcher poll"
|
|
637
|
+
fm_pr_poll_publish_prepared || fail "could not publish bounded watcher poll"
|
|
638
|
+
|
|
639
|
+
set +e
|
|
640
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
641
|
+
rc=$?
|
|
642
|
+
set -e
|
|
643
|
+
[ "$rc" -eq 0 ] || fail "bounded watcher did not complete through the authenticated poll"
|
|
644
|
+
rm -f "$dir/home/state/.last-check"
|
|
645
|
+
done
|
|
646
|
+
|
|
647
|
+
FM_TEST_GH_STATE=OPEN run_check_entry "$dir" task-a https://github.com/o/r/pull/1 >/dev/null 2>/dev/null \
|
|
648
|
+
|| fail "could not seed a prior valid static poll"
|
|
649
|
+
before=$(state_snapshot "$dir/home/state")
|
|
650
|
+
set +e
|
|
651
|
+
run_check_entry "$dir" task-a "${families[0]}" >/dev/null 2>/dev/null
|
|
652
|
+
rc=$?
|
|
653
|
+
set -e
|
|
654
|
+
[ "$rc" -ne 0 ] || fail "rejected replacement was accepted"
|
|
655
|
+
after=$(state_snapshot "$dir/home/state")
|
|
656
|
+
[ "$after" = "$before" ] || fail "rejected replacement changed a prior valid static poll"
|
|
657
|
+
pass "rejected metacharacter bytes remain inert at generation and watcher time"
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
make_poll_fixture() {
|
|
661
|
+
local dir=$1
|
|
662
|
+
cp "$POLL" "$dir/home/state/task-a.check.sh"
|
|
663
|
+
printf '%s\n%s\n%s\n%s\n%s\n' \
|
|
664
|
+
github https://github.com/o/r/pull/1 github.com o/r 1 > "$dir/home/state/task-a.pr-poll"
|
|
665
|
+
chmod 0600 "$dir/home/state/task-a.check.sh" "$dir/home/state/task-a.pr-poll"
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
run_poll() {
|
|
669
|
+
local dir=$1
|
|
670
|
+
FM_TEST_GH_LOG="$dir/gh.log" FM_TEST_GLAB_LOG="$dir/glab.log" \
|
|
671
|
+
PATH="$dir/fakebin:$BASE_PATH" \
|
|
672
|
+
bash "$dir/home/state/task-a.check.sh"
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
test_static_poll_contract() {
|
|
676
|
+
local dir state out rc
|
|
677
|
+
dir=$(make_case poll-contract)
|
|
678
|
+
make_poll_fixture "$dir"
|
|
679
|
+
|
|
680
|
+
for state in OPEN CLOSED EMPTY MALFORMED; do
|
|
681
|
+
case "$state" in
|
|
682
|
+
EMPTY) value= ;;
|
|
683
|
+
MALFORMED) value='not-a-state' ;;
|
|
684
|
+
*) value=$state ;;
|
|
685
|
+
esac
|
|
686
|
+
out=$(FM_TEST_GH_STATE="$value" run_poll "$dir")
|
|
687
|
+
[ -z "$out" ] || fail "static poll emitted for non-merged state"
|
|
688
|
+
done
|
|
689
|
+
out=$(FM_TEST_GH_STATE=MERGED run_poll "$dir")
|
|
690
|
+
[ "$out" = merged ] || fail "static poll did not emit exactly one merged line"
|
|
691
|
+
out=$(FM_TEST_GH_FAIL=1 run_poll "$dir")
|
|
692
|
+
[ -z "$out" ] || fail "static poll emitted after gh failure"
|
|
693
|
+
|
|
694
|
+
mv "$dir/home/state/task-a.pr-poll" "$dir/home/state/task-a.pr-poll.missing"
|
|
695
|
+
out=$(run_poll "$dir")
|
|
696
|
+
[ -z "$out" ] || fail "static poll emitted with missing sidecar"
|
|
697
|
+
mv "$dir/home/state/task-a.pr-poll.missing" "$dir/home/state/task-a.pr-poll"
|
|
698
|
+
printf '%s\n%s\n%s\n%s\n%s\n%s\n' github https://github.com/o/r/pull/1 github.com o/r 1 extra > "$dir/home/state/task-a.pr-poll"
|
|
699
|
+
out=$(FM_TEST_GH_STATE=MERGED run_poll "$dir")
|
|
700
|
+
[ -z "$out" ] || fail "static poll emitted with multiline sidecar"
|
|
701
|
+
printf '%s\n%s\n%s\n%s\n%s\n' github https://github.com/o/r/pull/1x github.com o/r 1x > "$dir/home/state/task-a.pr-poll"
|
|
702
|
+
out=$(FM_TEST_GH_STATE=MERGED run_poll "$dir")
|
|
703
|
+
[ -z "$out" ] || fail "static poll emitted with malformed numeric data"
|
|
704
|
+
|
|
705
|
+
make_poll_fixture "$dir"
|
|
706
|
+
set +e
|
|
707
|
+
out=$(FM_STATE_OVERRIDE="$dir/home/state" FM_CHECK_TIMEOUT=1 FM_TEST_GH_LOG="$dir/gh.log" \
|
|
708
|
+
FM_TEST_GH_SLEEP=3 PATH="$dir/fakebin:$BASE_PATH" \
|
|
709
|
+
bash -c '. "$1"; run_check "$2"' bash "$WATCH" "$dir/home/state/task-a.check.sh")
|
|
710
|
+
rc=$?
|
|
711
|
+
set -e
|
|
712
|
+
[ "$rc" -eq 0 ] || fail "watcher run_check timeout wrapper failed"
|
|
713
|
+
[ -z "$out" ] || fail "timed-out static poll emitted output"
|
|
714
|
+
|
|
715
|
+
write_poll_meta "$dir/home/state" task-a https://github.com/o/r/pull/1
|
|
716
|
+
fm_pr_poll_prepare "$dir/home/state" task-a github https://github.com/o/r/pull/1 github.com o/r 1 "$POLL" \
|
|
717
|
+
|| fail "could not prepare authenticated watcher poll"
|
|
718
|
+
fm_pr_poll_publish_prepared || fail "could not publish authenticated watcher poll"
|
|
719
|
+
rm -f "$dir/home/state/.last-check"
|
|
720
|
+
set +e
|
|
721
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
722
|
+
rc=$?
|
|
723
|
+
set -e
|
|
724
|
+
[ "$rc" -eq 0 ] || fail "watcher did not surface merged poll"
|
|
725
|
+
[ "$(grep -c '^check: .*: merged$' "$dir/watch.out")" -eq 1 ] || fail "watcher did not convert merged output into exactly one wake"
|
|
726
|
+
pass "static poll is silent except for one merged line and remains watcher-bounded"
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
test_atomic_interruption_leaves_no_partial_artifact() {
|
|
730
|
+
local dir rc
|
|
731
|
+
dir=$(make_case interrupted-write)
|
|
732
|
+
write_task_meta "$dir"
|
|
733
|
+
cat > "$dir/fakebin/cp" <<SH
|
|
734
|
+
#!/usr/bin/env bash
|
|
735
|
+
'$REAL_CP' "\$@" || exit 1
|
|
736
|
+
kill -TERM "\$PPID"
|
|
737
|
+
exit 0
|
|
738
|
+
SH
|
|
739
|
+
chmod +x "$dir/fakebin/cp"
|
|
740
|
+
|
|
741
|
+
set +e
|
|
742
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/1 > "$dir/stdout" 2> "$dir/stderr"
|
|
743
|
+
rc=$?
|
|
744
|
+
set -e
|
|
745
|
+
[ "$rc" -ne 0 ] || fail "interrupted publication unexpectedly succeeded"
|
|
746
|
+
[ ! -e "$dir/home/state/task-a.check.sh" ] || fail "interrupted publication left a runnable check"
|
|
747
|
+
[ ! -e "$dir/home/state/task-a.pr-poll" ] || fail "interrupted publication left a sidecar"
|
|
748
|
+
[ ! -e "$dir/home/state/task-a.pr-poll-registration" ] \
|
|
749
|
+
|| fail "interrupted publication left a registration"
|
|
750
|
+
! find "$dir/home/state" -name '.fm-pr-poll-*' -print | grep . >/dev/null \
|
|
751
|
+
|| fail "interrupted publication left temporary files"
|
|
752
|
+
assert_no_grep 'pr=' "$dir/home/state/task-a.meta" "interrupted preparation changed metadata"
|
|
753
|
+
pass "interrupted atomic preparation cleans private temporaries and publishes nothing"
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
test_concurrent_watcher_sees_only_complete_publication() {
|
|
757
|
+
local n dir direct_pid rc i
|
|
758
|
+
n=1
|
|
759
|
+
while [ "$n" -le 3 ]; do
|
|
760
|
+
dir=$(make_case "concurrent-$n")
|
|
761
|
+
write_task_meta "$dir"
|
|
762
|
+
cat > "$dir/fakebin/cp" <<SH
|
|
763
|
+
#!/usr/bin/env bash
|
|
764
|
+
'$REAL_CP' "\$@" || exit 1
|
|
765
|
+
sleep 0.3
|
|
766
|
+
SH
|
|
767
|
+
chmod +x "$dir/fakebin/cp"
|
|
768
|
+
|
|
769
|
+
FM_TEST_GH_HEAD=0123456789abcdef0123456789abcdef01234567 \
|
|
770
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/1 > "$dir/direct.out" 2> "$dir/direct.err" &
|
|
771
|
+
direct_pid=$!
|
|
772
|
+
i=0
|
|
773
|
+
while [ "$i" -lt 100 ] && ! find "$dir/home/state" -name '.fm-pr-poll-check.*' -print | grep . >/dev/null; do
|
|
774
|
+
sleep 0.01
|
|
775
|
+
i=$((i + 1))
|
|
776
|
+
done
|
|
777
|
+
[ "$i" -lt 100 ] || fail "atomic publication did not reach staged check"
|
|
778
|
+
|
|
779
|
+
set +e
|
|
780
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
781
|
+
rc=$?
|
|
782
|
+
set -e
|
|
783
|
+
wait "$direct_pid" || fail "concurrent direct arming failed"
|
|
784
|
+
[ "$rc" -eq 0 ] || fail "concurrent watcher did not complete"
|
|
785
|
+
grep -q '^check: .*: merged$' "$dir/watch.out" || fail "concurrent watcher never saw complete poll"
|
|
786
|
+
[ ! -s "$dir/watch.err" ] || fail "concurrent watcher observed a partial artifact error"
|
|
787
|
+
if [ -e "$dir/home/state/task-a.check.sh" ]; then
|
|
788
|
+
cmp -s "$POLL" "$dir/home/state/task-a.check.sh" || fail "concurrent publication check bytes changed"
|
|
789
|
+
[ "$(file_mode "$dir/home/state/task-a.check.sh")" = 600 ] || fail "concurrent check mode was not private"
|
|
790
|
+
[ "$(file_mode "$dir/home/state/task-a.pr-poll")" = 600 ] || fail "concurrent sidecar mode was not private"
|
|
791
|
+
[ "$(file_mode "$dir/home/state/task-a.pr-poll-registration")" = 600 ] \
|
|
792
|
+
|| fail "concurrent registration mode was not private"
|
|
793
|
+
fm_pr_poll_artifacts_valid "$dir/home/state" task-a "$POLL" \
|
|
794
|
+
|| fail "concurrent publication did not leave canonical provenance"
|
|
795
|
+
else
|
|
796
|
+
assert_poll_absent "$dir/home/state" task-a
|
|
797
|
+
fi
|
|
798
|
+
n=$((n + 1))
|
|
799
|
+
done
|
|
800
|
+
pass "concurrent watchers observe only complete private poll publications"
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
test_poll_publication_refuses_unsafe_destinations() {
|
|
804
|
+
local artifact kind dir state destination
|
|
805
|
+
for artifact in task-a.pr-poll task-a.pr-poll-registration task-a.check.sh; do
|
|
806
|
+
for kind in regular dangling directory; do
|
|
807
|
+
dir=$(make_case "poll-path-${artifact//./-}-$kind")
|
|
808
|
+
state="$dir/home/state"
|
|
809
|
+
fm_pr_poll_prepare "$state" task-a github https://github.com/o/r/pull/1 github.com o/r 1 "$POLL" \
|
|
810
|
+
|| fail "could not stage poll symlink refusal fixture"
|
|
811
|
+
destination="$state/$artifact"
|
|
812
|
+
make_private_symlink "$dir" "$destination" "$kind"
|
|
813
|
+
if fm_pr_poll_publish_prepared; then
|
|
814
|
+
fail "poll publication accepted a private destination symlink"
|
|
815
|
+
fi
|
|
816
|
+
fm_pr_poll_cleanup
|
|
817
|
+
assert_private_symlink_unchanged "$destination"
|
|
818
|
+
[ ! -e "$state/task-a.pr-poll" ] || [ "$artifact" = task-a.pr-poll ] \
|
|
819
|
+
|| fail "check destination refusal published the sidecar"
|
|
820
|
+
done
|
|
821
|
+
|
|
822
|
+
dir=$(make_case "poll-path-${artifact//./-}-direct-directory")
|
|
823
|
+
state="$dir/home/state"
|
|
824
|
+
fm_pr_poll_prepare "$state" task-a github https://github.com/o/r/pull/1 github.com o/r 1 "$POLL" \
|
|
825
|
+
|| fail "could not stage poll directory refusal fixture"
|
|
826
|
+
destination="$state/$artifact"
|
|
827
|
+
mkdir "$destination"
|
|
828
|
+
if fm_pr_poll_publish_prepared; then
|
|
829
|
+
fail "poll publication accepted a directory destination"
|
|
830
|
+
fi
|
|
831
|
+
fm_pr_poll_cleanup
|
|
832
|
+
[ -d "$destination" ] || fail "poll publication replaced a directory destination"
|
|
833
|
+
[ -z "$(find "$destination" -mindepth 1 -maxdepth 1 -print)" ] \
|
|
834
|
+
|| fail "poll publication wrote inside a directory destination"
|
|
835
|
+
done
|
|
836
|
+
pass "poll publication paths refuse symlinks and directories"
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
test_live_artifact_single_link_and_privacy_validation() {
|
|
840
|
+
local artifact dir state alias rc
|
|
841
|
+
for artifact in check.sh pr-poll pr-poll-registration; do
|
|
842
|
+
dir=$(make_case "single-link-live-${artifact//./-}")
|
|
843
|
+
state="$dir/home/state"
|
|
844
|
+
write_task_meta "$dir"
|
|
845
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/10 >/dev/null 2>/dev/null \
|
|
846
|
+
|| fail "could not publish $artifact hard-link fixture"
|
|
847
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$POLL" \
|
|
848
|
+
|| fail "$artifact fixture was not initially authenticated"
|
|
849
|
+
alias="$dir/$artifact.alias"
|
|
850
|
+
ln "$state/task-a.$artifact" "$alias"
|
|
851
|
+
if [ "$artifact" = pr-poll ]; then
|
|
852
|
+
printf '%s\n%s\n%s\n%s\n' https://github.com/o/r/pull/11 o r 11 > "$alias"
|
|
853
|
+
fi
|
|
854
|
+
! fm_pr_poll_artifacts_valid "$state" task-a "$POLL" \
|
|
855
|
+
|| fail "$artifact hard link remained authenticated"
|
|
856
|
+
[ -e "$alias" ] || fail "$artifact hard-link refusal removed the external alias"
|
|
857
|
+
done
|
|
858
|
+
|
|
859
|
+
dir=$(make_case single-link-custom-check-registration)
|
|
860
|
+
state="$dir/home/state"
|
|
861
|
+
printf '#!/usr/bin/env bash\nprintf "custom-ready\\n"\n' > "$state/custom.check.sh"
|
|
862
|
+
chmod 0700 "$state/custom.check.sh"
|
|
863
|
+
alias="$dir/custom-check.alias"
|
|
864
|
+
ln "$state/custom.check.sh" "$alias"
|
|
865
|
+
set +e
|
|
866
|
+
FM_HOME="$dir/home" "$REGISTER" custom > "$dir/register.out" 2> "$dir/register.err"
|
|
867
|
+
rc=$?
|
|
868
|
+
set -e
|
|
869
|
+
[ "$rc" -ne 0 ] || fail "custom check registration accepted a hard-linked source"
|
|
870
|
+
[ ! -e "$state/custom.check-trust" ] || fail "rejected hard-linked custom check received a trust record"
|
|
871
|
+
rm -f "$alias"
|
|
872
|
+
FM_HOME="$dir/home" "$REGISTER" custom >/dev/null \
|
|
873
|
+
|| fail "could not register the custom check single-link fixture"
|
|
874
|
+
ln "$state/custom.check.sh" "$alias"
|
|
875
|
+
! fm_custom_check_registered "$state" custom \
|
|
876
|
+
|| fail "registered custom check remained authenticated after source hard-linking"
|
|
877
|
+
! fm_custom_check_snapshot_prepare "$state" custom \
|
|
878
|
+
|| fail "watcher snapshot accepted a hard-linked custom check source"
|
|
879
|
+
fm_custom_check_snapshot_cleanup
|
|
880
|
+
rm -f "$alias"
|
|
881
|
+
alias="$dir/custom-trust.alias"
|
|
882
|
+
ln "$state/custom.check-trust" "$alias"
|
|
883
|
+
! fm_custom_check_registered "$state" custom \
|
|
884
|
+
|| fail "hard-linked custom check trust remained authenticated"
|
|
885
|
+
! fm_custom_check_snapshot_prepare "$state" custom \
|
|
886
|
+
|| fail "watcher snapshot accepted a hard-linked custom check trust record"
|
|
887
|
+
fm_custom_check_snapshot_cleanup
|
|
888
|
+
[ -e "$alias" ] || fail "custom-check hard-link refusal removed the external alias"
|
|
889
|
+
|
|
890
|
+
dir=$(make_case private-custom-check-source)
|
|
891
|
+
state="$dir/home/state"
|
|
892
|
+
printf '#!/usr/bin/env bash\nprintf "custom-ready\\n"\n' > "$state/custom.check.sh"
|
|
893
|
+
chmod 0755 "$state/custom.check.sh"
|
|
894
|
+
set +e
|
|
895
|
+
FM_HOME="$dir/home" "$REGISTER" custom > "$dir/register.out" 2> "$dir/register.err"
|
|
896
|
+
rc=$?
|
|
897
|
+
set -e
|
|
898
|
+
[ "$rc" -ne 0 ] || fail "custom check registration accepted a non-private source"
|
|
899
|
+
[ ! -e "$state/custom.check-trust" ] || fail "non-private custom check received a trust record"
|
|
900
|
+
chmod 0700 "$state/custom.check.sh"
|
|
901
|
+
FM_HOME="$dir/home" "$REGISTER" custom >/dev/null \
|
|
902
|
+
|| fail "could not register private custom check fixture"
|
|
903
|
+
chmod 0755 "$state/custom.check.sh"
|
|
904
|
+
! fm_custom_check_registered "$state" custom \
|
|
905
|
+
|| fail "registered custom check remained authenticated after becoming non-private"
|
|
906
|
+
! fm_custom_check_snapshot_prepare "$state" custom \
|
|
907
|
+
|| fail "watcher snapshot accepted a non-private custom check source"
|
|
908
|
+
fm_custom_check_snapshot_cleanup
|
|
909
|
+
pass "live poll and custom-check artifacts require private single-link files"
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
install_final_publication_fault() {
|
|
913
|
+
local dir=$1
|
|
914
|
+
cat > "$dir/fakebin/mv" <<'SH'
|
|
915
|
+
#!/usr/bin/env bash
|
|
916
|
+
last=${!#}
|
|
917
|
+
"${FM_TEST_REAL_MV:?}" "$@" || exit $?
|
|
918
|
+
[ "$last" = "${FM_TEST_FINAL_PATH:?}" ] || exit 0
|
|
919
|
+
case "${FM_TEST_FINAL_ACTION:?}" in
|
|
920
|
+
type)
|
|
921
|
+
rm -f -- "$last"
|
|
922
|
+
ln -s "${FM_TEST_FAULT_LINK_TARGET:?}" "$last"
|
|
923
|
+
;;
|
|
924
|
+
mode) "${FM_TEST_REAL_CHMOD:?}" 0644 "$last" ;;
|
|
925
|
+
content) printf 'faulted final bytes\n' > "$last" ;;
|
|
926
|
+
device) : > "${FM_TEST_FAULT_GATE:?}" ;;
|
|
927
|
+
*) exit 2 ;;
|
|
928
|
+
esac
|
|
929
|
+
SH
|
|
930
|
+
cat > "$dir/fakebin/stat" <<'SH'
|
|
931
|
+
#!/usr/bin/env bash
|
|
932
|
+
last=${!#}
|
|
933
|
+
if [ "$last" = "${FM_TEST_FINAL_PATH:-}" ] && [ -e "${FM_TEST_FAULT_GATE:-/nonexistent}" ]; then
|
|
934
|
+
case " $* " in
|
|
935
|
+
*" %d "*) printf '%s\n' 999999; exit 0 ;;
|
|
936
|
+
esac
|
|
937
|
+
fi
|
|
938
|
+
exec "${FM_TEST_REAL_STAT:?}" "$@"
|
|
939
|
+
SH
|
|
940
|
+
chmod +x "$dir/fakebin/mv" "$dir/fakebin/stat"
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
assert_no_final_poll() {
|
|
944
|
+
local state=$1
|
|
945
|
+
[ ! -e "$state/task-a.check.sh" ] && [ ! -L "$state/task-a.check.sh" ] \
|
|
946
|
+
|| fail "failed publication left a runnable check name"
|
|
947
|
+
[ ! -e "$state/task-a.pr-poll" ] && [ ! -L "$state/task-a.pr-poll" ] \
|
|
948
|
+
|| fail "failed publication left a sidecar name"
|
|
949
|
+
[ ! -e "$state/task-a.pr-poll-registration" ] && [ ! -L "$state/task-a.pr-poll-registration" ] \
|
|
950
|
+
|| fail "failed publication left a registration name"
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
test_postrename_poll_validation_revokes_and_retries() {
|
|
954
|
+
local artifact action dir state destination link_target gate
|
|
955
|
+
for artifact in data registration check; do
|
|
956
|
+
for action in type mode device content; do
|
|
957
|
+
dir=$(make_case "poll-final-$artifact-$action")
|
|
958
|
+
state="$dir/home/state"
|
|
959
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/1
|
|
960
|
+
fm_pr_poll_prepare "$state" task-a github https://github.com/o/r/pull/1 github.com o/r 1 "$POLL" \
|
|
961
|
+
|| fail "could not prepare prior poll"
|
|
962
|
+
fm_pr_poll_publish_prepared || fail "could not publish prior poll"
|
|
963
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/2
|
|
964
|
+
fm_pr_poll_prepare "$state" task-a github https://github.com/o/r/pull/2 github.com o/r 2 "$POLL" \
|
|
965
|
+
|| fail "could not stage replacement poll"
|
|
966
|
+
case "$artifact" in
|
|
967
|
+
data) destination="$state/task-a.pr-poll" ;;
|
|
968
|
+
registration) destination="$state/task-a.pr-poll-registration" ;;
|
|
969
|
+
check) destination="$state/task-a.check.sh" ;;
|
|
970
|
+
esac
|
|
971
|
+
link_target="$dir/external-sentinel"
|
|
972
|
+
gate="$dir/device-fault"
|
|
973
|
+
printf 'external sentinel\n' > "$link_target"
|
|
974
|
+
chmod 0644 "$link_target"
|
|
975
|
+
install_final_publication_fault "$dir"
|
|
976
|
+
if FM_TEST_FINAL_PATH="$destination" FM_TEST_FINAL_ACTION="$action" \
|
|
977
|
+
FM_TEST_FAULT_LINK_TARGET="$link_target" FM_TEST_FAULT_GATE="$gate" \
|
|
978
|
+
FM_TEST_REAL_MV="$REAL_MV" FM_TEST_REAL_STAT="$REAL_STAT" FM_TEST_REAL_CHMOD="$REAL_CHMOD" \
|
|
979
|
+
PATH="$dir/fakebin:$BASE_PATH" fm_pr_poll_publish_prepared; then
|
|
980
|
+
fail "post-rename $artifact $action fault was reported as success"
|
|
981
|
+
fi
|
|
982
|
+
fm_pr_poll_cleanup
|
|
983
|
+
assert_no_final_poll "$state"
|
|
984
|
+
[ "$(cat "$link_target")" = 'external sentinel' ] || fail "poll type fault changed an external target"
|
|
985
|
+
[ "$(file_mode "$link_target")" = 644 ] || fail "poll type fault changed an external target mode"
|
|
986
|
+
|
|
987
|
+
fm_pr_poll_prepare "$state" task-a github https://github.com/o/r/pull/2 github.com o/r 2 "$POLL" \
|
|
988
|
+
|| fail "could not prepare poll retry"
|
|
989
|
+
PATH="$BASE_PATH" fm_pr_poll_publish_prepared || fail "poll retry did not recover after final validation fault"
|
|
990
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$POLL" || fail "poll retry did not publish a valid pair"
|
|
991
|
+
done
|
|
992
|
+
done
|
|
993
|
+
pass "post-rename poll validation faults revoke both names and allow a clean retry"
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
test_bootstrap_leaves_unauthenticated_checks() {
|
|
997
|
+
local dir state
|
|
998
|
+
dir=$(make_case bootstrap-no-legacy-rewrite)
|
|
999
|
+
state="$dir/home/state"
|
|
1000
|
+
fm_write_meta "$state/task-a.meta" \
|
|
1001
|
+
'window=fm-task-a' \
|
|
1002
|
+
'pr=https://github.com/o/r/pull/11'
|
|
1003
|
+
printf 'legacy bytes\n' > "$state/task-a.check.sh"
|
|
1004
|
+
chmod 0700 "$state/task-a.check.sh"
|
|
1005
|
+
|
|
1006
|
+
mkdir -p "$dir/home/config"
|
|
1007
|
+
printf '%s\n' manual > "$dir/home/config/backlog-backend"
|
|
1008
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" FM_BOOTSTRAP_NETWORK=skip \
|
|
1009
|
+
PATH="$dir/fakebin:$BASE_PATH" \
|
|
1010
|
+
"$ROOT/bin/fm-bootstrap.sh" > "$dir/bootstrap.out" 2> "$dir/bootstrap.err" \
|
|
1011
|
+
|| fail "bootstrap failed after migration retirement"
|
|
1012
|
+
[ "$(cat "$state/task-a.check.sh")" = 'legacy bytes' ] \
|
|
1013
|
+
|| fail "bootstrap rewrote an unauthenticated check after migration retirement"
|
|
1014
|
+
assert_no_grep 'PR_CHECK_MIGRATION' "$dir/bootstrap.out" \
|
|
1015
|
+
"bootstrap still emitted a retired migration diagnostic on stdout"
|
|
1016
|
+
assert_no_grep 'PR_CHECK_MIGRATION' "$dir/bootstrap.err" \
|
|
1017
|
+
"bootstrap still emitted a retired migration diagnostic on stderr"
|
|
1018
|
+
pass "bootstrap does not rewrite unauthenticated checks or emit retired migration diagnostics"
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
test_custom_snapshot_cleanup_on_signal() {
|
|
1022
|
+
local dir state child_pid_file pid child_pid i rc
|
|
1023
|
+
dir=$(make_case custom-snapshot-signal)
|
|
1024
|
+
state="$dir/home/state"
|
|
1025
|
+
child_pid_file="$dir/custom-child.pid"
|
|
1026
|
+
# shellcheck disable=SC2016 # The generated child expands $$ when it runs.
|
|
1027
|
+
printf '%s\n' '#!/usr/bin/env bash' 'trap "" TERM' \
|
|
1028
|
+
'printf "%s\n" "$$" > "$FM_TEST_CUSTOM_CHILD_PID"' 'while :; do sleep 1; done' \
|
|
1029
|
+
> "$state/custom.check.sh"
|
|
1030
|
+
chmod 0700 "$state/custom.check.sh"
|
|
1031
|
+
cat > "$dir/fakebin/timeout" <<'SH'
|
|
1032
|
+
#!/usr/bin/env bash
|
|
1033
|
+
shift
|
|
1034
|
+
"$@" &
|
|
1035
|
+
child=$!
|
|
1036
|
+
trap 'kill -TERM "$child" 2>/dev/null; exit 124' TERM
|
|
1037
|
+
wait "$child"
|
|
1038
|
+
SH
|
|
1039
|
+
chmod 0700 "$dir/fakebin/timeout"
|
|
1040
|
+
FM_HOME="$dir/home" "$REGISTER" custom >/dev/null \
|
|
1041
|
+
|| fail "could not register signal cleanup custom check"
|
|
1042
|
+
|
|
1043
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" FM_POLL=0 FM_CHECK_INTERVAL=0 \
|
|
1044
|
+
FM_SIGNAL_GRACE=0 FM_TEST_CUSTOM_CHILD_PID="$child_pid_file" \
|
|
1045
|
+
PATH="$dir/fakebin:$BASE_PATH" "$WATCH" \
|
|
1046
|
+
> "$dir/watch.out" 2> "$dir/watch.err" &
|
|
1047
|
+
pid=$!
|
|
1048
|
+
i=0
|
|
1049
|
+
while [ "$i" -lt 100 ]; do
|
|
1050
|
+
[ -s "$child_pid_file" ] && break
|
|
1051
|
+
kill -0 "$pid" 2>/dev/null || break
|
|
1052
|
+
sleep 0.02
|
|
1053
|
+
i=$((i + 1))
|
|
1054
|
+
done
|
|
1055
|
+
[ -s "$child_pid_file" ] || fail "watcher did not start the custom check child"
|
|
1056
|
+
find "$state" -maxdepth 1 -name '.fm-custom-check.*' -print | grep . >/dev/null \
|
|
1057
|
+
|| fail "watcher did not create the custom check snapshot"
|
|
1058
|
+
child_pid=$(cat "$child_pid_file")
|
|
1059
|
+
kill -TERM "$pid" 2>/dev/null || fail "could not signal watcher during custom check"
|
|
1060
|
+
i=0
|
|
1061
|
+
while kill -0 "$pid" 2>/dev/null && [ "$i" -lt 100 ]; do
|
|
1062
|
+
sleep 0.02
|
|
1063
|
+
i=$((i + 1))
|
|
1064
|
+
done
|
|
1065
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
1066
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
1067
|
+
wait "$pid" 2>/dev/null || true
|
|
1068
|
+
fail "signaled watcher did not exit promptly"
|
|
1069
|
+
fi
|
|
1070
|
+
rc=0
|
|
1071
|
+
wait "$pid" || rc=$?
|
|
1072
|
+
[ "$rc" -ne 0 ] || fail "signaled watcher exited successfully"
|
|
1073
|
+
! kill -0 "$child_pid" 2>/dev/null || fail "signaled watcher left the custom check child running"
|
|
1074
|
+
! find "$state" -maxdepth 1 -name '.fm-custom-check.*' -print | grep . >/dev/null \
|
|
1075
|
+
|| fail "signaled watcher left a private custom check snapshot"
|
|
1076
|
+
! find "$state" -maxdepth 1 -name '.fm-check-output.*' -print | grep . >/dev/null \
|
|
1077
|
+
|| fail "signaled watcher left a private check output file"
|
|
1078
|
+
[ ! -e "$state/.watch.lock/pid" ] || fail "signaled watcher left its singleton lock"
|
|
1079
|
+
pass "watcher signals promptly stop custom checks and clean private state"
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
test_returned_custom_check_descendants_are_drained() {
|
|
1083
|
+
local backend dir state fakebin ready direct_done child_pid_file sentinel watcher_pid child_pid i rc alive force_fallback
|
|
1084
|
+
for backend in installed-timeout fallback-timeout; do
|
|
1085
|
+
dir=$(make_case "returned-custom-descendant-$backend")
|
|
1086
|
+
state="$dir/home/state"
|
|
1087
|
+
fakebin="$dir/fakebin"
|
|
1088
|
+
ready="$dir/descendant-ready"
|
|
1089
|
+
direct_done="$dir/direct-check-done"
|
|
1090
|
+
child_pid_file="$dir/descendant.pid"
|
|
1091
|
+
sentinel="$dir/descendant-sentinel"
|
|
1092
|
+
cat > "$state/custom.check.sh" <<'SH'
|
|
1093
|
+
#!/usr/bin/env bash
|
|
1094
|
+
perl -e '$SIG{TERM}="IGNORE"; open my $ready, ">", $ENV{FM_TEST_DESCENDANT_READY} or die $!; print {$ready} "ready\n"; close $ready; select undef, undef, undef, 4; open my $sentinel, ">", $ENV{FM_TEST_DESCENDANT_SENTINEL} or die $!; print {$sentinel} "late\n"; close $sentinel; select undef, undef, undef, 1' &
|
|
1095
|
+
printf '%s\n' "$!" > "$FM_TEST_DESCENDANT_PID"
|
|
1096
|
+
while [ ! -s "$FM_TEST_DESCENDANT_READY" ]; do sleep 0.01; done
|
|
1097
|
+
: > "$FM_TEST_DIRECT_DONE"
|
|
1098
|
+
SH
|
|
1099
|
+
chmod 0700 "$state/custom.check.sh"
|
|
1100
|
+
FM_HOME="$dir/home" "$REGISTER" custom >/dev/null \
|
|
1101
|
+
|| fail "could not register $backend returned-descendant check"
|
|
1102
|
+
if [ "$backend" = installed-timeout ]; then
|
|
1103
|
+
cat > "$fakebin/timeout" <<'SH'
|
|
1104
|
+
#!/usr/bin/env bash
|
|
1105
|
+
shift
|
|
1106
|
+
exec "$@"
|
|
1107
|
+
SH
|
|
1108
|
+
chmod 0700 "$fakebin/timeout"
|
|
1109
|
+
force_fallback=0
|
|
1110
|
+
else
|
|
1111
|
+
rm -f "$fakebin/timeout" "$fakebin/gtimeout"
|
|
1112
|
+
force_fallback=1
|
|
1113
|
+
fi
|
|
1114
|
+
|
|
1115
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" FM_POLL=0.1 FM_CHECK_INTERVAL=999999 \
|
|
1116
|
+
FM_CHECK_TIMEOUT=10 FM_HEARTBEAT=999999 FM_SIGNAL_GRACE=0 \
|
|
1117
|
+
FM_CHECK_FORCE_FALLBACK="$force_fallback" FM_TEST_DESCENDANT_READY="$ready" \
|
|
1118
|
+
FM_TEST_DESCENDANT_SENTINEL="$sentinel" FM_TEST_DESCENDANT_PID="$child_pid_file" \
|
|
1119
|
+
FM_TEST_DIRECT_DONE="$direct_done" PATH="$fakebin:$BASE_PATH" "$WATCH" \
|
|
1120
|
+
> "$dir/watch.out" 2> "$dir/watch.err" &
|
|
1121
|
+
watcher_pid=$!
|
|
1122
|
+
i=0
|
|
1123
|
+
while [ "$i" -lt 200 ]; do
|
|
1124
|
+
[ -s "$ready" ] && [ -s "$child_pid_file" ] && [ -e "$direct_done" ] \
|
|
1125
|
+
&& [ -e "$state/.last-check" ] && break
|
|
1126
|
+
kill -0 "$watcher_pid" 2>/dev/null || break
|
|
1127
|
+
sleep 0.02
|
|
1128
|
+
i=$((i + 1))
|
|
1129
|
+
done
|
|
1130
|
+
[ -s "$ready" ] && [ -s "$child_pid_file" ] && [ -e "$direct_done" ] \
|
|
1131
|
+
&& [ -e "$state/.last-check" ] \
|
|
1132
|
+
|| fail "$backend watcher did not complete the direct custom check"
|
|
1133
|
+
child_pid=$(cat "$child_pid_file")
|
|
1134
|
+
kill -TERM "$watcher_pid" 2>/dev/null || fail "could not stop $backend watcher"
|
|
1135
|
+
i=0
|
|
1136
|
+
while kill -0 "$watcher_pid" 2>/dev/null && [ "$i" -lt 150 ]; do
|
|
1137
|
+
sleep 0.02
|
|
1138
|
+
i=$((i + 1))
|
|
1139
|
+
done
|
|
1140
|
+
if kill -0 "$watcher_pid" 2>/dev/null; then
|
|
1141
|
+
kill -KILL "$watcher_pid" 2>/dev/null || true
|
|
1142
|
+
wait "$watcher_pid" 2>/dev/null || true
|
|
1143
|
+
kill -KILL "$child_pid" 2>/dev/null || true
|
|
1144
|
+
fail "$backend watcher did not stop after the direct check returned"
|
|
1145
|
+
fi
|
|
1146
|
+
rc=0
|
|
1147
|
+
wait "$watcher_pid" || rc=$?
|
|
1148
|
+
[ "$rc" -ne 0 ] || fail "$backend signaled watcher exited successfully"
|
|
1149
|
+
alive=0
|
|
1150
|
+
kill -0 "$child_pid" 2>/dev/null && alive=1
|
|
1151
|
+
[ "$alive" -eq 0 ] || kill -KILL "$child_pid" 2>/dev/null || true
|
|
1152
|
+
wait "$child_pid" 2>/dev/null || true
|
|
1153
|
+
[ "$alive" -eq 0 ] || fail "$backend watcher left a returned check descendant alive"
|
|
1154
|
+
[ ! -e "$sentinel" ] || fail "$backend returned check descendant reached its sentinel"
|
|
1155
|
+
! find "$state" -maxdepth 1 -name '.fm-custom-check.*' -print | grep . >/dev/null \
|
|
1156
|
+
|| fail "$backend watcher left a private custom check snapshot"
|
|
1157
|
+
! find "$state" -maxdepth 1 -name '.fm-check-output.*' -print | grep . >/dev/null \
|
|
1158
|
+
|| fail "$backend watcher left a private check output file"
|
|
1159
|
+
[ ! -e "$state/.watch.lock/pid" ] || fail "$backend watcher left its singleton lock"
|
|
1160
|
+
done
|
|
1161
|
+
pass "returned custom check descendants are drained on installed and fallback timeout paths"
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
test_teardown_removes_poll_artifacts() {
|
|
1165
|
+
local dir fakebin artifact counterpart rc
|
|
1166
|
+
dir=$(make_case teardown-cleanup)
|
|
1167
|
+
fakebin="$dir/fakebin"
|
|
1168
|
+
fm_write_meta "$dir/home/state/task-a.meta" \
|
|
1169
|
+
'window=firstmate:fm-task-a' \
|
|
1170
|
+
'endpoint_task_id=task-a' \
|
|
1171
|
+
"worktree=$dir/missing-worktree" \
|
|
1172
|
+
"project=$dir/project" \
|
|
1173
|
+
'kind=ship' \
|
|
1174
|
+
'mode=local-only'
|
|
1175
|
+
printf 'check\n' > "$dir/home/state/task-a.check.sh"
|
|
1176
|
+
printf 'data\n' > "$dir/home/state/task-a.pr-poll"
|
|
1177
|
+
printf 'registration\n' > "$dir/home/state/task-a.pr-poll-registration"
|
|
1178
|
+
printf 'trust\n' > "$dir/home/state/task-a.check-trust"
|
|
1179
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
1180
|
+
#!/usr/bin/env bash
|
|
1181
|
+
exit 0
|
|
1182
|
+
SH
|
|
1183
|
+
chmod +x "$fakebin/tmux"
|
|
1184
|
+
touch "$dir/home/state/.last-watcher-beat"
|
|
1185
|
+
|
|
1186
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" PATH="$fakebin:$BASE_PATH" \
|
|
1187
|
+
"$TEARDOWN" task-a --force > "$dir/teardown.out" 2> "$dir/teardown.err" \
|
|
1188
|
+
|| fail "teardown cleanup fixture failed"
|
|
1189
|
+
[ ! -e "$dir/home/state/task-a.check.sh" ] || fail "teardown left the runnable check"
|
|
1190
|
+
[ ! -e "$dir/home/state/task-a.pr-poll" ] || fail "teardown left the sidecar"
|
|
1191
|
+
[ ! -e "$dir/home/state/task-a.pr-poll-registration" ] || fail "teardown left the PR poll registration"
|
|
1192
|
+
[ ! -e "$dir/home/state/task-a.check-trust" ] || fail "teardown left the custom check registration"
|
|
1193
|
+
|
|
1194
|
+
dir=$(make_case teardown-retirement-receipt)
|
|
1195
|
+
fakebin="$dir/fakebin"
|
|
1196
|
+
fm_write_meta "$dir/home/state/task-a.meta" \
|
|
1197
|
+
'window=firstmate:fm-task-a' \
|
|
1198
|
+
'endpoint_task_id=task-a' \
|
|
1199
|
+
"worktree=$dir/missing-worktree" \
|
|
1200
|
+
"project=$dir/project" \
|
|
1201
|
+
'kind=ship' \
|
|
1202
|
+
'mode=local-only' \
|
|
1203
|
+
'pr=https://github.com/o/r/pull/18'
|
|
1204
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/18
|
|
1205
|
+
fm_pr_poll_snapshot_capture "$dir/home/state" task-a "$POLL" \
|
|
1206
|
+
|| fail "could not snapshot teardown receipt fixture"
|
|
1207
|
+
fm_pr_poll_retirement_publish "$dir/home/state" task-a "$POLL" merged \
|
|
1208
|
+
|| fail "could not publish teardown receipt fixture"
|
|
1209
|
+
rm -f "$dir/home/state/task-a.check.sh"
|
|
1210
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
1211
|
+
#!/usr/bin/env bash
|
|
1212
|
+
exit 0
|
|
1213
|
+
SH
|
|
1214
|
+
chmod +x "$fakebin/tmux"
|
|
1215
|
+
touch "$dir/home/state/.last-watcher-beat"
|
|
1216
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" PATH="$fakebin:$BASE_PATH" \
|
|
1217
|
+
"$TEARDOWN" task-a --force > "$dir/teardown.out" 2> "$dir/teardown.err" \
|
|
1218
|
+
|| fail "teardown could not finish a valid crash-left retirement receipt"
|
|
1219
|
+
assert_poll_absent "$dir/home/state" task-a
|
|
1220
|
+
[ ! -e "$dir/home/state/task-a.meta" ] || fail "receipt-aware teardown left task metadata"
|
|
1221
|
+
|
|
1222
|
+
for artifact in check.sh pr-poll; do
|
|
1223
|
+
dir=$(make_case "teardown-final-directory-${artifact//./-}")
|
|
1224
|
+
fakebin="$dir/fakebin"
|
|
1225
|
+
fm_write_meta "$dir/home/state/task-a.meta" \
|
|
1226
|
+
'window=firstmate:fm-task-a' \
|
|
1227
|
+
'endpoint_task_id=task-a' \
|
|
1228
|
+
"worktree=$dir/missing-worktree" \
|
|
1229
|
+
"project=$dir/project" \
|
|
1230
|
+
'kind=ship' \
|
|
1231
|
+
'mode=local-only'
|
|
1232
|
+
if [ "$artifact" = check.sh ]; then
|
|
1233
|
+
counterpart=pr-poll
|
|
1234
|
+
else
|
|
1235
|
+
counterpart=check.sh
|
|
1236
|
+
fi
|
|
1237
|
+
mkdir "$dir/home/state/task-a.$artifact"
|
|
1238
|
+
printf 'directory sentinel\n' > "$dir/home/state/task-a.$artifact/sentinel"
|
|
1239
|
+
printf 'counterpart sentinel\n' > "$dir/home/state/task-a.$counterpart"
|
|
1240
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
1241
|
+
#!/usr/bin/env bash
|
|
1242
|
+
printf '%s\n' "$*" >> "${FM_FAKE_TMUX_LOG:?}"
|
|
1243
|
+
exit 0
|
|
1244
|
+
SH
|
|
1245
|
+
chmod +x "$fakebin/tmux"
|
|
1246
|
+
touch "$dir/home/state/.last-watcher-beat"
|
|
1247
|
+
set +e
|
|
1248
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" FM_FAKE_TMUX_LOG="$dir/tmux.log" \
|
|
1249
|
+
PATH="$fakebin:$BASE_PATH" "$TEARDOWN" task-a --force \
|
|
1250
|
+
> "$dir/teardown.out" 2> "$dir/teardown.err"
|
|
1251
|
+
rc=$?
|
|
1252
|
+
set -e
|
|
1253
|
+
[ "$rc" -ne 0 ] || fail "teardown accepted a directory-shaped $artifact"
|
|
1254
|
+
[ -e "$dir/home/state/task-a.meta" ] || fail "teardown removed metadata before $artifact refusal"
|
|
1255
|
+
[ "$(cat "$dir/home/state/task-a.$artifact/sentinel")" = 'directory sentinel' ] \
|
|
1256
|
+
|| fail "teardown changed the directory-shaped $artifact"
|
|
1257
|
+
[ "$(cat "$dir/home/state/task-a.$counterpart")" = 'counterpart sentinel' ] \
|
|
1258
|
+
|| fail "teardown removed the counterpart before $artifact refusal"
|
|
1259
|
+
grep -F 'kill-window' "$dir/tmux.log" >/dev/null 2>&1 \
|
|
1260
|
+
&& fail "teardown killed the endpoint before $artifact refusal"
|
|
1261
|
+
done
|
|
1262
|
+
|
|
1263
|
+
pass "teardown removes safe poll artifacts and refuses directory-shaped check files without traversal"
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
# The GitLab watch must follow a merge request exactly as the GitHub watch
|
|
1267
|
+
# follows a pull request, on any instance, and must never turn an unreadable
|
|
1268
|
+
# merge request into a merge. Its evidence against the public fixture project
|
|
1269
|
+
# https://gitlab.com/KarotKris/gitlab-merge-watch-fixture is in
|
|
1270
|
+
# docs/gitlab-merge-watch.md; this exercises the same paths hermetically.
|
|
1271
|
+
test_gitlab_merge_watch() {
|
|
1272
|
+
local dir state out rc url value noglab entry bindir name
|
|
1273
|
+
dir=$(make_case gitlab-merge-watch)
|
|
1274
|
+
state="$dir/home/state"
|
|
1275
|
+
url=https://gitlab.example/group/subgroup/project/-/merge_requests/7
|
|
1276
|
+
|
|
1277
|
+
write_poll_meta "$state" task-a "$url"
|
|
1278
|
+
fm_pr_poll_prepare "$state" task-a gitlab "$url" gitlab.example group/subgroup/project 7 "$POLL" \
|
|
1279
|
+
|| fail "could not prepare a GitLab poll"
|
|
1280
|
+
fm_pr_poll_publish_prepared || fail "could not publish a GitLab poll"
|
|
1281
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$POLL" \
|
|
1282
|
+
|| fail "published GitLab poll provenance or metadata binding was invalid"
|
|
1283
|
+
[ "$(cat "$state/task-a.pr-poll")" = "gitlab
|
|
1284
|
+
$url
|
|
1285
|
+
gitlab.example
|
|
1286
|
+
group/subgroup/project
|
|
1287
|
+
7" ] || fail "published GitLab sidecar bytes were not exact"
|
|
1288
|
+
|
|
1289
|
+
# Only an exact merged state wakes firstmate. Every other reading, including
|
|
1290
|
+
# an unreadable merge request and a changed output format, stays silent.
|
|
1291
|
+
for value in opened closed locked '' not-a-state MERGED merged-but-not; do
|
|
1292
|
+
out=$(FM_TEST_GLAB_STATE="$value" run_poll "$dir")
|
|
1293
|
+
[ -z "$out" ] || fail "GitLab poll emitted for a non-merged state"
|
|
1294
|
+
done
|
|
1295
|
+
out=$(FM_TEST_GLAB_STATE=merged run_poll "$dir")
|
|
1296
|
+
[ "$out" = merged ] || fail "GitLab poll did not emit exactly one merged line"
|
|
1297
|
+
out=$(FM_TEST_GLAB_FAIL=1 run_poll "$dir")
|
|
1298
|
+
[ -z "$out" ] || fail "GitLab poll emitted after a glab failure"
|
|
1299
|
+
|
|
1300
|
+
# glab is addressed by project URL and merge request number, never by the
|
|
1301
|
+
# merge request URL, which the real CLI resolves through the current git
|
|
1302
|
+
# repository the watcher does not have.
|
|
1303
|
+
grep -qF -- "mr view 7 -R https://gitlab.example/group/subgroup/project" "$dir/glab.log" \
|
|
1304
|
+
|| fail "GitLab poll did not address glab by project URL and merge request number"
|
|
1305
|
+
! grep -qF -- "$url" "$dir/glab.log" \
|
|
1306
|
+
|| fail "GitLab poll passed a merge request URL to glab"
|
|
1307
|
+
|
|
1308
|
+
# An absent CLI must produce no wake rather than a false merge. The whole
|
|
1309
|
+
# search path is mirrored without glab, because a real glab anywhere on
|
|
1310
|
+
# PATH would make this prove nothing.
|
|
1311
|
+
noglab="$dir/noglab"
|
|
1312
|
+
mkdir -p "$noglab"
|
|
1313
|
+
while IFS= read -r bindir; do
|
|
1314
|
+
[ -d "$bindir" ] || continue
|
|
1315
|
+
for entry in "$bindir"/*; do
|
|
1316
|
+
[ -e "$entry" ] || continue
|
|
1317
|
+
name=$(basename "$entry")
|
|
1318
|
+
[ "$name" = glab ] && continue
|
|
1319
|
+
[ -e "$noglab/$name" ] || ln -s "$entry" "$noglab/$name" 2>/dev/null
|
|
1320
|
+
done
|
|
1321
|
+
done <<EOF
|
|
1322
|
+
$dir/fakebin
|
|
1323
|
+
$(printf '%s\n' "$BASE_PATH" | tr ':' '\n')
|
|
1324
|
+
EOF
|
|
1325
|
+
! PATH="$noglab" command -v glab >/dev/null 2>&1 \
|
|
1326
|
+
|| fail "the glab-free search path still resolved glab"
|
|
1327
|
+
out=$(FM_TEST_GLAB_STATE=merged FM_TEST_GH_LOG="$dir/gh.log" FM_TEST_GLAB_LOG="$dir/glab.log" \
|
|
1328
|
+
PATH="$noglab" \
|
|
1329
|
+
bash "$state/task-a.check.sh")
|
|
1330
|
+
[ -z "$out" ] || fail "GitLab poll emitted with glab absent from PATH"
|
|
1331
|
+
|
|
1332
|
+
# A doctored sidecar cannot redirect the poll: the stored parts must rebuild
|
|
1333
|
+
# the stored URL exactly.
|
|
1334
|
+
printf '%s\n%s\n%s\n%s\n%s\n' gitlab "$url" elsewhere.example group/subgroup/project 7 \
|
|
1335
|
+
> "$state/task-a.pr-poll"
|
|
1336
|
+
out=$(FM_TEST_GLAB_STATE=merged run_poll "$dir")
|
|
1337
|
+
[ -z "$out" ] || fail "GitLab poll emitted for a sidecar whose host was swapped"
|
|
1338
|
+
printf '%s\n%s\n%s\n%s\n%s\n' gitlab "$url" gitlab.example group/subgroup/other 7 \
|
|
1339
|
+
> "$state/task-a.pr-poll"
|
|
1340
|
+
out=$(FM_TEST_GLAB_STATE=merged run_poll "$dir")
|
|
1341
|
+
[ -z "$out" ] || fail "GitLab poll emitted for a sidecar whose project was swapped"
|
|
1342
|
+
|
|
1343
|
+
# Arming is where a missing CLI can still be reported, so it refuses there.
|
|
1344
|
+
write_task_meta "$dir" task-b
|
|
1345
|
+
set +e
|
|
1346
|
+
out=$(FM_ROOT_OVERRIDE="$dir/root" FM_HOME="$dir/home" \
|
|
1347
|
+
FM_TEST_GUARD_LOG="$dir/guard.log" PATH="$noglab" \
|
|
1348
|
+
"$PR_CHECK" task-b "$url" 2>&1)
|
|
1349
|
+
rc=$?
|
|
1350
|
+
set -e
|
|
1351
|
+
[ "$rc" -ne 0 ] || fail "arming a GitLab watch succeeded with glab absent"
|
|
1352
|
+
case "$out" in
|
|
1353
|
+
*"requires glab on PATH"*) ;;
|
|
1354
|
+
*) fail "arming a GitLab watch with glab absent did not report the missing CLI" ;;
|
|
1355
|
+
esac
|
|
1356
|
+
[ ! -e "$state/task-b.check.sh" ] || fail "refused GitLab arming left a poll armed"
|
|
1357
|
+
|
|
1358
|
+
# The merge path addresses the forge the URL names, and never the other one.
|
|
1359
|
+
# This fixture's glab answers with the field output the poll reads, so the
|
|
1360
|
+
# merge's JSON read cannot be parsed, which must refuse rather than merge on a
|
|
1361
|
+
# state it could not read.
|
|
1362
|
+
write_task_meta "$dir" task-c
|
|
1363
|
+
: > "$dir/glab.log"
|
|
1364
|
+
# The merge path needs jq before it reads anything, so this case supplies it
|
|
1365
|
+
# and the refusal below is the unreadable state rather than a missing tool.
|
|
1366
|
+
ln -sf "$REAL_JQ" "$dir/fakebin/jq"
|
|
1367
|
+
set +e
|
|
1368
|
+
run_merge_entry "$dir" task-c "$url" >/dev/null 2> "$dir/merge-c.err"
|
|
1369
|
+
rc=$?
|
|
1370
|
+
set -e
|
|
1371
|
+
[ "$rc" -ne 0 ] || fail "merge wrapper merged a GitLab merge request it could not read"
|
|
1372
|
+
grep -qF 'could not read the GitLab merge request state before merging' "$dir/merge-c.err" \
|
|
1373
|
+
|| fail "merge wrapper refused for some reason other than the state it could not read"
|
|
1374
|
+
[ ! -s "$dir/gh-axi.log" ] || fail "merge wrapper reached the GitHub CLI for a GitLab URL"
|
|
1375
|
+
grep -qF "mr view 7 -R https://gitlab.example/group/subgroup/project" "$dir/glab.log" \
|
|
1376
|
+
|| fail "merge wrapper did not read the merge request through glab at its own instance"
|
|
1377
|
+
! grep -qF ' mr merge ' "$dir/glab.log" \
|
|
1378
|
+
|| fail "merge wrapper merged despite an unreadable merge request state"
|
|
1379
|
+
|
|
1380
|
+
pass "GitLab merge requests are followed on any instance and never wake falsely"
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
seed_canonical_poll() {
|
|
1384
|
+
local dir=$1 id=$2 url=$3 template=${4:-$POLL} state provider host path number
|
|
1385
|
+
state="$dir/home/state"
|
|
1386
|
+
fm_pr_url_parse "$url" || fail "retirement fixture URL was invalid"
|
|
1387
|
+
provider=$FM_PR_PROVIDER
|
|
1388
|
+
host=$FM_PR_HOST
|
|
1389
|
+
path=$FM_PR_PATH
|
|
1390
|
+
number=$FM_PR_NUMBER
|
|
1391
|
+
fm_pr_poll_prepare "$state" "$id" "$provider" "$url" "$host" "$path" "$number" "$template" \
|
|
1392
|
+
|| fail "could not prepare retirement fixture"
|
|
1393
|
+
fm_pr_poll_publish_prepared || fail "could not publish retirement fixture"
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
add_stop_custom_check() {
|
|
1397
|
+
local dir=$1 state
|
|
1398
|
+
state="$dir/home/state"
|
|
1399
|
+
printf '#!/usr/bin/env bash\nprintf "stop-cycle\\n"\n' > "$state/z-stop.check.sh"
|
|
1400
|
+
chmod 0700 "$state/z-stop.check.sh"
|
|
1401
|
+
FM_HOME="$dir/home" "$REGISTER" z-stop >/dev/null \
|
|
1402
|
+
|| fail "could not register stop-cycle custom check"
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
assert_poll_absent() {
|
|
1406
|
+
local state=$1 id=$2 suffix
|
|
1407
|
+
for suffix in check.sh pr-poll pr-poll-registration pr-poll-retirement; do
|
|
1408
|
+
[ ! -e "$state/$id.$suffix" ] && [ ! -L "$state/$id.$suffix" ] \
|
|
1409
|
+
|| fail "retired poll left $id.$suffix"
|
|
1410
|
+
done
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
poll_artifact_snapshot() {
|
|
1414
|
+
local state=$1 id=$2 suffix path
|
|
1415
|
+
for suffix in check.sh pr-poll pr-poll-registration pr-poll-retirement meta; do
|
|
1416
|
+
path="$state/$id.$suffix"
|
|
1417
|
+
[ -e "$path" ] || [ -L "$path" ] || continue
|
|
1418
|
+
if [ -L "$path" ]; then
|
|
1419
|
+
printf 'link %s %s\n' "$suffix" "$(readlink "$path")"
|
|
1420
|
+
elif [ -f "$path" ]; then
|
|
1421
|
+
printf 'file %s %s ' "$suffix" "$(file_mode "$path")"
|
|
1422
|
+
shasum -a 256 "$path" | awk '{print $1}'
|
|
1423
|
+
else
|
|
1424
|
+
printf 'other %s\n' "$suffix"
|
|
1425
|
+
fi
|
|
1426
|
+
done
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
test_merged_poll_retires_once() {
|
|
1430
|
+
local dir state rc first second meta_before
|
|
1431
|
+
dir=$(make_case merged-retirement-once)
|
|
1432
|
+
state="$dir/home/state"
|
|
1433
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/1
|
|
1434
|
+
meta_before=$(cat "$state/task-a.meta")
|
|
1435
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/1
|
|
1436
|
+
add_stop_custom_check "$dir"
|
|
1437
|
+
|
|
1438
|
+
set +e
|
|
1439
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-1.out" 2> "$dir/watch-1.err"
|
|
1440
|
+
rc=$?
|
|
1441
|
+
set -e
|
|
1442
|
+
[ "$rc" -eq 0 ] || fail "merged retirement watcher failed: $(cat "$dir/watch-1.err")"
|
|
1443
|
+
first=$(cat "$dir/watch-1.out")
|
|
1444
|
+
case "$first" in check:*task-a.check.sh:*merged) ;; *) fail "first merged notification was not preserved: $first" ;; esac
|
|
1445
|
+
ack_watcher_cycle "$state" || fail "first merged notification handling acknowledgement failed"
|
|
1446
|
+
assert_poll_absent "$state" task-a
|
|
1447
|
+
[ "$(cat "$state/task-a.meta")" = "$meta_before" ] || fail "merged retirement changed canonical metadata"
|
|
1448
|
+
|
|
1449
|
+
rm -f "$state/.last-check"
|
|
1450
|
+
set +e
|
|
1451
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-2.out" 2> "$dir/watch-2.err"
|
|
1452
|
+
rc=$?
|
|
1453
|
+
set -e
|
|
1454
|
+
[ "$rc" -eq 0 ] || fail "second watcher cycle failed: $(cat "$dir/watch-2.err")"
|
|
1455
|
+
second=$(cat "$dir/watch-2.out")
|
|
1456
|
+
case "$second" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "second cycle did not reach the control check: $second" ;; esac
|
|
1457
|
+
! grep -F 'task-a.check.sh: merged' "$dir/watch-2.out" >/dev/null \
|
|
1458
|
+
|| fail "retired merged poll executed a second time"
|
|
1459
|
+
! grep "$(printf '\tcheck\ttask-a.check.sh\t')" "$state/.wake-queue" >/dev/null 2>&1 \
|
|
1460
|
+
|| fail "handled merged notification remained queued after acknowledgement"
|
|
1461
|
+
pass "validated merged polls notify once and retire before the next watcher cycle"
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
# A poll's own retirement state is scoped to ONE registration, so it cannot by
|
|
1465
|
+
# itself catch a poll re-registered for a task whose merge was already
|
|
1466
|
+
# surfaced (e.g. bin/fm-pr-check.sh re-armed after the fact). The per-task
|
|
1467
|
+
# merge-notified marker (bin/fm-pr-lib.sh) is what stops that re-registration
|
|
1468
|
+
# from producing a second main-blocking wake for the identical merge, while a
|
|
1469
|
+
# genuinely first notification (test_merged_poll_retires_once above) still
|
|
1470
|
+
# reaches main.
|
|
1471
|
+
test_merged_poll_reregistration_after_notification_is_absorbed() {
|
|
1472
|
+
local dir state rc first
|
|
1473
|
+
dir=$(make_case merged-reregistration-absorbed)
|
|
1474
|
+
state="$dir/home/state"
|
|
1475
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/1
|
|
1476
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/1
|
|
1477
|
+
add_stop_custom_check "$dir"
|
|
1478
|
+
|
|
1479
|
+
set +e
|
|
1480
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-1.out" 2> "$dir/watch-1.err"
|
|
1481
|
+
rc=$?
|
|
1482
|
+
set -e
|
|
1483
|
+
[ "$rc" -eq 0 ] || fail "first merged watcher cycle failed: $(cat "$dir/watch-1.err")"
|
|
1484
|
+
first=$(cat "$dir/watch-1.out")
|
|
1485
|
+
case "$first" in check:*task-a.check.sh:*merged) ;; *) fail "first merge confirmation was not delivered: $first" ;; esac
|
|
1486
|
+
ack_watcher_cycle "$state" || fail "first merge confirmation acknowledgement failed"
|
|
1487
|
+
assert_poll_absent "$state" task-a
|
|
1488
|
+
[ -f "$state/task-a.pr-poll-merge-notified" ] || fail "the merge-notified marker was not recorded"
|
|
1489
|
+
|
|
1490
|
+
# Re-registration: fm-pr-check.sh re-armed for a task whose PR is already
|
|
1491
|
+
# merged (a fresh check.sh/pr-poll/pr-poll-registration, a distinct
|
|
1492
|
+
# retirement identity from the one just retired).
|
|
1493
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/1
|
|
1494
|
+
rm -f "$state/.last-check"
|
|
1495
|
+
|
|
1496
|
+
set +e
|
|
1497
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-2.out" 2> "$dir/watch-2.err"
|
|
1498
|
+
rc=$?
|
|
1499
|
+
set -e
|
|
1500
|
+
[ "$rc" -eq 0 ] || fail "second watcher cycle failed: $(cat "$dir/watch-2.err")"
|
|
1501
|
+
case "$(cat "$dir/watch-2.out")" in
|
|
1502
|
+
check:*z-stop.check.sh:*stop-cycle) ;;
|
|
1503
|
+
*) fail "the re-registered duplicate did not fall through to the next check: $(cat "$dir/watch-2.out")" ;;
|
|
1504
|
+
esac
|
|
1505
|
+
! grep -F 'task-a.check.sh: merged' "$dir/watch-2.out" >/dev/null \
|
|
1506
|
+
|| fail "a repeat identical merged poll opened a main-blocking row: $(cat "$dir/watch-2.out")"
|
|
1507
|
+
! grep "$(printf '\tcheck\ttask-a.check.sh\t')" "$state/.wake-queue" >/dev/null 2>&1 \
|
|
1508
|
+
|| fail "the absorbed duplicate merge notice was queued as a main-blocking row"
|
|
1509
|
+
assert_poll_absent "$state" task-a
|
|
1510
|
+
pass "a repeat identical merged poll for an already-notified task is absorbed, never queued as a main-blocking row"
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
# The captain merging a PR himself on the forge is the same outcome as a merge
|
|
1514
|
+
# this home performed: bin/fm-merge-outcome-lib.sh carries both to the parent on
|
|
1515
|
+
# the one reply channel, so no second watch path exists for the captain's case.
|
|
1516
|
+
# The poll's own durable row still lands here, because the mate that owns the
|
|
1517
|
+
# task still has to act on it.
|
|
1518
|
+
seed_secondmate_home() { # <dir> [<route>]
|
|
1519
|
+
local dir=$1 route=${2:-remote}
|
|
1520
|
+
printf '%s\n' mate-x > "$dir/home/.fm-secondmate-home"
|
|
1521
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=%s\n' "$route" \
|
|
1522
|
+
> "$dir/home/.fm-secondmate-parent"
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
test_merged_poll_retries_a_failed_upward_report() {
|
|
1526
|
+
local dir state rc replies url
|
|
1527
|
+
url=https://github.com/o/r/pull/1
|
|
1528
|
+
dir=$(make_case merged-poll-upward-retry)
|
|
1529
|
+
state="$dir/home/state"
|
|
1530
|
+
replies="$state/parent-replies.status"
|
|
1531
|
+
printf '%s\n' mate-x > "$dir/home/.fm-secondmate-home"
|
|
1532
|
+
write_poll_meta "$state" task-a "$url"
|
|
1533
|
+
seed_canonical_poll "$dir" task-a "$url"
|
|
1534
|
+
add_stop_custom_check "$dir"
|
|
1535
|
+
|
|
1536
|
+
set +e
|
|
1537
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" \
|
|
1538
|
+
> "$dir/watch-1.out" 2> "$dir/watch-1.err"
|
|
1539
|
+
rc=$?
|
|
1540
|
+
set -e
|
|
1541
|
+
[ "$rc" -ne 0 ] || fail "merged-poll-upward-retry: failed report did not keep the watcher loud"
|
|
1542
|
+
[ -f "$state/task-a.check.sh" ] \
|
|
1543
|
+
|| fail "merged-poll-upward-retry: failed report retired its retry poll"
|
|
1544
|
+
[ ! -e "$state/task-a.pr-poll-merge-notified" ] \
|
|
1545
|
+
|| fail "merged-poll-upward-retry: failed report was marked notified"
|
|
1546
|
+
[ ! -e "$replies" ] \
|
|
1547
|
+
|| fail "merged-poll-upward-retry: failed report wrote a parent reply"
|
|
1548
|
+
|
|
1549
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=remote\n' \
|
|
1550
|
+
> "$dir/home/.fm-secondmate-parent"
|
|
1551
|
+
rm -f "$state/.last-check"
|
|
1552
|
+
set +e
|
|
1553
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" \
|
|
1554
|
+
> "$dir/watch-2.out" 2> "$dir/watch-2.err"
|
|
1555
|
+
rc=$?
|
|
1556
|
+
set -e
|
|
1557
|
+
[ "$rc" -eq 0 ] || fail "merged-poll-upward-retry: retry failed: $(cat "$dir/watch-2.err")"
|
|
1558
|
+
if [ ! -e "$replies" ]; then
|
|
1559
|
+
ack_watcher_cycle "$state" \
|
|
1560
|
+
|| fail "merged-poll-upward-retry: recovery acknowledgement failed"
|
|
1561
|
+
rm -f "$state/.last-check"
|
|
1562
|
+
set +e
|
|
1563
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" \
|
|
1564
|
+
> "$dir/watch-3.out" 2> "$dir/watch-3.err"
|
|
1565
|
+
rc=$?
|
|
1566
|
+
set -e
|
|
1567
|
+
[ "$rc" -eq 0 ] || fail "merged-poll-upward-retry: post-recovery retry failed: $(cat "$dir/watch-3.err")"
|
|
1568
|
+
fi
|
|
1569
|
+
assert_grep "done [key=merged-task-a]: merged task-a $url" "$replies" \
|
|
1570
|
+
"merged-poll-upward-retry: repaired binding did not receive the retry"
|
|
1571
|
+
assert_poll_absent "$state" task-a
|
|
1572
|
+
pass "a failed upward merge report keeps its poll armed for repair and retry"
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
test_self_merge_and_poll_publish_one_outcome() {
|
|
1576
|
+
local dir state replies url rc
|
|
1577
|
+
url=https://github.com/o/r/pull/1
|
|
1578
|
+
|
|
1579
|
+
# Interleaving one: self publication commits before the poll observes the
|
|
1580
|
+
# merge, so the poll absorbs the committed identity without reporting again.
|
|
1581
|
+
dir=$(make_case merge-outcome-committed)
|
|
1582
|
+
state="$dir/home/state"
|
|
1583
|
+
replies="$state/parent-replies.status"
|
|
1584
|
+
seed_secondmate_home "$dir"
|
|
1585
|
+
write_task_meta "$dir" task-a
|
|
1586
|
+
run_check_entry "$dir" task-a "$url" >/dev/null 2>"$dir/seed.err" \
|
|
1587
|
+
|| fail "merge-outcome-committed: could not arm merge poll"
|
|
1588
|
+
run_merge_entry "$dir" task-a "$url" >"$dir/merge.out" 2>"$dir/merge.err" \
|
|
1589
|
+
|| fail "merge-outcome-committed: merge entrypoint failed: $(cat "$dir/merge.err")"
|
|
1590
|
+
add_stop_custom_check "$dir"
|
|
1591
|
+
set +e
|
|
1592
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" \
|
|
1593
|
+
>"$dir/watch.out" 2>"$dir/watch.err"
|
|
1594
|
+
rc=$?
|
|
1595
|
+
set -e
|
|
1596
|
+
[ "$rc" -eq 0 ] \
|
|
1597
|
+
|| fail "merge-outcome-committed: watcher failed: $(cat "$dir/watch.err")"
|
|
1598
|
+
[ "$(grep -c -F "$url" "$replies")" -eq 1 ] \
|
|
1599
|
+
|| fail "merge-outcome-committed: self and poll reports produced duplicate outcomes"
|
|
1600
|
+
assert_no_grep "check: $state/task-a.check.sh: merged" "$state/.wake-queue" \
|
|
1601
|
+
"merge-outcome-committed: absorbed poll published a second outcome"
|
|
1602
|
+
assert_poll_absent "$state" task-a
|
|
1603
|
+
|
|
1604
|
+
# Interleaving two: self publication lands but its marker commit fails. After
|
|
1605
|
+
# that outcome is drained, the still-armed poll must publish it again rather
|
|
1606
|
+
# than treating the interrupted attempt as complete and going silent.
|
|
1607
|
+
dir=$(make_case merge-outcome-uncommitted)
|
|
1608
|
+
state="$dir/home/state"
|
|
1609
|
+
write_task_meta "$dir" task-a
|
|
1610
|
+
run_check_entry "$dir" task-a "$url" >/dev/null 2>"$dir/seed.err" \
|
|
1611
|
+
|| fail "merge-outcome-uncommitted: could not arm merge poll"
|
|
1612
|
+
cat >"$dir/fakebin/mv" <<'SH'
|
|
1613
|
+
#!/usr/bin/env bash
|
|
1614
|
+
case " $* " in
|
|
1615
|
+
*pr-poll-merge-notified*) exit 1 ;;
|
|
1616
|
+
esac
|
|
1617
|
+
exec "$FM_TEST_REAL_MV" "$@"
|
|
1618
|
+
SH
|
|
1619
|
+
chmod +x "$dir/fakebin/mv"
|
|
1620
|
+
set +e
|
|
1621
|
+
FM_TEST_REAL_MV="$REAL_MV" run_merge_entry "$dir" task-a "$url" \
|
|
1622
|
+
>"$dir/merge.out" 2>"$dir/merge.err"
|
|
1623
|
+
rc=$?
|
|
1624
|
+
set -e
|
|
1625
|
+
[ "$rc" -eq 0 ] \
|
|
1626
|
+
|| fail "merge-outcome-uncommitted: landed merge was reported as failed"
|
|
1627
|
+
assert_grep "$url" "$state/.wake-queue" \
|
|
1628
|
+
"merge-outcome-uncommitted: interrupted publication emitted no outcome"
|
|
1629
|
+
[ ! -e "$state/task-a.pr-poll-merge-notified" ] \
|
|
1630
|
+
|| fail "merge-outcome-uncommitted: failed marker commit was treated as complete"
|
|
1631
|
+
ack_watcher_cycle "$state" \
|
|
1632
|
+
|| fail "merge-outcome-uncommitted: could not drain the first outcome"
|
|
1633
|
+
assert_no_grep "$url" "$state/.wake-queue" \
|
|
1634
|
+
"merge-outcome-uncommitted: first outcome remained queued after its drain"
|
|
1635
|
+
rm -f "$dir/fakebin/mv" "$state/.last-check"
|
|
1636
|
+
|
|
1637
|
+
set +e
|
|
1638
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" \
|
|
1639
|
+
>"$dir/watch.out" 2>"$dir/watch.err"
|
|
1640
|
+
rc=$?
|
|
1641
|
+
set -e
|
|
1642
|
+
[ "$rc" -eq 0 ] \
|
|
1643
|
+
|| fail "merge-outcome-uncommitted: poll retry failed: $(cat "$dir/watch.err")"
|
|
1644
|
+
case "$(cat "$dir/watch.out")" in
|
|
1645
|
+
check:*task-a.check.sh:*merged) ;;
|
|
1646
|
+
*) fail "merge-outcome-uncommitted: poll retry did not re-emit the outcome" ;;
|
|
1647
|
+
esac
|
|
1648
|
+
assert_grep "$url" "$state/.wake-queue" \
|
|
1649
|
+
"merge-outcome-uncommitted: drained outcome was not durably re-emitted"
|
|
1650
|
+
fm_pr_poll_merge_already_notified "$state" task-a github github.com o/r 1 \
|
|
1651
|
+
|| fail "merge-outcome-uncommitted: successful retry did not commit the marker"
|
|
1652
|
+
assert_poll_absent "$state" task-a
|
|
1653
|
+
pass "staged self-merge and poll interleavings are never silent"
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
test_merged_poll_reports_upward_from_a_secondmate_home_once() {
|
|
1657
|
+
local dir state rc replies url
|
|
1658
|
+
url=https://github.com/o/r/pull/1
|
|
1659
|
+
dir=$(make_case merged-poll-upward)
|
|
1660
|
+
state="$dir/home/state"
|
|
1661
|
+
replies="$state/parent-replies.status"
|
|
1662
|
+
seed_secondmate_home "$dir"
|
|
1663
|
+
write_poll_meta "$state" task-a "$url"
|
|
1664
|
+
seed_canonical_poll "$dir" task-a "$url"
|
|
1665
|
+
add_stop_custom_check "$dir"
|
|
1666
|
+
|
|
1667
|
+
set +e
|
|
1668
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-1.out" 2> "$dir/watch-1.err"
|
|
1669
|
+
rc=$?
|
|
1670
|
+
set -e
|
|
1671
|
+
[ "$rc" -eq 0 ] || fail "merged-poll-upward: watcher failed: $(cat "$dir/watch-1.err")"
|
|
1672
|
+
case "$(cat "$dir/watch-1.out")" in
|
|
1673
|
+
check:*task-a.check.sh:*merged) ;;
|
|
1674
|
+
*) fail "merged-poll-upward: the poll's own row was lost: $(cat "$dir/watch-1.out")" ;;
|
|
1675
|
+
esac
|
|
1676
|
+
assert_grep "done [key=merged-task-a]: merged task-a $url" "$replies" \
|
|
1677
|
+
"merged-poll-upward: a merge this home did not perform was never reported upward"
|
|
1678
|
+
[ "$(grep -c -F "$url" "$replies")" -eq 1 ] \
|
|
1679
|
+
|| fail "merged-poll-upward: one detected merge produced more than one upward line"
|
|
1680
|
+
ack_watcher_cycle "$state" || fail "merged-poll-upward: acknowledgement failed"
|
|
1681
|
+
|
|
1682
|
+
# Re-registered for the same, already-reported merge: the absorbed duplicate
|
|
1683
|
+
# must not tell the parent a second time either.
|
|
1684
|
+
seed_canonical_poll "$dir" task-a "$url"
|
|
1685
|
+
rm -f "$state/.last-check"
|
|
1686
|
+
set +e
|
|
1687
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-2.out" 2> "$dir/watch-2.err"
|
|
1688
|
+
rc=$?
|
|
1689
|
+
set -e
|
|
1690
|
+
[ "$rc" -eq 0 ] || fail "merged-poll-upward: second watcher cycle failed: $(cat "$dir/watch-2.err")"
|
|
1691
|
+
[ "$(grep -c -F "$url" "$replies")" -eq 1 ] \
|
|
1692
|
+
|| fail "merged-poll-upward: an absorbed duplicate detection reported the merge again"
|
|
1693
|
+
pass "a merge detected by the poll is reported upward from a secondmate home exactly once"
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
test_different_merged_pr_for_same_task_is_not_absorbed() {
|
|
1697
|
+
local dir state rc
|
|
1698
|
+
dir=$(make_case different-merged-pr-not-absorbed)
|
|
1699
|
+
state="$dir/home/state"
|
|
1700
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/1
|
|
1701
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/1
|
|
1702
|
+
|
|
1703
|
+
set +e
|
|
1704
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-1.out" 2> "$dir/watch-1.err"
|
|
1705
|
+
rc=$?
|
|
1706
|
+
set -e
|
|
1707
|
+
[ "$rc" -eq 0 ] || fail "first merged watcher cycle failed: $(cat "$dir/watch-1.err")"
|
|
1708
|
+
case "$(cat "$dir/watch-1.out")" in
|
|
1709
|
+
check:*task-a.check.sh:*merged) ;;
|
|
1710
|
+
*) fail "first PR merge confirmation was not delivered: $(cat "$dir/watch-1.out")" ;;
|
|
1711
|
+
esac
|
|
1712
|
+
ack_watcher_cycle "$state" || fail "first PR merge confirmation acknowledgement failed"
|
|
1713
|
+
assert_poll_absent "$state" task-a
|
|
1714
|
+
|
|
1715
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/2
|
|
1716
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/2
|
|
1717
|
+
rm -f "$state/.last-check"
|
|
1718
|
+
|
|
1719
|
+
set +e
|
|
1720
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch-2.out" 2> "$dir/watch-2.err"
|
|
1721
|
+
rc=$?
|
|
1722
|
+
set -e
|
|
1723
|
+
[ "$rc" -eq 0 ] || fail "different-PR watcher cycle failed: $(cat "$dir/watch-2.err")"
|
|
1724
|
+
case "$(cat "$dir/watch-2.out")" in
|
|
1725
|
+
check:*task-a.check.sh:*merged) ;;
|
|
1726
|
+
*) fail "a different PR merge was absorbed: $(cat "$dir/watch-2.out")" ;;
|
|
1727
|
+
esac
|
|
1728
|
+
grep -F "$(printf '\tcheck\tmerged-task-a-https://github.com/o/r/pull/2\t')" \
|
|
1729
|
+
"$state/.wake-queue" >/dev/null 2>&1 \
|
|
1730
|
+
|| fail "the different PR merge did not create a main-blocking wake row"
|
|
1731
|
+
fm_pr_poll_merge_already_notified "$state" task-a github github.com o/r 2 \
|
|
1732
|
+
|| fail "the marker was not advanced to the different PR identity"
|
|
1733
|
+
! fm_pr_poll_merge_already_notified "$state" task-a github github.com o/r 1 \
|
|
1734
|
+
|| fail "the marker still matched the superseded PR identity"
|
|
1735
|
+
assert_poll_absent "$state" task-a
|
|
1736
|
+
pass "a different merged PR for the same task gets its own first notification"
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
test_persistent_secondmate_retirement_is_poll_only() {
|
|
1740
|
+
local dir state meta_before status_before registry_before endpoint_before rc
|
|
1741
|
+
dir=$(make_case merged-retirement-secondmate)
|
|
1742
|
+
state="$dir/home/state"
|
|
1743
|
+
fm_write_meta "$state/domain.meta" \
|
|
1744
|
+
'window=session:fm-domain' \
|
|
1745
|
+
"worktree=$dir/secondmate-home" \
|
|
1746
|
+
"project=$dir/project" \
|
|
1747
|
+
'kind=secondmate' \
|
|
1748
|
+
'mode=secondmate' \
|
|
1749
|
+
'backend=tmux' \
|
|
1750
|
+
"home=$dir/secondmate-home" \
|
|
1751
|
+
'pr=https://github.com/o/r/pull/2' \
|
|
1752
|
+
'pr_head=0123456789abcdef0123456789abcdef01234567'
|
|
1753
|
+
mkdir -p "$dir/secondmate-home"
|
|
1754
|
+
printf 'working: persistent endpoint remains healthy\n' > "$state/domain.status"
|
|
1755
|
+
printf -- '- domain | scope: test | home: %s\n' "$dir/secondmate-home" > "$dir/home/data/secondmates.md"
|
|
1756
|
+
printf 'endpoint-alive\n' > "$dir/endpoint-sentinel"
|
|
1757
|
+
meta_before=$(shasum -a 256 "$state/domain.meta")
|
|
1758
|
+
status_before=$(shasum -a 256 "$state/domain.status")
|
|
1759
|
+
registry_before=$(shasum -a 256 "$dir/home/data/secondmates.md")
|
|
1760
|
+
endpoint_before=$(shasum -a 256 "$dir/endpoint-sentinel")
|
|
1761
|
+
seed_canonical_poll "$dir" domain https://github.com/o/r/pull/2
|
|
1762
|
+
|
|
1763
|
+
set +e
|
|
1764
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
1765
|
+
rc=$?
|
|
1766
|
+
set -e
|
|
1767
|
+
[ "$rc" -eq 0 ] || fail "persistent secondmate merged watcher failed: $(cat "$dir/watch.err")"
|
|
1768
|
+
assert_poll_absent "$state" domain
|
|
1769
|
+
[ "$(shasum -a 256 "$state/domain.meta")" = "$meta_before" ] || fail "retirement changed secondmate metadata"
|
|
1770
|
+
[ "$(shasum -a 256 "$state/domain.status")" = "$status_before" ] || fail "retirement changed secondmate status"
|
|
1771
|
+
[ "$(shasum -a 256 "$dir/home/data/secondmates.md")" = "$registry_before" ] || fail "retirement changed secondmate registry"
|
|
1772
|
+
[ "$(shasum -a 256 "$dir/endpoint-sentinel")" = "$endpoint_before" ] || fail "retirement changed secondmate endpoint evidence"
|
|
1773
|
+
[ -d "$dir/secondmate-home" ] || fail "retirement removed the persistent secondmate home"
|
|
1774
|
+
pass "merged poll retirement preserves every persistent secondmate lifecycle artifact"
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
test_retirement_crash_recovery() {
|
|
1778
|
+
local dir state rc raw_count drain_count historical_poll
|
|
1779
|
+
|
|
1780
|
+
dir=$(make_case retirement-after-queue)
|
|
1781
|
+
state="$dir/home/state"
|
|
1782
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/3
|
|
1783
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/3
|
|
1784
|
+
FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_wake_append check "$2" "$3"' _ \
|
|
1785
|
+
"$ROOT/bin/fm-wake-lib.sh" "$state/task-a.check.sh" "check: $state/task-a.check.sh: merged" \
|
|
1786
|
+
|| fail "could not seed post-queue crash"
|
|
1787
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/recovery.out" 2> "$dir/recovery.err" \
|
|
1788
|
+
|| fail "post-queue crash recovery wake failed: $(cat "$dir/recovery.err")"
|
|
1789
|
+
grep -F 'check: rearm-resurface' "$dir/recovery.out" >/dev/null \
|
|
1790
|
+
|| fail "post-queue crash did not surface its durable recovery first"
|
|
1791
|
+
ack_watcher_cycle "$state" || fail "post-queue crash recovery acknowledgement failed"
|
|
1792
|
+
set +e
|
|
1793
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
1794
|
+
rc=$?
|
|
1795
|
+
set -e
|
|
1796
|
+
[ "$rc" -eq 0 ] || fail "post-queue retry watcher failed: $(cat "$dir/watch.err")"
|
|
1797
|
+
assert_poll_absent "$state" task-a
|
|
1798
|
+
raw_count=$(grep -cF "$(printf '\tcheck\tmerged-task-a-https://github.com/o/r/pull/3\t')" \
|
|
1799
|
+
"$state/.wake-queue" || true)
|
|
1800
|
+
[ "$raw_count" -eq 1 ] || fail "post-queue retry did not publish exactly one new terminal row"
|
|
1801
|
+
FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" "$ROOT/bin/fm-wake-drain.sh" > "$dir/drain.out" 2>/dev/null
|
|
1802
|
+
drain_count=$(grep -cF "$(printf '\tcheck\tmerged-task-a-https://github.com/o/r/pull/3\t')" \
|
|
1803
|
+
"$dir/drain.out" || true)
|
|
1804
|
+
[ "$drain_count" -eq 1 ] || fail "same-key crash retry rows did not deduplicate at drain"
|
|
1805
|
+
|
|
1806
|
+
dir=$(make_case retirement-after-receipt)
|
|
1807
|
+
state="$dir/home/state"
|
|
1808
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/4
|
|
1809
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/4
|
|
1810
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot receipt crash fixture"
|
|
1811
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish crash receipt"
|
|
1812
|
+
add_stop_custom_check "$dir"
|
|
1813
|
+
set +e
|
|
1814
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/restart.out" 2> "$dir/restart.err"
|
|
1815
|
+
rc=$?
|
|
1816
|
+
set -e
|
|
1817
|
+
[ "$rc" -eq 0 ] || fail "receipt recovery watcher failed: $(cat "$dir/restart.err")"
|
|
1818
|
+
assert_poll_absent "$state" task-a
|
|
1819
|
+
! grep -F 'task-a.check.sh: merged' "$dir/restart.out" >/dev/null || fail "receipt recovery duplicated the terminal wake"
|
|
1820
|
+
|
|
1821
|
+
dir=$(make_case retirement-after-check-removal)
|
|
1822
|
+
state="$dir/home/state"
|
|
1823
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/5
|
|
1824
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/5
|
|
1825
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot partial removal fixture"
|
|
1826
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish partial removal receipt"
|
|
1827
|
+
rm -f "$state/task-a.check.sh"
|
|
1828
|
+
add_stop_custom_check "$dir"
|
|
1829
|
+
set +e
|
|
1830
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/restart.out" 2> "$dir/restart.err"
|
|
1831
|
+
rc=$?
|
|
1832
|
+
set -e
|
|
1833
|
+
[ "$rc" -eq 0 ] || fail "post-check-removal restart failed: $(cat "$dir/restart.err")"
|
|
1834
|
+
case "$(cat "$dir/restart.out")" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "post-check-removal restart did not reach the control check" ;; esac
|
|
1835
|
+
assert_poll_absent "$state" task-a
|
|
1836
|
+
fm_pr_poll_retirement_recover_one "$state" task-a "$POLL" || fail "completed retirement was not idempotent"
|
|
1837
|
+
|
|
1838
|
+
dir=$(make_case retirement-after-registration-removal)
|
|
1839
|
+
state="$dir/home/state"
|
|
1840
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/5
|
|
1841
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/5
|
|
1842
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot sidecar-removal fixture"
|
|
1843
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish sidecar-removal receipt"
|
|
1844
|
+
rm -f "$state/task-a.check.sh" "$state/task-a.pr-poll-registration"
|
|
1845
|
+
add_stop_custom_check "$dir"
|
|
1846
|
+
set +e
|
|
1847
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/restart.out" 2> "$dir/restart.err"
|
|
1848
|
+
rc=$?
|
|
1849
|
+
set -e
|
|
1850
|
+
[ "$rc" -eq 0 ] || fail "post-registration-removal restart failed: $(cat "$dir/restart.err")"
|
|
1851
|
+
case "$(cat "$dir/restart.out")" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "post-registration-removal restart did not reach the control check" ;; esac
|
|
1852
|
+
assert_poll_absent "$state" task-a
|
|
1853
|
+
|
|
1854
|
+
dir=$(make_case retirement-before-receipt-removal)
|
|
1855
|
+
state="$dir/home/state"
|
|
1856
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/5
|
|
1857
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/5
|
|
1858
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot receipt-only fixture"
|
|
1859
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish receipt-only fixture"
|
|
1860
|
+
rm -f "$state/task-a.check.sh" "$state/task-a.pr-poll-registration" "$state/task-a.pr-poll"
|
|
1861
|
+
add_stop_custom_check "$dir"
|
|
1862
|
+
set +e
|
|
1863
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/restart.out" 2> "$dir/restart.err"
|
|
1864
|
+
rc=$?
|
|
1865
|
+
set -e
|
|
1866
|
+
[ "$rc" -eq 0 ] || fail "receipt-only restart failed: $(cat "$dir/restart.err")"
|
|
1867
|
+
case "$(cat "$dir/restart.out")" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "receipt-only restart did not reach the control check" ;; esac
|
|
1868
|
+
assert_poll_absent "$state" task-a
|
|
1869
|
+
|
|
1870
|
+
dir=$(make_case retirement-after-template-update)
|
|
1871
|
+
state="$dir/home/state"
|
|
1872
|
+
historical_poll="$dir/historical-fm-pr-poll.sh"
|
|
1873
|
+
cp "$POLL" "$historical_poll"
|
|
1874
|
+
printf '\n' >> "$historical_poll"
|
|
1875
|
+
chmod 0600 "$historical_poll"
|
|
1876
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/22
|
|
1877
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/22 "$historical_poll"
|
|
1878
|
+
FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_wake_append check "$2" "$3"' _ \
|
|
1879
|
+
"$ROOT/bin/fm-wake-lib.sh" "$state/task-a.check.sh" "check: $state/task-a.check.sh: merged" \
|
|
1880
|
+
|| fail "could not seed pre-update terminal wake"
|
|
1881
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$historical_poll" \
|
|
1882
|
+
|| fail "could not snapshot pre-update retirement fixture"
|
|
1883
|
+
fm_pr_poll_retirement_publish "$state" task-a "$historical_poll" merged \
|
|
1884
|
+
|| fail "could not publish pre-update retirement receipt"
|
|
1885
|
+
add_stop_custom_check "$dir"
|
|
1886
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/template-recovery.out" 2> "$dir/template-recovery.err" \
|
|
1887
|
+
|| fail "template-update recovery wake failed: $(cat "$dir/template-recovery.err")"
|
|
1888
|
+
grep -F 'check: rearm-resurface' "$dir/template-recovery.out" >/dev/null \
|
|
1889
|
+
|| fail "template-update recovery did not surface its durable wake first"
|
|
1890
|
+
ack_watcher_cycle "$state" || fail "template-update recovery acknowledgement failed"
|
|
1891
|
+
set +e
|
|
1892
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/restart.out" 2> "$dir/restart.err"
|
|
1893
|
+
rc=$?
|
|
1894
|
+
set -e
|
|
1895
|
+
[ "$rc" -eq 0 ] || fail "template-update recovery watcher failed: $(cat "$dir/restart.err")"
|
|
1896
|
+
case "$(cat "$dir/restart.out")" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "template-update recovery did not reach the control check" ;; esac
|
|
1897
|
+
[ ! -s "$dir/gh.log" ] || fail "template-update migration rebuilt and queried the retired poll"
|
|
1898
|
+
! grep "$(printf '\tcheck\ttask-a.check.sh\t')" "$state/.wake-queue" >/dev/null 2>&1 \
|
|
1899
|
+
|| fail "template-update recovery left the handled terminal wake queued"
|
|
1900
|
+
assert_poll_absent "$state" task-a
|
|
1901
|
+
pass "queue, receipt, and every fixed-path removal crash point recover without loss or repeated execution"
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
test_external_merge_transition_retires_only_terminal_poll() {
|
|
1905
|
+
local dir state before rc label
|
|
1906
|
+
dir=$(make_case external-merge-transition)
|
|
1907
|
+
state="$dir/home/state"
|
|
1908
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/19
|
|
1909
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/19
|
|
1910
|
+
add_stop_custom_check "$dir"
|
|
1911
|
+
before=$(poll_artifact_snapshot "$state" task-a)
|
|
1912
|
+
|
|
1913
|
+
for label in open-green open-red closed-unmerged forge-error malformed; do
|
|
1914
|
+
rm -f "$state/.last-check"
|
|
1915
|
+
set +e
|
|
1916
|
+
case "$label" in
|
|
1917
|
+
open-green|open-red)
|
|
1918
|
+
FM_TEST_GH_STATE=OPEN run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/$label.out" 2> "$dir/$label.err"
|
|
1919
|
+
;;
|
|
1920
|
+
closed-unmerged)
|
|
1921
|
+
FM_TEST_GH_STATE=CLOSED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/$label.out" 2> "$dir/$label.err"
|
|
1922
|
+
;;
|
|
1923
|
+
forge-error)
|
|
1924
|
+
FM_TEST_GH_FAIL=1 run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/$label.out" 2> "$dir/$label.err"
|
|
1925
|
+
;;
|
|
1926
|
+
malformed)
|
|
1927
|
+
FM_TEST_GH_STATE=NOT_A_FORGE_STATE run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/$label.out" 2> "$dir/$label.err"
|
|
1928
|
+
;;
|
|
1929
|
+
esac
|
|
1930
|
+
rc=$?
|
|
1931
|
+
set -e
|
|
1932
|
+
[ "$rc" -eq 0 ] || fail "$label watcher cycle failed: $(cat "$dir/$label.err")"
|
|
1933
|
+
case "$(cat "$dir/$label.out")" in check:*z-stop.check.sh:*stop-cycle) ;; *) fail "$label did not reach the control check" ;; esac
|
|
1934
|
+
[ "$(poll_artifact_snapshot "$state" task-a)" = "$before" ] || fail "$label changed the armed poll"
|
|
1935
|
+
ack_watcher_cycle "$state" || fail "$label control wake acknowledgement failed"
|
|
1936
|
+
done
|
|
1937
|
+
|
|
1938
|
+
rm -f "$state/z-stop.check.sh" "$state/z-stop.check-trust" "$state/.last-check"
|
|
1939
|
+
set +e
|
|
1940
|
+
FM_TEST_GH_STATE=MERGED run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/merged.out" 2> "$dir/merged.err"
|
|
1941
|
+
rc=$?
|
|
1942
|
+
set -e
|
|
1943
|
+
[ "$rc" -eq 0 ] || fail "external merged transition failed: $(cat "$dir/merged.err")"
|
|
1944
|
+
case "$(cat "$dir/merged.out")" in check:*task-a.check.sh:*merged) ;; *) fail "external merge did not preserve its notification" ;; esac
|
|
1945
|
+
assert_poll_absent "$state" task-a
|
|
1946
|
+
pass "open/red, closed-unmerged, malformed, and forge errors remain armed until an exact merged transition"
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
test_retirement_refuses_replacement_and_nonterminal_results() {
|
|
1950
|
+
local dir state before rc replacement_check replacement_data replacement_registration replacement_meta
|
|
1951
|
+
local historical_poll current_poll
|
|
1952
|
+
dir=$(make_case retirement-replacement)
|
|
1953
|
+
state="$dir/home/state"
|
|
1954
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/6
|
|
1955
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/6
|
|
1956
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot replacement fixture"
|
|
1957
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/7
|
|
1958
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/7
|
|
1959
|
+
before=$(state_snapshot "$state")
|
|
1960
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged \
|
|
1961
|
+
&& fail "stale terminal snapshot retired a replacement poll"
|
|
1962
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "snapshot mismatch changed replacement artifacts"
|
|
1963
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$POLL" || fail "replacement poll was not left canonical"
|
|
1964
|
+
|
|
1965
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot nonterminal fixture"
|
|
1966
|
+
for result in OPEN CLOSED ERROR 'merged extra'; do
|
|
1967
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" "$result" \
|
|
1968
|
+
&& fail "nonterminal result '$result' received retirement authority"
|
|
1969
|
+
done
|
|
1970
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "nonterminal result changed canonical artifacts"
|
|
1971
|
+
|
|
1972
|
+
printf '# tamper\n' >> "$state/task-a.check.sh"
|
|
1973
|
+
before=$(state_snapshot "$state")
|
|
1974
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged \
|
|
1975
|
+
&& fail "tampered check received a retirement receipt"
|
|
1976
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "tampered retirement attempt changed state"
|
|
1977
|
+
|
|
1978
|
+
dir=$(make_case retirement-rearm-race)
|
|
1979
|
+
state="$dir/home/state"
|
|
1980
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/20
|
|
1981
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/20
|
|
1982
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot rearm-race fixture"
|
|
1983
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish rearm-race receipt"
|
|
1984
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/21
|
|
1985
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/21
|
|
1986
|
+
replacement_check=$(shasum -a 256 "$state/task-a.check.sh")
|
|
1987
|
+
replacement_data=$(shasum -a 256 "$state/task-a.pr-poll")
|
|
1988
|
+
replacement_registration=$(shasum -a 256 "$state/task-a.pr-poll-registration")
|
|
1989
|
+
replacement_meta=$(shasum -a 256 "$state/task-a.meta")
|
|
1990
|
+
fm_pr_poll_retirement_recover_one "$state" task-a "$POLL" || fail "stale receipt did not yield to a canonical replacement"
|
|
1991
|
+
[ ! -e "$state/task-a.pr-poll-retirement" ] || fail "stale receipt survived canonical replacement recovery"
|
|
1992
|
+
[ "$(shasum -a 256 "$state/task-a.check.sh")" = "$replacement_check" ] || fail "stale receipt changed replacement check"
|
|
1993
|
+
[ "$(shasum -a 256 "$state/task-a.pr-poll")" = "$replacement_data" ] || fail "stale receipt changed replacement data"
|
|
1994
|
+
[ "$(shasum -a 256 "$state/task-a.pr-poll-registration")" = "$replacement_registration" ] || fail "stale receipt changed replacement registration"
|
|
1995
|
+
[ "$(shasum -a 256 "$state/task-a.meta")" = "$replacement_meta" ] || fail "stale receipt changed replacement metadata"
|
|
1996
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$POLL" || fail "replacement poll lost canonical provenance"
|
|
1997
|
+
|
|
1998
|
+
dir=$(make_case retirement-template-update-rearm-race)
|
|
1999
|
+
state="$dir/home/state"
|
|
2000
|
+
historical_poll="$dir/historical-fm-pr-poll.sh"
|
|
2001
|
+
current_poll="$dir/current-fm-pr-poll.sh"
|
|
2002
|
+
cp "$POLL" "$historical_poll"
|
|
2003
|
+
cp "$POLL" "$current_poll"
|
|
2004
|
+
printf '\n' >> "$current_poll"
|
|
2005
|
+
chmod 0600 "$historical_poll" "$current_poll"
|
|
2006
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/23
|
|
2007
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/23 "$historical_poll"
|
|
2008
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$historical_poll" \
|
|
2009
|
+
|| fail "could not snapshot pre-update rearm fixture"
|
|
2010
|
+
fm_pr_poll_retirement_publish "$state" task-a "$historical_poll" merged \
|
|
2011
|
+
|| fail "could not publish pre-update rearm receipt"
|
|
2012
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/24
|
|
2013
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/24 "$current_poll"
|
|
2014
|
+
replacement_check=$(shasum -a 256 "$state/task-a.check.sh")
|
|
2015
|
+
replacement_data=$(shasum -a 256 "$state/task-a.pr-poll")
|
|
2016
|
+
replacement_registration=$(shasum -a 256 "$state/task-a.pr-poll-registration")
|
|
2017
|
+
replacement_meta=$(shasum -a 256 "$state/task-a.meta")
|
|
2018
|
+
fm_pr_poll_retirement_recover_one "$state" task-a "$current_poll" \
|
|
2019
|
+
|| fail "template update blocked stale receipt recovery"
|
|
2020
|
+
[ ! -e "$state/task-a.pr-poll-retirement" ] || fail "pre-update receipt survived canonical replacement recovery"
|
|
2021
|
+
[ "$(shasum -a 256 "$state/task-a.check.sh")" = "$replacement_check" ] || fail "pre-update receipt changed replacement check"
|
|
2022
|
+
[ "$(shasum -a 256 "$state/task-a.pr-poll")" = "$replacement_data" ] || fail "pre-update receipt changed replacement data"
|
|
2023
|
+
[ "$(shasum -a 256 "$state/task-a.pr-poll-registration")" = "$replacement_registration" ] || fail "pre-update receipt changed replacement registration"
|
|
2024
|
+
[ "$(shasum -a 256 "$state/task-a.meta")" = "$replacement_meta" ] || fail "pre-update receipt changed replacement metadata"
|
|
2025
|
+
fm_pr_poll_artifacts_valid "$state" task-a "$current_poll" || fail "updated replacement poll lost canonical provenance"
|
|
2026
|
+
|
|
2027
|
+
dir=$(make_case custom-merged-not-retired)
|
|
2028
|
+
state="$dir/home/state"
|
|
2029
|
+
printf '#!/usr/bin/env bash\nprintf "merged\\n"\n' > "$state/custom.check.sh"
|
|
2030
|
+
chmod 0700 "$state/custom.check.sh"
|
|
2031
|
+
FM_HOME="$dir/home" "$REGISTER" custom >/dev/null || fail "could not register merged custom check"
|
|
2032
|
+
set +e
|
|
2033
|
+
run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/custom.out" 2> "$dir/custom.err"
|
|
2034
|
+
rc=$?
|
|
2035
|
+
set -e
|
|
2036
|
+
[ "$rc" -eq 0 ] || fail "merged custom watcher failed: $(cat "$dir/custom.err")"
|
|
2037
|
+
[ -f "$state/custom.check.sh" ] && [ -f "$state/custom.check-trust" ] \
|
|
2038
|
+
|| fail "custom merged output was retired as a PR poll"
|
|
2039
|
+
[ ! -e "$state/custom.pr-poll-retirement" ] || fail "custom check received a PR receipt"
|
|
2040
|
+
pass "replacement, nonterminal, tampered, and custom results receive no deletion authority"
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
test_retirement_queue_failure_and_receipt_tampering() {
|
|
2044
|
+
local dir state before rc external
|
|
2045
|
+
dir=$(make_case retirement-queue-failure)
|
|
2046
|
+
state="$dir/home/state"
|
|
2047
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/8
|
|
2048
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/8
|
|
2049
|
+
# Fail sequence publication without making the queue itself look non-empty:
|
|
2050
|
+
# a directory at .wake-queue would now (correctly) trigger re-arm recovery
|
|
2051
|
+
# before the poll runs, so it no longer exercises the terminal append path.
|
|
2052
|
+
mkdir "$state/.wake-queue.seq"
|
|
2053
|
+
before=$(poll_artifact_snapshot "$state" task-a)
|
|
2054
|
+
set +e
|
|
2055
|
+
FM_TEST_GH_LOG="$dir/gh.log" FM_TEST_GH_STATE=MERGED \
|
|
2056
|
+
run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
2057
|
+
rc=$?
|
|
2058
|
+
set -e
|
|
2059
|
+
[ "$rc" -ne 0 ] || fail "watcher retired despite queue publication failure"
|
|
2060
|
+
[ -s "$dir/gh.log" ] || fail "queue failure fixture did not reach the authenticated poll"
|
|
2061
|
+
[ "$(poll_artifact_snapshot "$state" task-a)" = "$before" ] || fail "queue failure changed poll artifacts"
|
|
2062
|
+
[ ! -e "$state/task-a.pr-poll-retirement" ] || fail "queue failure published a receipt"
|
|
2063
|
+
|
|
2064
|
+
dir=$(make_case retirement-receipt-tamper)
|
|
2065
|
+
state="$dir/home/state"
|
|
2066
|
+
write_poll_meta "$state" task-a https://github.com/o/r/pull/9
|
|
2067
|
+
seed_canonical_poll "$dir" task-a https://github.com/o/r/pull/9
|
|
2068
|
+
fm_pr_poll_snapshot_capture "$state" task-a "$POLL" || fail "could not snapshot receipt tamper fixture"
|
|
2069
|
+
fm_pr_poll_retirement_publish "$state" task-a "$POLL" merged || fail "could not publish receipt tamper fixture"
|
|
2070
|
+
printf 'extra\n' >> "$state/task-a.pr-poll-retirement"
|
|
2071
|
+
before=$(state_snapshot "$state")
|
|
2072
|
+
fm_pr_poll_retirement_recover_one "$state" task-a "$POLL" \
|
|
2073
|
+
&& fail "malformed receipt authorized poll deletion"
|
|
2074
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "malformed receipt changed poll state"
|
|
2075
|
+
set +e
|
|
2076
|
+
run_check_entry "$dir" task-a https://github.com/o/r/pull/10 > "$dir/rearm.out" 2> "$dir/rearm.err"
|
|
2077
|
+
rc=$?
|
|
2078
|
+
set -e
|
|
2079
|
+
[ "$rc" -ne 0 ] || fail "rearm accepted an invalid pending retirement receipt"
|
|
2080
|
+
[ "$(cat "$dir/rearm.err")" = 'error: pending PR poll retirement could not be validated' ] \
|
|
2081
|
+
|| fail "rearm did not report the invalid pending receipt"
|
|
2082
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "refused rearm changed poll state"
|
|
2083
|
+
|
|
2084
|
+
rm -f "$state/task-a.pr-poll-retirement"
|
|
2085
|
+
external="$dir/external-receipt"
|
|
2086
|
+
printf 'external\n' > "$external"
|
|
2087
|
+
ln -s "$external" "$state/task-a.pr-poll-retirement"
|
|
2088
|
+
before=$(state_snapshot "$state")
|
|
2089
|
+
fm_pr_poll_retirement_recover_one "$state" task-a "$POLL" \
|
|
2090
|
+
&& fail "receipt symlink authorized poll deletion"
|
|
2091
|
+
[ "$(state_snapshot "$state")" = "$before" ] || fail "receipt symlink changed poll state"
|
|
2092
|
+
[ "$(cat "$external")" = external ] || fail "receipt symlink target was changed"
|
|
2093
|
+
pass "queue failure and untrusted receipts preserve canonical poll evidence"
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
test_gitlab_merged_poll_retires() {
|
|
2097
|
+
local dir state url rc
|
|
2098
|
+
dir=$(make_case gitlab-merged-retirement)
|
|
2099
|
+
state="$dir/home/state"
|
|
2100
|
+
url=https://gitlab.example/group/subgroup/project/-/merge_requests/17
|
|
2101
|
+
write_poll_meta "$state" task-a "$url"
|
|
2102
|
+
seed_canonical_poll "$dir" task-a "$url"
|
|
2103
|
+
set +e
|
|
2104
|
+
FM_TEST_GLAB_STATE=merged run_watcher_bounded "$dir/home" "$dir/fakebin" > "$dir/watch.out" 2> "$dir/watch.err"
|
|
2105
|
+
rc=$?
|
|
2106
|
+
set -e
|
|
2107
|
+
[ "$rc" -eq 0 ] || fail "GitLab merged retirement watcher failed: $(cat "$dir/watch.err")"
|
|
2108
|
+
case "$(cat "$dir/watch.out")" in check:*task-a.check.sh:*merged) ;; *) fail "GitLab merged wake was missing" ;; esac
|
|
2109
|
+
assert_poll_absent "$state" task-a
|
|
2110
|
+
grep -qxF "pr=$url" "$state/task-a.meta" || fail "GitLab retirement removed canonical metadata"
|
|
2111
|
+
pass "GitHub and GitLab exact merged results share one retirement path"
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
test_parser_matrix
|
|
2115
|
+
test_gitlab_merge_watch
|
|
2116
|
+
test_merged_poll_retires_once
|
|
2117
|
+
test_merged_poll_reregistration_after_notification_is_absorbed
|
|
2118
|
+
test_merged_poll_retries_a_failed_upward_report
|
|
2119
|
+
test_self_merge_and_poll_publish_one_outcome
|
|
2120
|
+
test_merged_poll_reports_upward_from_a_secondmate_home_once
|
|
2121
|
+
test_different_merged_pr_for_same_task_is_not_absorbed
|
|
2122
|
+
test_persistent_secondmate_retirement_is_poll_only
|
|
2123
|
+
test_retirement_crash_recovery
|
|
2124
|
+
test_external_merge_transition_retires_only_terminal_poll
|
|
2125
|
+
test_retirement_refuses_replacement_and_nonterminal_results
|
|
2126
|
+
test_retirement_queue_failure_and_receipt_tampering
|
|
2127
|
+
test_gitlab_merged_poll_retires
|
|
2128
|
+
test_invalid_entrypoints_have_zero_side_effects
|
|
2129
|
+
test_valid_recording_and_merge_derivation
|
|
2130
|
+
test_rejected_metacharacter_bytes_are_inert
|
|
2131
|
+
test_static_poll_contract
|
|
2132
|
+
test_atomic_interruption_leaves_no_partial_artifact
|
|
2133
|
+
test_concurrent_watcher_sees_only_complete_publication
|
|
2134
|
+
test_poll_publication_refuses_unsafe_destinations
|
|
2135
|
+
test_live_artifact_single_link_and_privacy_validation
|
|
2136
|
+
test_postrename_poll_validation_revokes_and_retries
|
|
2137
|
+
test_bootstrap_leaves_unauthenticated_checks
|
|
2138
|
+
test_custom_snapshot_cleanup_on_signal
|
|
2139
|
+
test_returned_custom_check_descendants_are_drained
|
|
2140
|
+
test_teardown_removes_poll_artifacts
|