@pennyfarthing/core 11.0.0 → 11.1.1
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/README.md +81 -23
- package/package.json +1 -1
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
- package/packages/core/dist/cli/utils/constants.d.ts +8 -2
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +4 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
- package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/constants.test.js +38 -0
- package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
- package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.js +178 -0
- package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
- package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
- package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/public/js/react/react.js +3 -3
- package/packages/core/dist/scripts/theme-detail.test.d.ts +10 -0
- package/packages/core/dist/scripts/theme-detail.test.js +199 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
- package/packages/core/dist/server/api/health-score.js +25 -1
- package/packages/core/dist/server/api/health-score.js.map +1 -1
- package/packages/core/dist/server/api/settings.d.ts.map +1 -1
- package/packages/core/dist/server/api/settings.js +63 -1
- package/packages/core/dist/server/api/settings.js.map +1 -1
- package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
- package/packages/core/dist/server/api/theme-agents.js +61 -0
- package/packages/core/dist/server/api/theme-agents.js.map +1 -1
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +17 -12
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
- package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.js +157 -0
- package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
- package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
- package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
- package/packages/core/dist/workflow/handoff.js +2 -2
- package/packages/core/dist/workflow/handoff.js.map +1 -1
- package/packages/core/dist/workflow/handoff.test.js +16 -0
- package/packages/core/dist/workflow/handoff.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +43 -8
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +6 -14
- package/pennyfarthing-dist/agents/architect.md +43 -29
- package/pennyfarthing-dist/agents/ba.md +30 -29
- package/pennyfarthing-dist/agents/dev.md +32 -43
- package/pennyfarthing-dist/agents/devops.md +57 -21
- package/pennyfarthing-dist/agents/orchestrator.md +3 -10
- package/pennyfarthing-dist/agents/pm.md +45 -31
- package/pennyfarthing-dist/agents/reviewer.md +20 -66
- package/pennyfarthing-dist/agents/sm-setup.md +2 -2
- package/pennyfarthing-dist/agents/sm.md +8 -30
- package/pennyfarthing-dist/agents/tea.md +25 -41
- package/pennyfarthing-dist/agents/tech-writer.md +33 -90
- package/pennyfarthing-dist/agents/ux-designer.md +39 -39
- package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
- package/pennyfarthing-dist/commands/benchmark.md +8 -480
- package/pennyfarthing-dist/commands/job-fair.md +8 -97
- package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
- package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
- package/pennyfarthing-dist/commands/pf-chore.md +4 -4
- package/pennyfarthing-dist/commands/pf-ci.md +40 -0
- package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
- package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
- package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
- package/pennyfarthing-dist/commands/pf-docs.md +28 -0
- package/pennyfarthing-dist/commands/pf-epic.md +67 -0
- package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
- package/pennyfarthing-dist/commands/pf-git.md +75 -0
- package/pennyfarthing-dist/commands/pf-help.md +110 -128
- package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
- package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
- package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
- package/pennyfarthing-dist/commands/pf-release.md +11 -76
- package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
- package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
- package/pennyfarthing-dist/commands/pf-session.md +51 -0
- package/pennyfarthing-dist/commands/pf-solo.md +447 -0
- package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
- package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
- package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
- package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
- package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
- package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
- package/pennyfarthing-dist/commands/solo.md +8 -442
- package/pennyfarthing-dist/guides/agent-behavior.md +13 -13
- package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -5
- package/pennyfarthing-dist/guides/bikerack.md +128 -0
- package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
- package/pennyfarthing-dist/guides/gate-schema.md +227 -0
- package/pennyfarthing-dist/guides/gates.md +120 -0
- package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
- package/pennyfarthing-dist/guides/hooks.md +86 -4
- package/pennyfarthing-dist/guides/output-styles.md +65 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
- package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
- package/pennyfarthing-dist/guides/reflector.md +4 -4
- package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
- package/pennyfarthing-dist/guides/skill-schema.md +1 -1
- package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +5 -4
- package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
- package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
- package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
- package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
- package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +5 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
- package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
- package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
- package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
- package/pennyfarthing-dist/workflows/trivial.yaml +2 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +114 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__init__.py +0 -0
- package/pennyfarthing_scripts/epic/cli.py +64 -0
- package/pennyfarthing_scripts/gate/__init__.py +1 -0
- package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/cli.py +56 -0
- package/pennyfarthing_scripts/gate/validate.py +266 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__init__.py +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +100 -0
- package/pennyfarthing_scripts/handoff/__init__.py +1 -0
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/cli.py +120 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
- package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
- package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
- package/pennyfarthing_scripts/handoff/marker.py +109 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/workflow.py +39 -0
- package/pennyfarthing_scripts/session/__init__.py +0 -0
- package/pennyfarthing_scripts/session/cli.py +87 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_108_1_gate_migration.py +540 -0
- package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
- package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
- package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
- package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
- package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
- package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
- package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/skill_command.py +200 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
- package/pennyfarthing_scripts/validate/cli.py +15 -4
- package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
- package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
- package/packages/core/dist/scripts/benchmark-integration.js +0 -691
- package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
- package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
- package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
- package/pennyfarthing-dist/agents/handoff.md +0 -250
- package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Tests for SM confidence gate file — Story 90-2.
|
|
2
|
+
|
|
3
|
+
Epic: 90 (Confidence Circuit Breaker via Gate)
|
|
4
|
+
Story: 90-2 — Implement SM confidence gate file
|
|
5
|
+
|
|
6
|
+
Tests the confidence-sm gate file that checks whether an instruction to the
|
|
7
|
+
SM agent is ambiguous. If ambiguous, <fail> returns clarifying options. If
|
|
8
|
+
unambiguous, <pass> lets the agent proceed.
|
|
9
|
+
|
|
10
|
+
Acceptance Criteria:
|
|
11
|
+
- [AC1] Gate file exists in pennyfarthing-dist/gates/ following Gate PRD schema
|
|
12
|
+
- [AC2] Gate has <gate>, <purpose>, <pass>, <fail> blocks
|
|
13
|
+
- [AC3] Gate checks whether SM instruction is ambiguous
|
|
14
|
+
- [AC4] <fail> block returns clarifying options when ambiguous
|
|
15
|
+
- [AC5] <pass> block lets the agent proceed when unambiguous
|
|
16
|
+
- [AC6] Gate uses model="haiku"
|
|
17
|
+
- [AC7] Gate validates against existing schema (same structure as tests-pass.md)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from pennyfarthing_scripts.handoff.gate_file import resolve_gate_file
|
|
27
|
+
from pennyfarthing_scripts.handoff.gate_runner import parse_gate_file
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Fixtures
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
GATE_NAME = "confidence-sm"
|
|
34
|
+
|
|
35
|
+
# The gate file lives in pennyfarthing-dist/gates/ relative to the framework root
|
|
36
|
+
# In the dogfooding context, the project root is the orchestrator, so we need
|
|
37
|
+
# to resolve paths relative to this test file.
|
|
38
|
+
_THIS_DIR = Path(__file__).resolve().parent
|
|
39
|
+
_SCRIPTS_DIR = _THIS_DIR.parent # pennyfarthing_scripts/
|
|
40
|
+
_FRAMEWORK_ROOT = _SCRIPTS_DIR.parent # pennyfarthing/
|
|
41
|
+
_GATE_FILE = _FRAMEWORK_ROOT / "pennyfarthing-dist" / "gates" / f"{GATE_NAME}.md"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def gate_path() -> Path:
|
|
46
|
+
"""Return the expected path to the confidence-sm gate file."""
|
|
47
|
+
return _GATE_FILE
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def gate_content(gate_path: Path) -> str:
|
|
52
|
+
"""Read and return the gate file content. Fails if file missing."""
|
|
53
|
+
assert gate_path.is_file(), f"Gate file not found: {gate_path}"
|
|
54
|
+
return gate_path.read_text()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def parsed_gate(gate_path: Path) -> dict:
|
|
59
|
+
"""Parse the gate file using the gate runner's parse_gate_file."""
|
|
60
|
+
return parse_gate_file(gate_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ===========================================================================
|
|
64
|
+
# AC1: Gate file exists in pennyfarthing-dist/gates/
|
|
65
|
+
# ===========================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestGateFileExists:
|
|
69
|
+
"""AC1: Gate file exists at the expected location."""
|
|
70
|
+
|
|
71
|
+
def test_gate_file_exists(self, gate_path: Path) -> None:
|
|
72
|
+
"""AC1: confidence-sm.md exists in pennyfarthing-dist/gates/."""
|
|
73
|
+
assert gate_path.is_file(), f"Gate file not found: {gate_path}"
|
|
74
|
+
|
|
75
|
+
def test_gate_file_not_empty(self, gate_path: Path) -> None:
|
|
76
|
+
"""AC1: Gate file is not empty."""
|
|
77
|
+
assert gate_path.is_file(), f"Gate file not found: {gate_path}"
|
|
78
|
+
content = gate_path.read_text()
|
|
79
|
+
assert len(content.strip()) > 0, "Gate file is empty"
|
|
80
|
+
|
|
81
|
+
def test_gate_discoverable(self, gate_path: Path) -> None:
|
|
82
|
+
"""AC1: Gate is discoverable via resolve_gate_file()."""
|
|
83
|
+
# Use the framework root which has pennyfarthing-dist/gates/
|
|
84
|
+
result = resolve_gate_file(GATE_NAME, project_root=_FRAMEWORK_ROOT)
|
|
85
|
+
assert result["status"] == "found", (
|
|
86
|
+
f"Gate not discoverable: {result.get('error')}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_gate_discoverable_with_prefix(self, gate_path: Path) -> None:
|
|
90
|
+
"""AC1: Gate discoverable with gates/ prefix."""
|
|
91
|
+
result = resolve_gate_file(
|
|
92
|
+
f"gates/{GATE_NAME}", project_root=_FRAMEWORK_ROOT
|
|
93
|
+
)
|
|
94
|
+
assert result["status"] == "found"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ===========================================================================
|
|
98
|
+
# AC2: Gate has <gate>, <purpose>, <pass>, <fail> blocks
|
|
99
|
+
# ===========================================================================
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestGateSchemaStructure:
|
|
103
|
+
"""AC2: Gate file has all required XML-tagged blocks."""
|
|
104
|
+
|
|
105
|
+
def test_has_gate_tag(self, gate_content: str) -> None:
|
|
106
|
+
"""AC2: File contains a <gate> opening tag."""
|
|
107
|
+
assert "<gate " in gate_content or "<gate>" in gate_content
|
|
108
|
+
|
|
109
|
+
def test_has_closing_gate_tag(self, gate_content: str) -> None:
|
|
110
|
+
"""AC2: File contains a </gate> closing tag."""
|
|
111
|
+
assert "</gate>" in gate_content
|
|
112
|
+
|
|
113
|
+
def test_has_purpose_block(self, gate_content: str) -> None:
|
|
114
|
+
"""AC2: File contains <purpose> and </purpose> tags."""
|
|
115
|
+
assert "<purpose>" in gate_content
|
|
116
|
+
assert "</purpose>" in gate_content
|
|
117
|
+
|
|
118
|
+
def test_has_pass_block(self, gate_content: str) -> None:
|
|
119
|
+
"""AC2: File contains <pass> and </pass> tags."""
|
|
120
|
+
assert "<pass>" in gate_content
|
|
121
|
+
assert "</pass>" in gate_content
|
|
122
|
+
|
|
123
|
+
def test_has_fail_block(self, gate_content: str) -> None:
|
|
124
|
+
"""AC2: File contains <fail> and </fail> tags."""
|
|
125
|
+
assert "<fail>" in gate_content
|
|
126
|
+
assert "</fail>" in gate_content
|
|
127
|
+
|
|
128
|
+
def test_parses_without_error(self, parsed_gate: dict) -> None:
|
|
129
|
+
"""AC2: parse_gate_file returns status 'ok'."""
|
|
130
|
+
assert parsed_gate["status"] == "ok", (
|
|
131
|
+
f"Parse error: {parsed_gate.get('error')}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ===========================================================================
|
|
136
|
+
# AC3: Gate checks whether SM instruction is ambiguous
|
|
137
|
+
# ===========================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestGateAmbiguityDetection:
|
|
141
|
+
"""AC3: Gate content describes checking for ambiguous SM instructions."""
|
|
142
|
+
|
|
143
|
+
def test_purpose_mentions_ambiguity(self, gate_content: str) -> None:
|
|
144
|
+
"""AC3: Purpose section references ambiguity or unclear instructions."""
|
|
145
|
+
# Extract purpose content between tags
|
|
146
|
+
import re
|
|
147
|
+
|
|
148
|
+
purpose_match = re.search(
|
|
149
|
+
r"<purpose>(.*?)</purpose>", gate_content, re.DOTALL
|
|
150
|
+
)
|
|
151
|
+
assert purpose_match is not None, "No <purpose> block found"
|
|
152
|
+
purpose = purpose_match.group(1).lower()
|
|
153
|
+
assert any(
|
|
154
|
+
term in purpose
|
|
155
|
+
for term in ["ambig", "unclear", "vague", "confidence", "clarif"]
|
|
156
|
+
), f"Purpose doesn't reference ambiguity: {purpose}"
|
|
157
|
+
|
|
158
|
+
def test_gate_name_is_confidence_sm(self, parsed_gate: dict) -> None:
|
|
159
|
+
"""AC3: Gate name is 'confidence-sm'."""
|
|
160
|
+
assert parsed_gate["name"] == GATE_NAME
|
|
161
|
+
|
|
162
|
+
def test_content_references_sm_agent(self, gate_content: str) -> None:
|
|
163
|
+
"""AC3: Gate content references the SM agent or scrum master role."""
|
|
164
|
+
content_lower = gate_content.lower()
|
|
165
|
+
assert any(
|
|
166
|
+
term in content_lower
|
|
167
|
+
for term in ["sm agent", "scrum master", "sm ", "story management"]
|
|
168
|
+
), "Gate doesn't reference SM agent"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ===========================================================================
|
|
172
|
+
# AC4: <fail> block returns clarifying options when ambiguous
|
|
173
|
+
# ===========================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestGateFailBlock:
|
|
177
|
+
"""AC4: Fail block provides clarifying options for ambiguous instructions."""
|
|
178
|
+
|
|
179
|
+
def test_fail_block_has_content(self, gate_content: str) -> None:
|
|
180
|
+
"""AC4: Fail block is not empty."""
|
|
181
|
+
import re
|
|
182
|
+
|
|
183
|
+
fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
|
|
184
|
+
assert fail_match is not None, "No <fail> block found"
|
|
185
|
+
fail_content = fail_match.group(1).strip()
|
|
186
|
+
assert len(fail_content) > 0, "Fail block is empty"
|
|
187
|
+
|
|
188
|
+
def test_fail_block_mentions_clarification(self, gate_content: str) -> None:
|
|
189
|
+
"""AC4: Fail block mentions clarifying or asking for more info."""
|
|
190
|
+
import re
|
|
191
|
+
|
|
192
|
+
fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
|
|
193
|
+
assert fail_match is not None
|
|
194
|
+
fail_content = fail_match.group(1).lower()
|
|
195
|
+
assert any(
|
|
196
|
+
term in fail_content
|
|
197
|
+
for term in ["clarif", "option", "which", "specify", "did you mean"]
|
|
198
|
+
), f"Fail block doesn't offer clarifying options: {fail_content[:200]}"
|
|
199
|
+
|
|
200
|
+
def test_fail_block_has_gate_result(self, gate_content: str) -> None:
|
|
201
|
+
"""AC4: Fail block includes GATE_RESULT template with status: fail."""
|
|
202
|
+
import re
|
|
203
|
+
|
|
204
|
+
fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
|
|
205
|
+
assert fail_match is not None
|
|
206
|
+
fail_content = fail_match.group(1)
|
|
207
|
+
assert "GATE_RESULT" in fail_content, "Fail block missing GATE_RESULT"
|
|
208
|
+
assert "status: fail" in fail_content, "Fail block missing status: fail"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ===========================================================================
|
|
212
|
+
# AC5: <pass> block lets the agent proceed when unambiguous
|
|
213
|
+
# ===========================================================================
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestGatePassBlock:
|
|
217
|
+
"""AC5: Pass block describes proceeding when instruction is clear."""
|
|
218
|
+
|
|
219
|
+
def test_pass_block_has_content(self, gate_content: str) -> None:
|
|
220
|
+
"""AC5: Pass block is not empty."""
|
|
221
|
+
import re
|
|
222
|
+
|
|
223
|
+
pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
|
|
224
|
+
assert pass_match is not None, "No <pass> block found"
|
|
225
|
+
pass_content = pass_match.group(1).strip()
|
|
226
|
+
assert len(pass_content) > 0, "Pass block is empty"
|
|
227
|
+
|
|
228
|
+
def test_pass_block_indicates_proceed(self, gate_content: str) -> None:
|
|
229
|
+
"""AC5: Pass block indicates the agent should proceed."""
|
|
230
|
+
import re
|
|
231
|
+
|
|
232
|
+
pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
|
|
233
|
+
assert pass_match is not None
|
|
234
|
+
pass_content = pass_match.group(1).lower()
|
|
235
|
+
assert any(
|
|
236
|
+
term in pass_content
|
|
237
|
+
for term in ["proceed", "continue", "clear", "unambig", "confident"]
|
|
238
|
+
), f"Pass block doesn't indicate proceeding: {pass_content[:200]}"
|
|
239
|
+
|
|
240
|
+
def test_pass_block_has_gate_result(self, gate_content: str) -> None:
|
|
241
|
+
"""AC5: Pass block includes GATE_RESULT template with status: pass."""
|
|
242
|
+
import re
|
|
243
|
+
|
|
244
|
+
pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
|
|
245
|
+
assert pass_match is not None
|
|
246
|
+
pass_content = pass_match.group(1)
|
|
247
|
+
assert "GATE_RESULT" in pass_content, "Pass block missing GATE_RESULT"
|
|
248
|
+
assert "status: pass" in pass_content, "Pass block missing status: pass"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ===========================================================================
|
|
252
|
+
# AC6: Gate uses model="haiku"
|
|
253
|
+
# ===========================================================================
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestGateModel:
|
|
257
|
+
"""AC6: Gate specifies model="haiku" per Gate PRD defaults."""
|
|
258
|
+
|
|
259
|
+
def test_model_is_haiku(self, parsed_gate: dict) -> None:
|
|
260
|
+
"""AC6: parse_gate_file extracts model as 'haiku' from a valid gate."""
|
|
261
|
+
assert parsed_gate["status"] == "ok", (
|
|
262
|
+
f"Gate parse failed: {parsed_gate.get('error')}"
|
|
263
|
+
)
|
|
264
|
+
assert parsed_gate["model"] == "haiku", (
|
|
265
|
+
f"Expected model 'haiku', got '{parsed_gate['model']}'"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def test_gate_tag_has_model_attribute(self, gate_content: str) -> None:
|
|
269
|
+
"""AC6: <gate> tag explicitly includes model="haiku"."""
|
|
270
|
+
import re
|
|
271
|
+
|
|
272
|
+
gate_match = re.search(r"<gate\b[^>]*>", gate_content)
|
|
273
|
+
assert gate_match is not None
|
|
274
|
+
gate_tag = gate_match.group(0)
|
|
275
|
+
assert 'model="haiku"' in gate_tag, (
|
|
276
|
+
f"<gate> tag missing model=\"haiku\": {gate_tag}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ===========================================================================
|
|
281
|
+
# AC7: Validates against existing schema (same structure as tests-pass.md)
|
|
282
|
+
# ===========================================================================
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestGateSchemaConsistency:
|
|
286
|
+
"""AC7: Gate follows the same structural patterns as tests-pass.md."""
|
|
287
|
+
|
|
288
|
+
def test_result_has_all_required_keys(self, parsed_gate: dict) -> None:
|
|
289
|
+
"""AC7: parse_gate_file result has all expected keys and parse succeeds."""
|
|
290
|
+
assert parsed_gate["status"] == "ok", (
|
|
291
|
+
f"Gate parse failed: {parsed_gate.get('error')}"
|
|
292
|
+
)
|
|
293
|
+
for key in ("status", "name", "model", "content", "error"):
|
|
294
|
+
assert key in parsed_gate, f"Missing key: {key}"
|
|
295
|
+
|
|
296
|
+
def test_name_matches_filename(self, parsed_gate: dict) -> None:
|
|
297
|
+
"""AC7: Gate name attribute matches the filename convention."""
|
|
298
|
+
assert parsed_gate["name"] == GATE_NAME
|
|
299
|
+
|
|
300
|
+
def test_content_is_full_file(self, gate_path: Path, parsed_gate: dict) -> None:
|
|
301
|
+
"""AC7: Parsed content matches raw file read."""
|
|
302
|
+
assert parsed_gate["status"] == "ok"
|
|
303
|
+
raw = gate_path.read_text()
|
|
304
|
+
assert parsed_gate["content"] == raw
|
|
305
|
+
|
|
306
|
+
def test_gate_result_yaml_in_both_blocks(self, gate_content: str) -> None:
|
|
307
|
+
"""AC7: Both pass and fail blocks include GATE_RESULT YAML blocks."""
|
|
308
|
+
import re
|
|
309
|
+
|
|
310
|
+
pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
|
|
311
|
+
fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
|
|
312
|
+
assert pass_match is not None
|
|
313
|
+
assert fail_match is not None
|
|
314
|
+
assert "GATE_RESULT:" in pass_match.group(1)
|
|
315
|
+
assert "GATE_RESULT:" in fail_match.group(1)
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Tests for gate file discovery and resolution — Story 106-4.
|
|
2
|
+
|
|
3
|
+
Epic: 106 (Gate Files & First Migration)
|
|
4
|
+
Story: 106-4 — Gate file discovery and resolution
|
|
5
|
+
|
|
6
|
+
Tests the resolve_gate_file() function that locates gate definition files
|
|
7
|
+
using a priority-based discovery algorithm.
|
|
8
|
+
|
|
9
|
+
Acceptance Criteria:
|
|
10
|
+
- [AC1] resolve_gate_file() resolves gate names to file paths
|
|
11
|
+
- [AC2] Resolution order: .pennyfarthing/gates/{name}.md → pennyfarthing-dist/gates/{name}.md
|
|
12
|
+
- [AC3] Non-existent gate file returns error/blocked status
|
|
13
|
+
- [AC5] Tests cover: found in local, found in built-in, not found, symlink resolution
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from pennyfarthing_scripts.handoff.gate_file import resolve_gate_file
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Fixtures: Project structure
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def project(tmp_path: Path) -> Path:
|
|
31
|
+
"""Create a minimal project structure with .pennyfarthing/ and gates."""
|
|
32
|
+
(tmp_path / ".pennyfarthing").mkdir()
|
|
33
|
+
(tmp_path / ".pennyfarthing" / "gates").mkdir()
|
|
34
|
+
(tmp_path / "pennyfarthing-dist" / "gates").mkdir(parents=True)
|
|
35
|
+
return tmp_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def project_with_builtin_gate(project: Path) -> Path:
|
|
40
|
+
"""Project with a gate file in pennyfarthing-dist/gates/ only."""
|
|
41
|
+
gate = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
|
|
42
|
+
gate.write_text(
|
|
43
|
+
'<gate name="tests-pass" model="haiku">\n'
|
|
44
|
+
" <purpose>Verify tests pass</purpose>\n"
|
|
45
|
+
" <pass>Check tests</pass>\n"
|
|
46
|
+
" <fail>Report failures</fail>\n"
|
|
47
|
+
"</gate>\n"
|
|
48
|
+
)
|
|
49
|
+
return project
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def project_with_local_gate(project: Path) -> Path:
|
|
54
|
+
"""Project with a gate file in .pennyfarthing/gates/ only."""
|
|
55
|
+
gate = project / ".pennyfarthing" / "gates" / "custom-gate.md"
|
|
56
|
+
gate.write_text(
|
|
57
|
+
'<gate name="custom-gate" model="haiku">\n'
|
|
58
|
+
" <purpose>Custom project gate</purpose>\n"
|
|
59
|
+
" <pass>Custom pass</pass>\n"
|
|
60
|
+
" <fail>Custom fail</fail>\n"
|
|
61
|
+
"</gate>\n"
|
|
62
|
+
)
|
|
63
|
+
return project
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def project_with_both_gates(project: Path) -> Path:
|
|
68
|
+
"""Project with same gate in both locations (local should win)."""
|
|
69
|
+
# Built-in version
|
|
70
|
+
builtin = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
|
|
71
|
+
builtin.write_text(
|
|
72
|
+
'<gate name="tests-pass" model="haiku">\n'
|
|
73
|
+
" <purpose>Built-in version</purpose>\n"
|
|
74
|
+
" <pass>Built-in pass</pass>\n"
|
|
75
|
+
" <fail>Built-in fail</fail>\n"
|
|
76
|
+
"</gate>\n"
|
|
77
|
+
)
|
|
78
|
+
# Local override
|
|
79
|
+
local = project / ".pennyfarthing" / "gates" / "tests-pass.md"
|
|
80
|
+
local.write_text(
|
|
81
|
+
'<gate name="tests-pass" model="haiku">\n'
|
|
82
|
+
" <purpose>Local override version</purpose>\n"
|
|
83
|
+
" <pass>Local pass</pass>\n"
|
|
84
|
+
" <fail>Local fail</fail>\n"
|
|
85
|
+
"</gate>\n"
|
|
86
|
+
)
|
|
87
|
+
return project
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.fixture
|
|
91
|
+
def project_with_symlinked_gates(project: Path) -> Path:
|
|
92
|
+
"""Project where .pennyfarthing/gates is a symlink to pennyfarthing-dist/gates."""
|
|
93
|
+
import shutil
|
|
94
|
+
|
|
95
|
+
# Remove the real .pennyfarthing/gates dir
|
|
96
|
+
shutil.rmtree(project / ".pennyfarthing" / "gates")
|
|
97
|
+
# Create symlink (mimics real install)
|
|
98
|
+
(project / ".pennyfarthing" / "gates").symlink_to(
|
|
99
|
+
project / "pennyfarthing-dist" / "gates"
|
|
100
|
+
)
|
|
101
|
+
# Add a gate file to the source
|
|
102
|
+
gate = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
|
|
103
|
+
gate.write_text(
|
|
104
|
+
'<gate name="tests-pass" model="haiku">\n'
|
|
105
|
+
" <purpose>Symlinked gate</purpose>\n"
|
|
106
|
+
" <pass>Check tests</pass>\n"
|
|
107
|
+
" <fail>Report failures</fail>\n"
|
|
108
|
+
"</gate>\n"
|
|
109
|
+
)
|
|
110
|
+
return project
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ===========================================================================
|
|
114
|
+
# AC1: resolve_gate_file() resolves gate names to file paths
|
|
115
|
+
# ===========================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestResolveGateFileFound:
|
|
119
|
+
"""AC1: Function returns path when gate file exists."""
|
|
120
|
+
|
|
121
|
+
def test_returns_found_status(
|
|
122
|
+
self, project_with_builtin_gate: Path
|
|
123
|
+
) -> None:
|
|
124
|
+
"""AC1: Status should be 'found' when gate file exists."""
|
|
125
|
+
result = resolve_gate_file(
|
|
126
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
127
|
+
)
|
|
128
|
+
assert result["status"] == "found"
|
|
129
|
+
|
|
130
|
+
def test_returns_absolute_path(
|
|
131
|
+
self, project_with_builtin_gate: Path
|
|
132
|
+
) -> None:
|
|
133
|
+
"""AC1: Path should be an absolute path string."""
|
|
134
|
+
result = resolve_gate_file(
|
|
135
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
136
|
+
)
|
|
137
|
+
assert result["path"] is not None
|
|
138
|
+
assert Path(result["path"]).is_absolute()
|
|
139
|
+
|
|
140
|
+
def test_path_points_to_existing_file(
|
|
141
|
+
self, project_with_builtin_gate: Path
|
|
142
|
+
) -> None:
|
|
143
|
+
"""AC1: Returned path should point to an actual file."""
|
|
144
|
+
result = resolve_gate_file(
|
|
145
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
146
|
+
)
|
|
147
|
+
assert Path(result["path"]).is_file()
|
|
148
|
+
|
|
149
|
+
def test_error_is_none_when_found(
|
|
150
|
+
self, project_with_builtin_gate: Path
|
|
151
|
+
) -> None:
|
|
152
|
+
"""AC1: Error field should be None when gate is found."""
|
|
153
|
+
result = resolve_gate_file(
|
|
154
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
155
|
+
)
|
|
156
|
+
assert result["error"] is None
|
|
157
|
+
|
|
158
|
+
def test_strips_gates_prefix(
|
|
159
|
+
self, project_with_builtin_gate: Path
|
|
160
|
+
) -> None:
|
|
161
|
+
"""AC1: 'gates/tests-pass' and 'tests-pass' should resolve the same."""
|
|
162
|
+
result_bare = resolve_gate_file(
|
|
163
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
164
|
+
)
|
|
165
|
+
result_prefixed = resolve_gate_file(
|
|
166
|
+
"gates/tests-pass", project_root=project_with_builtin_gate
|
|
167
|
+
)
|
|
168
|
+
assert result_bare["path"] == result_prefixed["path"]
|
|
169
|
+
|
|
170
|
+
def test_result_has_required_fields(
|
|
171
|
+
self, project_with_builtin_gate: Path
|
|
172
|
+
) -> None:
|
|
173
|
+
"""AC1: Result dict must have status, path, error keys."""
|
|
174
|
+
result = resolve_gate_file(
|
|
175
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
176
|
+
)
|
|
177
|
+
assert "status" in result
|
|
178
|
+
assert "path" in result
|
|
179
|
+
assert "error" in result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ===========================================================================
|
|
183
|
+
# AC2: Resolution order — local first, built-in fallback
|
|
184
|
+
# ===========================================================================
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestResolveGateFileOrder:
|
|
188
|
+
"""AC2: .pennyfarthing/gates/ takes priority over pennyfarthing-dist/gates/."""
|
|
189
|
+
|
|
190
|
+
def test_local_gate_found(self, project_with_local_gate: Path) -> None:
|
|
191
|
+
"""AC2: Gate in .pennyfarthing/gates/ should be found."""
|
|
192
|
+
result = resolve_gate_file(
|
|
193
|
+
"custom-gate", project_root=project_with_local_gate
|
|
194
|
+
)
|
|
195
|
+
assert result["status"] == "found"
|
|
196
|
+
assert ".pennyfarthing/gates/custom-gate.md" in result["path"]
|
|
197
|
+
|
|
198
|
+
def test_builtin_gate_found_as_fallback(
|
|
199
|
+
self, project_with_builtin_gate: Path
|
|
200
|
+
) -> None:
|
|
201
|
+
"""AC2: Gate in pennyfarthing-dist/gates/ found when not in local."""
|
|
202
|
+
result = resolve_gate_file(
|
|
203
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
204
|
+
)
|
|
205
|
+
assert result["status"] == "found"
|
|
206
|
+
assert "pennyfarthing-dist/gates/tests-pass.md" in result["path"]
|
|
207
|
+
|
|
208
|
+
def test_local_overrides_builtin(
|
|
209
|
+
self, project_with_both_gates: Path
|
|
210
|
+
) -> None:
|
|
211
|
+
"""AC2: When same gate exists in both, local wins."""
|
|
212
|
+
result = resolve_gate_file(
|
|
213
|
+
"tests-pass", project_root=project_with_both_gates
|
|
214
|
+
)
|
|
215
|
+
assert result["status"] == "found"
|
|
216
|
+
# Path should be the .pennyfarthing/gates/ version, not pennyfarthing-dist/
|
|
217
|
+
assert ".pennyfarthing/gates/tests-pass.md" in result["path"]
|
|
218
|
+
|
|
219
|
+
def test_local_override_content_is_local_version(
|
|
220
|
+
self, project_with_both_gates: Path
|
|
221
|
+
) -> None:
|
|
222
|
+
"""AC2: Content at resolved path should be the local version."""
|
|
223
|
+
result = resolve_gate_file(
|
|
224
|
+
"tests-pass", project_root=project_with_both_gates
|
|
225
|
+
)
|
|
226
|
+
content = Path(result["path"]).read_text()
|
|
227
|
+
assert "Local override version" in content
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ===========================================================================
|
|
231
|
+
# AC3: Non-existent gate file returns error/blocked
|
|
232
|
+
# ===========================================================================
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TestResolveGateFileNotFound:
|
|
236
|
+
"""AC3: Missing gate files return error status."""
|
|
237
|
+
|
|
238
|
+
def test_missing_gate_returns_not_found(self, project: Path) -> None:
|
|
239
|
+
"""AC3: Non-existent gate → status: not_found."""
|
|
240
|
+
result = resolve_gate_file("nonexistent-gate", project_root=project)
|
|
241
|
+
assert result["status"] == "not_found"
|
|
242
|
+
|
|
243
|
+
def test_missing_gate_path_is_none(self, project: Path) -> None:
|
|
244
|
+
"""AC3: Non-existent gate → path: None."""
|
|
245
|
+
result = resolve_gate_file("nonexistent-gate", project_root=project)
|
|
246
|
+
assert result["path"] is None
|
|
247
|
+
|
|
248
|
+
def test_missing_gate_has_error_message(self, project: Path) -> None:
|
|
249
|
+
"""AC3: Non-existent gate → error message present."""
|
|
250
|
+
result = resolve_gate_file("nonexistent-gate", project_root=project)
|
|
251
|
+
assert result["error"] is not None
|
|
252
|
+
assert len(result["error"]) > 0
|
|
253
|
+
|
|
254
|
+
def test_empty_gate_name_returns_not_found(self, project: Path) -> None:
|
|
255
|
+
"""AC3: Empty string gate name → not_found."""
|
|
256
|
+
result = resolve_gate_file("", project_root=project)
|
|
257
|
+
assert result["status"] == "not_found"
|
|
258
|
+
|
|
259
|
+
def test_missing_gates_directory_returns_not_found(
|
|
260
|
+
self, tmp_path: Path
|
|
261
|
+
) -> None:
|
|
262
|
+
"""AC3: No gates/ directories at all → not_found (no crash)."""
|
|
263
|
+
# Project with .pennyfarthing but no gates subdirectory
|
|
264
|
+
(tmp_path / ".pennyfarthing").mkdir()
|
|
265
|
+
result = resolve_gate_file("tests-pass", project_root=tmp_path)
|
|
266
|
+
assert result["status"] == "not_found"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ===========================================================================
|
|
270
|
+
# AC5: Symlink resolution
|
|
271
|
+
# ===========================================================================
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestResolveGateFileSymlink:
|
|
275
|
+
"""AC5: Gate files found through symlinked .pennyfarthing/gates/."""
|
|
276
|
+
|
|
277
|
+
def test_symlinked_gate_found(
|
|
278
|
+
self, project_with_symlinked_gates: Path
|
|
279
|
+
) -> None:
|
|
280
|
+
"""AC5: Gate found through .pennyfarthing/gates/ symlink."""
|
|
281
|
+
result = resolve_gate_file(
|
|
282
|
+
"tests-pass", project_root=project_with_symlinked_gates
|
|
283
|
+
)
|
|
284
|
+
assert result["status"] == "found"
|
|
285
|
+
|
|
286
|
+
def test_symlinked_gate_path_is_valid(
|
|
287
|
+
self, project_with_symlinked_gates: Path
|
|
288
|
+
) -> None:
|
|
289
|
+
"""AC5: Path through symlink points to real file."""
|
|
290
|
+
result = resolve_gate_file(
|
|
291
|
+
"tests-pass", project_root=project_with_symlinked_gates
|
|
292
|
+
)
|
|
293
|
+
assert result["path"] is not None
|
|
294
|
+
assert Path(result["path"]).exists()
|
|
295
|
+
|
|
296
|
+
def test_symlinked_gate_content_readable(
|
|
297
|
+
self, project_with_symlinked_gates: Path
|
|
298
|
+
) -> None:
|
|
299
|
+
"""AC5: Content at symlinked path is the actual gate file."""
|
|
300
|
+
result = resolve_gate_file(
|
|
301
|
+
"tests-pass", project_root=project_with_symlinked_gates
|
|
302
|
+
)
|
|
303
|
+
content = Path(result["path"]).read_text()
|
|
304
|
+
assert "Symlinked gate" in content
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ===========================================================================
|
|
308
|
+
# Edge cases
|
|
309
|
+
# ===========================================================================
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestResolveGateFileEdgeCases:
|
|
313
|
+
"""Edge cases and boundary conditions."""
|
|
314
|
+
|
|
315
|
+
def test_gate_name_with_md_extension_still_works(
|
|
316
|
+
self, project_with_builtin_gate: Path
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Edge: 'tests-pass.md' should resolve same as 'tests-pass'."""
|
|
319
|
+
result = resolve_gate_file(
|
|
320
|
+
"tests-pass.md", project_root=project_with_builtin_gate
|
|
321
|
+
)
|
|
322
|
+
assert result["status"] == "found"
|
|
323
|
+
|
|
324
|
+
def test_gate_name_with_nested_path_rejected(
|
|
325
|
+
self, project_with_builtin_gate: Path
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Edge: '../escape/tests-pass' should not resolve (path traversal)."""
|
|
328
|
+
result = resolve_gate_file(
|
|
329
|
+
"../escape/tests-pass",
|
|
330
|
+
project_root=project_with_builtin_gate,
|
|
331
|
+
)
|
|
332
|
+
assert result["status"] == "not_found"
|
|
333
|
+
|
|
334
|
+
def test_result_status_is_valid_enum(
|
|
335
|
+
self, project_with_builtin_gate: Path
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Edge: Status must be one of the expected values."""
|
|
338
|
+
result = resolve_gate_file(
|
|
339
|
+
"tests-pass", project_root=project_with_builtin_gate
|
|
340
|
+
)
|
|
341
|
+
assert result["status"] in ("found", "not_found")
|