@pennyfarthing/core 11.2.0 → 11.2.2
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 +100 -40
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +474 -66
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.js +4 -4
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +4 -5
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts +3 -8
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +3 -4
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/cli/utils/settings.d.ts +7 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +70 -29
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +16 -16
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.js +1 -1
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -1
- package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
- package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.js +131 -0
- package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
- package/packages/core/dist/public/css/react.css +1 -1
- package/packages/core/dist/public/js/react/react.js +14 -14
- package/packages/core/dist/server/api/agent-load.js +1 -1
- package/packages/core/dist/server/api/agent-load.js.map +1 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +0 -1
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/api/index.d.ts +2 -0
- package/packages/core/dist/server/api/index.d.ts.map +1 -1
- package/packages/core/dist/server/api/index.js +2 -0
- package/packages/core/dist/server/api/index.js.map +1 -1
- package/packages/core/dist/server/api/project-info.d.ts +11 -0
- package/packages/core/dist/server/api/project-info.d.ts.map +1 -0
- package/packages/core/dist/server/api/project-info.js +18 -0
- package/packages/core/dist/server/api/project-info.js.map +1 -0
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +18 -1
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.js +1 -1
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -1
- package/packages/core/dist/server/server.d.ts +0 -3
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +5 -38
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/server/server.test.d.ts +1 -1
- package/packages/core/dist/server/server.test.js +12 -23
- package/packages/core/dist/server/server.test.js.map +1 -1
- package/packages/core/dist/server/settings.d.ts +1 -0
- package/packages/core/dist/server/settings.d.ts.map +1 -1
- package/packages/core/dist/server/settings.js +13 -0
- package/packages/core/dist/server/settings.js.map +1 -1
- package/packages/core/dist/shared/capabilities.d.ts +88 -0
- package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.js +133 -0
- package/packages/core/dist/shared/capabilities.js.map +1 -0
- package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
- package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.test.js +217 -0
- package/packages/core/dist/shared/capabilities.test.js.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
- package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.js +82 -0
- package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
- package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.d.ts +169 -0
- package/packages/core/dist/workflow/team-lifecycle.d.ts.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.js +217 -0
- package/packages/core/dist/workflow/team-lifecycle.js.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.test.d.ts +20 -0
- package/packages/core/dist/workflow/team-lifecycle.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.test.js +966 -0
- package/packages/core/dist/workflow/team-lifecycle.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +120 -0
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
- package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-team-templates.test.d.ts +17 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.js +275 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +21 -12
- package/pennyfarthing-dist/agents/reviewer.md +23 -4
- package/pennyfarthing-dist/agents/sm-finish.md +19 -2
- package/pennyfarthing-dist/agents/sm-setup.md +7 -7
- package/pennyfarthing-dist/agents/sm.md +12 -12
- package/pennyfarthing-dist/agents/tea.md +2 -2
- package/pennyfarthing-dist/agents/testing-runner.md +1 -1
- package/pennyfarthing-dist/commands/pf-architect.md +1 -1
- package/pennyfarthing-dist/commands/pf-ba.md +1 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-dev.md +1 -1
- package/pennyfarthing-dist/commands/pf-devops.md +1 -1
- package/pennyfarthing-dist/commands/pf-epic.md +6 -6
- package/pennyfarthing-dist/commands/pf-git.md +12 -10
- package/pennyfarthing-dist/commands/pf-health-check.md +1 -1
- package/pennyfarthing-dist/commands/pf-help.md +12 -12
- package/pennyfarthing-dist/commands/pf-orchestrator.md +1 -1
- package/pennyfarthing-dist/commands/pf-pm.md +1 -1
- package/pennyfarthing-dist/commands/pf-prime.md +8 -8
- package/pennyfarthing-dist/commands/pf-reviewer.md +1 -1
- package/pennyfarthing-dist/commands/pf-session.md +7 -7
- package/pennyfarthing-dist/commands/pf-sm.md +1 -1
- package/pennyfarthing-dist/commands/pf-sprint.md +7 -7
- package/pennyfarthing-dist/commands/pf-tea.md +1 -1
- package/pennyfarthing-dist/commands/pf-tech-writer.md +1 -1
- package/pennyfarthing-dist/commands/pf-theme.md +9 -9
- package/pennyfarthing-dist/commands/pf-ux-designer.md +1 -1
- package/pennyfarthing-dist/commands/pf-work.md +1 -1
- package/pennyfarthing-dist/gates/approval.md +63 -0
- package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
- package/pennyfarthing-dist/gates/context-ok.md +56 -0
- package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
- package/pennyfarthing-dist/gates/quality-pass.md +67 -0
- package/pennyfarthing-dist/gates/tests-fail.md +84 -0
- package/pennyfarthing-dist/gates/tests-pass.md +79 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +84 -29
- package/pennyfarthing-dist/guides/agent-coordination.md +10 -10
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
- package/pennyfarthing-dist/guides/agent-template-tactical.md +1 -1
- package/pennyfarthing-dist/guides/bell-mode.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +10 -10
- package/pennyfarthing-dist/guides/brownfield-tools.md +24 -24
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/gate-schema.md +2 -2
- package/pennyfarthing-dist/guides/gates.md +3 -3
- package/pennyfarthing-dist/guides/handoff-cli.md +8 -8
- package/pennyfarthing-dist/guides/hooks.md +29 -29
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/reflector.md +1 -1
- package/pennyfarthing-dist/guides/skill-schema.md +6 -6
- package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
- package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +8 -8
- package/pennyfarthing-dist/scripts/README.md +4 -4
- package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -5
- package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
- package/pennyfarthing-dist/scripts/core/pf.sh +5 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +4 -89
- package/pennyfarthing-dist/scripts/core/prime.sh +2 -25
- package/pennyfarthing-dist/scripts/git/README.md +14 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +2 -3
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +2 -3
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -3
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +2 -4
- package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +5 -4
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +29 -34
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +3 -93
- package/pennyfarthing-dist/scripts/lib/env.sh +34 -0
- package/pennyfarthing-dist/scripts/lib/run-pf.sh +39 -0
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
- package/pennyfarthing-dist/scripts/sprint/README.md +21 -21
- package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +2 -16
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +3 -3
- package/pennyfarthing-dist/skills/pf-bc/examples.md +23 -23
- package/pennyfarthing-dist/skills/pf-bc/skill.md +17 -17
- package/pennyfarthing-dist/skills/pf-bc/usage.md +8 -8
- package/pennyfarthing-dist/skills/pf-jira/SKILL.md +15 -15
- package/pennyfarthing-dist/skills/pf-jira/examples.md +48 -48
- package/pennyfarthing-dist/skills/pf-jira/usage.md +15 -15
- package/pennyfarthing-dist/skills/pf-sprint/examples.md +80 -80
- package/pennyfarthing-dist/skills/pf-sprint/skill.md +35 -35
- package/pennyfarthing-dist/skills/pf-sprint/usage.md +30 -30
- package/pennyfarthing-dist/skills/pf-theme/examples.md +15 -15
- package/pennyfarthing-dist/skills/pf-theme/skill.md +6 -6
- package/pennyfarthing-dist/skills/pf-theme/usage.md +5 -5
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -27
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +11 -11
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +11 -11
- package/pennyfarthing-dist/skills/skill-registry.yaml +19 -19
- package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
- package/pennyfarthing-dist/workflows/bdd-team.yaml +89 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/project-setup/steps/step-01-discover.md +47 -0
- package/pennyfarthing-dist/workflows/tdd-team.yaml +80 -0
- package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
- package/pennyfarthing_scripts/CLAUDE.md +19 -10
- package/pennyfarthing_scripts/__init__.py +1 -1
- 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__/context.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.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/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +2 -2
- package/pennyfarthing_scripts/bellmode_hook.py +9 -296
- package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.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__/context_meter_footer.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__/events.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__/portrait_resolver.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.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/audit_log_panel.py +161 -0
- package/pennyfarthing_scripts/bikerack/base_panel.py +27 -4
- package/pennyfarthing_scripts/bikerack/changed_panel.py +96 -4
- package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
- package/pennyfarthing_scripts/bikerack/debug_panel.py +1 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +30 -0
- package/pennyfarthing_scripts/bikerack/events.py +28 -0
- package/pennyfarthing_scripts/bikerack/launcher.py +6 -6
- package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
- package/pennyfarthing_scripts/bikerack/progress_panel.py +0 -1
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +373 -142
- package/pennyfarthing_scripts/bikerack/story_detail_data.py +247 -0
- package/pennyfarthing_scripts/bikerack/story_detail_screen.py +177 -0
- package/pennyfarthing_scripts/bikerack/tui.py +304 -62
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +5 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +29 -2
- package/pennyfarthing_scripts/common/pr_config.py +38 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +3 -3
- package/pennyfarthing_scripts/context.py +3 -3
- 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__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/hooks_installer.py +2 -3
- package/pennyfarthing_scripts/git/status_all.py +1 -1
- package/pennyfarthing_scripts/git/worktree.py +2 -2
- package/pennyfarthing_scripts/git_group/__pycache__/cli.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__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/cli.py +33 -1
- package/pennyfarthing_scripts/handoff/complete_phase.py +28 -0
- package/pennyfarthing_scripts/handoff/marker.py +15 -15
- package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +13 -1
- package/pennyfarthing_scripts/hooks/__init__.py +442 -0
- package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/bell_mode.py +214 -0
- package/pennyfarthing_scripts/hooks/cli.py +96 -0
- package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
- package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
- package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
- package/pennyfarthing_scripts/hooks/pre_edit_check.py +77 -0
- package/pennyfarthing_scripts/hooks/reflector_check.py +270 -0
- package/pennyfarthing_scripts/hooks/schema_validation.py +202 -0
- package/pennyfarthing_scripts/hooks/session_start.py +294 -0
- package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
- package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
- package/pennyfarthing_scripts/hooks/statusline.py +429 -0
- package/pennyfarthing_scripts/hooks.py +27 -432
- package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +3 -15
- package/pennyfarthing_scripts/prime/workflow.py +2 -1
- package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
- package/pennyfarthing_scripts/session_start_hook.py +4 -186
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.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/cli.py +121 -0
- package/pennyfarthing_scripts/sprint/loader.py +154 -3
- package/pennyfarthing_scripts/sprint/story_update.py +26 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.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_workflow_list_team.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +26 -26
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +0 -1
- package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
- package/pennyfarthing_scripts/tests/test_workflow_list_team.py +147 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/team_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/team_mode.py +323 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
- package/pennyfarthing_scripts/welcome_hook.py +3 -149
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/team_lifecycle.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +22 -20
- package/pennyfarthing_scripts/workflow/state.py +0 -1
- package/pennyfarthing_scripts/workflow/team_lifecycle.py +256 -0
- package/packages/core/dist/cli/cyclist-migration.test.d.ts +0 -16
- package/packages/core/dist/cli/cyclist-migration.test.d.ts.map +0 -1
- package/packages/core/dist/cli/cyclist-migration.test.js +0 -229
- package/packages/core/dist/cli/cyclist-migration.test.js.map +0 -1
- package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
- package/packages/core/dist/scripts/theme-detail.test.d.ts.map +0 -1
- package/packages/core/dist/scripts/theme-detail.test.js +0 -199
- package/packages/core/dist/scripts/theme-detail.test.js.map +0 -1
|
@@ -6,13 +6,13 @@ Epic: 103 — BikeRack TUI (MSSCI-14951)
|
|
|
6
6
|
Acceptance Criteria:
|
|
7
7
|
- [AC1] SprintPanel subscribes to /ws/sprint channel
|
|
8
8
|
- [AC2] Receives and parses JSON payloads: {type, currentStory, nextStory, epics, ...}
|
|
9
|
-
- [AC3] Renders sprint status
|
|
9
|
+
- [AC3] Renders sprint status with epic/story tree hierarchy
|
|
10
10
|
- [AC4] Displays velocity and sprint metrics
|
|
11
11
|
- [AC5] Default panel on TUI launch
|
|
12
12
|
- [AC6] Updates in real-time when data changes on channel
|
|
13
13
|
- [AC7] All tests GREEN (Dev phase)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Migrated to Textual Tree widget from Rich Table rendering.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
@@ -21,11 +21,17 @@ from typing import Any
|
|
|
21
21
|
from unittest.mock import MagicMock, patch
|
|
22
22
|
|
|
23
23
|
import pytest
|
|
24
|
-
from rich.table import Table
|
|
25
24
|
from rich.text import Text
|
|
26
25
|
|
|
27
|
-
from pennyfarthing_scripts.bikerack.
|
|
28
|
-
|
|
26
|
+
from pennyfarthing_scripts.bikerack.sprint_panel import (
|
|
27
|
+
SprintPanel,
|
|
28
|
+
_build_epic_label,
|
|
29
|
+
_build_story_label,
|
|
30
|
+
_format_assignee,
|
|
31
|
+
_is_terminal,
|
|
32
|
+
_should_expand,
|
|
33
|
+
_status_badge,
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
# ---------------------------------------------------------------------------
|
|
31
37
|
# Fixtures
|
|
@@ -177,19 +183,20 @@ class TestSprintPanelChannel:
|
|
|
177
183
|
panel = SprintPanel()
|
|
178
184
|
assert panel.channel == "sprint"
|
|
179
185
|
|
|
180
|
-
def
|
|
181
|
-
"""SprintPanel should be a
|
|
182
|
-
|
|
186
|
+
def test_is_widget_subclass(self) -> None:
|
|
187
|
+
"""SprintPanel should be a Widget subclass."""
|
|
188
|
+
from textual.widget import Widget
|
|
189
|
+
|
|
190
|
+
assert issubclass(SprintPanel, Widget)
|
|
183
191
|
|
|
184
192
|
def test_subscribes_on_mount(self, panel: SprintPanel, mock_client: MagicMock) -> None:
|
|
185
193
|
"""SprintPanel should subscribe to 'sprint' channel on mount."""
|
|
186
194
|
panel.on_mount()
|
|
187
|
-
mock_client.subscribe.assert_called_once_with("sprint", panel.
|
|
195
|
+
mock_client.subscribe.assert_called_once_with("sprint", panel._handle_ws_message)
|
|
188
196
|
|
|
189
197
|
def test_no_subscribe_without_client(self) -> None:
|
|
190
198
|
"""SprintPanel should not crash on mount without client."""
|
|
191
199
|
panel = SprintPanel(client=None)
|
|
192
|
-
# Should not raise
|
|
193
200
|
panel.on_mount()
|
|
194
201
|
|
|
195
202
|
|
|
@@ -201,117 +208,283 @@ class TestSprintPanelChannel:
|
|
|
201
208
|
class TestSprintPanelParsing:
|
|
202
209
|
"""AC2: Receives and parses JSON payloads correctly."""
|
|
203
210
|
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def
|
|
212
|
-
"""
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"""render_panel should handle null currentStory."""
|
|
226
|
-
payload = {**SAMPLE_INIT_PAYLOAD, "currentStory": None}
|
|
227
|
-
result = panel.render_panel(payload)
|
|
228
|
-
assert result is not None
|
|
229
|
-
assert result != ""
|
|
230
|
-
|
|
231
|
-
def test_handles_multi_epic_payload(self, panel: SprintPanel) -> None:
|
|
232
|
-
"""render_panel should handle payloads with multiple epics."""
|
|
233
|
-
result = panel.render_panel(SAMPLE_MULTI_EPIC_PAYLOAD)
|
|
234
|
-
assert result is not None
|
|
235
|
-
assert result != ""
|
|
211
|
+
def test_stores_payload(self, panel: SprintPanel) -> None:
|
|
212
|
+
"""_handle_ws_message should store the payload."""
|
|
213
|
+
panel._mounted = True
|
|
214
|
+
with patch.object(panel, "post_message"):
|
|
215
|
+
panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
|
|
216
|
+
assert panel._last_payload == SAMPLE_INIT_PAYLOAD
|
|
217
|
+
|
|
218
|
+
def test_ignores_none(self, panel: SprintPanel) -> None:
|
|
219
|
+
"""_handle_ws_message should ignore None messages."""
|
|
220
|
+
panel._mounted = True
|
|
221
|
+
with patch.object(panel, "post_message") as mock_post:
|
|
222
|
+
panel._handle_ws_message(None)
|
|
223
|
+
mock_post.assert_not_called()
|
|
224
|
+
|
|
225
|
+
def test_ignores_after_unmount(self, panel: SprintPanel) -> None:
|
|
226
|
+
"""Messages after unmount should be ignored."""
|
|
227
|
+
panel._mounted = True
|
|
228
|
+
panel.on_unmount()
|
|
229
|
+
with patch.object(panel, "post_message") as mock_post:
|
|
230
|
+
panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
|
|
231
|
+
mock_post.assert_not_called()
|
|
236
232
|
|
|
237
233
|
|
|
238
234
|
# ---------------------------------------------------------------------------
|
|
239
|
-
# AC3: Renders sprint status
|
|
235
|
+
# AC3: Renders sprint status with epic/story tree hierarchy
|
|
240
236
|
# ---------------------------------------------------------------------------
|
|
241
237
|
|
|
242
238
|
|
|
243
|
-
class
|
|
244
|
-
"""
|
|
245
|
-
|
|
246
|
-
def
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
assert
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
""
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
239
|
+
class TestStatusBadge:
|
|
240
|
+
"""Status badge helper produces symbol-only Rich Text (no text word)."""
|
|
241
|
+
|
|
242
|
+
def test_done_badge(self) -> None:
|
|
243
|
+
badge = _status_badge("done")
|
|
244
|
+
assert "\u2713" in badge.plain
|
|
245
|
+
assert "done" not in badge.plain
|
|
246
|
+
|
|
247
|
+
def test_in_progress_badge(self) -> None:
|
|
248
|
+
badge = _status_badge("in-progress")
|
|
249
|
+
assert "\u27f3" in badge.plain
|
|
250
|
+
assert "in-progress" not in badge.plain
|
|
251
|
+
|
|
252
|
+
def test_in_progress_underscore(self) -> None:
|
|
253
|
+
"""Handle both 'in-progress' and 'in_progress' status strings."""
|
|
254
|
+
badge = _status_badge("in_progress")
|
|
255
|
+
assert "\u27f3" in badge.plain
|
|
256
|
+
|
|
257
|
+
def test_backlog_badge(self) -> None:
|
|
258
|
+
badge = _status_badge("backlog")
|
|
259
|
+
assert "\u25ef" in badge.plain
|
|
260
|
+
assert "backlog" not in badge.plain
|
|
261
|
+
|
|
262
|
+
def test_blocked_badge(self) -> None:
|
|
263
|
+
badge = _status_badge("blocked")
|
|
264
|
+
assert "!" in badge.plain
|
|
265
|
+
assert "blocked" not in badge.plain
|
|
266
|
+
|
|
267
|
+
def test_review_badge(self) -> None:
|
|
268
|
+
badge = _status_badge("review")
|
|
269
|
+
assert "\u25ce" in badge.plain
|
|
270
|
+
assert "review" not in badge.plain
|
|
271
|
+
|
|
272
|
+
def test_canceled_badge(self) -> None:
|
|
273
|
+
badge = _status_badge("canceled")
|
|
274
|
+
assert "\u2715" in badge.plain
|
|
275
|
+
|
|
276
|
+
def test_cancelled_british_spelling(self) -> None:
|
|
277
|
+
badge = _status_badge("cancelled")
|
|
278
|
+
assert "\u2715" in badge.plain
|
|
279
|
+
|
|
280
|
+
def test_unknown_status(self) -> None:
|
|
281
|
+
badge = _status_badge("unknown-status")
|
|
282
|
+
assert "\u2014" in badge.plain
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestEpicLabel:
|
|
286
|
+
"""Epic label builder produces correct Rich Text."""
|
|
287
|
+
|
|
288
|
+
def test_includes_epic_id_fallback(self) -> None:
|
|
289
|
+
label = _build_epic_label("103", "BikeRack TUI", 4, 6)
|
|
290
|
+
assert "103" in label.plain
|
|
291
|
+
|
|
292
|
+
def test_includes_jira_key_when_provided(self) -> None:
|
|
293
|
+
label = _build_epic_label("103", "BikeRack TUI", 4, 6, jira_key="MSSCI-14510")
|
|
294
|
+
assert "MSSCI-14510" in label.plain
|
|
295
|
+
|
|
296
|
+
def test_long_id_gets_ellipsed(self) -> None:
|
|
297
|
+
label = _build_epic_label("standalone", "Standalone Stories", 2, 7, jira_key="epic-standalone")
|
|
298
|
+
plain = label.plain
|
|
299
|
+
assert "\u2026" in plain, f"Long ID should be ellipsed, got: {plain}"
|
|
300
|
+
# Should not exceed 11 chars for the ID portion
|
|
301
|
+
id_part = plain.split(" ")[0]
|
|
302
|
+
assert len(id_part) <= 11
|
|
303
|
+
|
|
304
|
+
def test_includes_progress(self) -> None:
|
|
305
|
+
label = _build_epic_label("103", "BikeRack TUI", 4, 6)
|
|
306
|
+
assert "4/6 pts" in label.plain
|
|
307
|
+
|
|
308
|
+
def test_includes_title(self) -> None:
|
|
309
|
+
label = _build_epic_label("103", "BikeRack TUI", 4, 6)
|
|
310
|
+
assert "BikeRack TUI" in label.plain
|
|
311
|
+
|
|
312
|
+
def test_zero_points(self) -> None:
|
|
313
|
+
label = _build_epic_label("100", "Empty", 0, 0)
|
|
314
|
+
assert "0 pts" in label.plain
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class TestStoryLabel:
|
|
318
|
+
"""Story label builder produces correct Rich Text."""
|
|
319
|
+
|
|
320
|
+
def test_includes_jira_key(self) -> None:
|
|
321
|
+
story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
|
|
322
|
+
label = _build_story_label(story, "")
|
|
323
|
+
assert "MSSCI-14952" in label.plain
|
|
324
|
+
|
|
325
|
+
def test_includes_points(self) -> None:
|
|
326
|
+
story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
|
|
327
|
+
label = _build_story_label(story, "")
|
|
328
|
+
assert "2" in label.plain
|
|
329
|
+
|
|
330
|
+
def test_includes_title(self) -> None:
|
|
331
|
+
story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
|
|
332
|
+
label = _build_story_label(story, "")
|
|
333
|
+
assert "Scaffold" in label.plain
|
|
334
|
+
|
|
335
|
+
def test_null_jira_key_shows_dash(self) -> None:
|
|
336
|
+
story = {"id": "103-1", "title": "Test", "points": 1, "status": "backlog", "jiraKey": None}
|
|
337
|
+
label = _build_story_label(story, "")
|
|
338
|
+
assert "\u2014" in label.plain
|
|
339
|
+
|
|
340
|
+
def test_current_story_bolded(self) -> None:
|
|
341
|
+
story = {"id": "103-6", "title": "Current", "points": 2, "status": "in-progress", "jiraKey": "X"}
|
|
342
|
+
label = _build_story_label(story, "103-6")
|
|
343
|
+
has_bold = any("bold" in str(span.style) for span in label._spans)
|
|
344
|
+
assert has_bold, "Current story should have bold styling"
|
|
345
|
+
|
|
346
|
+
def test_done_story_is_dim(self) -> None:
|
|
347
|
+
story = {"id": "103-1", "title": "Done one", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
|
|
348
|
+
label = _build_story_label(story, "")
|
|
349
|
+
# Overall dim styling applied to done stories
|
|
350
|
+
has_dim = any("dim" in str(span.style) for span in label._spans)
|
|
351
|
+
assert has_dim, "Done story should have dim styling"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class TestFormatAssignee:
|
|
355
|
+
"""Email to display name formatting."""
|
|
356
|
+
|
|
357
|
+
def test_standard_email(self) -> None:
|
|
358
|
+
assert _format_assignee("keith.avery@1898andco.io") == "K. Avery"
|
|
359
|
+
|
|
360
|
+
def test_underscore_email(self) -> None:
|
|
361
|
+
assert _format_assignee("john_doe@example.com") == "J. Doe"
|
|
362
|
+
|
|
363
|
+
def test_none_returns_empty(self) -> None:
|
|
364
|
+
assert _format_assignee(None) == ""
|
|
365
|
+
|
|
366
|
+
def test_empty_string_returns_empty(self) -> None:
|
|
367
|
+
assert _format_assignee("") == ""
|
|
368
|
+
|
|
369
|
+
def test_single_part_local(self) -> None:
|
|
370
|
+
result = _format_assignee("admin@example.com")
|
|
371
|
+
assert result == "Admin"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestStoryLabelOwner:
|
|
375
|
+
"""Owner shown for in-progress stories, hidden for done/backlog."""
|
|
376
|
+
|
|
377
|
+
def test_in_progress_shows_owner(self) -> None:
|
|
378
|
+
story = {
|
|
379
|
+
"id": "110-2", "title": "Drill", "points": 5,
|
|
380
|
+
"status": "in-progress", "jiraKey": "MSSCI-15186",
|
|
381
|
+
"assignee": "keith.avery@1898andco.io",
|
|
382
|
+
}
|
|
383
|
+
label = _build_story_label(story, "")
|
|
384
|
+
assert "K. Avery" in label.plain
|
|
385
|
+
|
|
386
|
+
def test_done_hides_owner(self) -> None:
|
|
387
|
+
story = {
|
|
388
|
+
"id": "110-1", "title": "Done", "points": 3,
|
|
389
|
+
"status": "done", "jiraKey": "MSSCI-15185",
|
|
390
|
+
"assignee": "keith.avery@1898andco.io",
|
|
391
|
+
}
|
|
392
|
+
label = _build_story_label(story, "")
|
|
393
|
+
assert "K. Avery" not in label.plain
|
|
394
|
+
|
|
395
|
+
def test_backlog_hides_owner(self) -> None:
|
|
396
|
+
story = {
|
|
397
|
+
"id": "110-3", "title": "Backlog", "points": 3,
|
|
398
|
+
"status": "backlog", "jiraKey": "MSSCI-15187",
|
|
399
|
+
"assignee": "keith.avery@1898andco.io",
|
|
400
|
+
}
|
|
401
|
+
label = _build_story_label(story, "")
|
|
402
|
+
assert "K. Avery" not in label.plain
|
|
403
|
+
|
|
404
|
+
def test_in_progress_no_assignee(self) -> None:
|
|
405
|
+
story = {
|
|
406
|
+
"id": "110-2", "title": "Drill", "points": 5,
|
|
407
|
+
"status": "in-progress", "jiraKey": "MSSCI-15186",
|
|
408
|
+
}
|
|
409
|
+
label = _build_story_label(story, "")
|
|
410
|
+
assert "[" not in label.plain or "[]" not in label.plain
|
|
411
|
+
|
|
412
|
+
def test_canceled_story_is_dim(self) -> None:
|
|
413
|
+
story = {
|
|
414
|
+
"id": "110-4", "title": "Canceled one", "points": 2,
|
|
415
|
+
"status": "canceled", "jiraKey": "MSSCI-15999",
|
|
416
|
+
}
|
|
417
|
+
label = _build_story_label(story, "")
|
|
418
|
+
has_dim = any("dim" in str(span.style) for span in label._spans)
|
|
419
|
+
assert has_dim, "Canceled story should have dim styling"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class TestShouldExpand:
|
|
423
|
+
"""Default expand logic for epics."""
|
|
424
|
+
|
|
425
|
+
def test_expands_with_incomplete_work(self) -> None:
|
|
426
|
+
epic = {"stories": [
|
|
427
|
+
{"points": 2, "status": "done"},
|
|
428
|
+
{"points": 3, "status": "in-progress"},
|
|
429
|
+
]}
|
|
430
|
+
assert _should_expand(epic) is True
|
|
431
|
+
|
|
432
|
+
def test_collapses_when_all_done(self) -> None:
|
|
433
|
+
epic = {"stories": [
|
|
434
|
+
{"points": 2, "status": "done"},
|
|
435
|
+
{"points": 3, "status": "done"},
|
|
436
|
+
]}
|
|
437
|
+
assert _should_expand(epic) is False
|
|
438
|
+
|
|
439
|
+
def test_expands_when_backlog_remains(self) -> None:
|
|
440
|
+
epic = {"stories": [
|
|
441
|
+
{"points": 2, "status": "done"},
|
|
442
|
+
{"points": 3, "status": "backlog"},
|
|
443
|
+
]}
|
|
444
|
+
assert _should_expand(epic) is True
|
|
445
|
+
|
|
446
|
+
def test_expands_empty_epic(self) -> None:
|
|
447
|
+
epic = {"stories": []}
|
|
448
|
+
assert _should_expand(epic) is False
|
|
294
449
|
|
|
295
|
-
def
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
assert any("jira" in name.lower() for name in column_names), f"Expected 'Jira' column, got: {column_names}"
|
|
450
|
+
def test_collapses_canceled_epic(self) -> None:
|
|
451
|
+
epic = {"status": "canceled", "stories": [
|
|
452
|
+
{"points": 2, "status": "backlog"},
|
|
453
|
+
]}
|
|
454
|
+
assert _should_expand(epic) is False
|
|
301
455
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
assert
|
|
456
|
+
def test_collapses_when_all_done_or_canceled(self) -> None:
|
|
457
|
+
epic = {"stories": [
|
|
458
|
+
{"points": 2, "status": "done"},
|
|
459
|
+
{"points": 3, "status": "canceled"},
|
|
460
|
+
]}
|
|
461
|
+
assert _should_expand(epic) is False
|
|
308
462
|
|
|
309
|
-
def
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
assert
|
|
463
|
+
def test_expands_when_mix_of_canceled_and_backlog(self) -> None:
|
|
464
|
+
epic = {"stories": [
|
|
465
|
+
{"points": 2, "status": "canceled"},
|
|
466
|
+
{"points": 3, "status": "backlog"},
|
|
467
|
+
]}
|
|
468
|
+
assert _should_expand(epic) is True
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class TestIsTerminal:
|
|
472
|
+
"""Terminal status detection."""
|
|
473
|
+
|
|
474
|
+
def test_done_is_terminal(self) -> None:
|
|
475
|
+
assert _is_terminal("done") is True
|
|
476
|
+
|
|
477
|
+
def test_canceled_is_terminal(self) -> None:
|
|
478
|
+
assert _is_terminal("canceled") is True
|
|
479
|
+
|
|
480
|
+
def test_cancelled_british_is_terminal(self) -> None:
|
|
481
|
+
assert _is_terminal("cancelled") is True
|
|
482
|
+
|
|
483
|
+
def test_in_progress_is_not_terminal(self) -> None:
|
|
484
|
+
assert _is_terminal("in-progress") is False
|
|
485
|
+
|
|
486
|
+
def test_backlog_is_not_terminal(self) -> None:
|
|
487
|
+
assert _is_terminal("backlog") is False
|
|
315
488
|
|
|
316
489
|
|
|
317
490
|
# ---------------------------------------------------------------------------
|
|
@@ -320,38 +493,25 @@ class TestSprintPanelRendering:
|
|
|
320
493
|
|
|
321
494
|
|
|
322
495
|
class TestSprintPanelMetrics:
|
|
323
|
-
"""AC4:
|
|
324
|
-
|
|
325
|
-
def
|
|
326
|
-
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
""
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
assert "
|
|
342
|
-
|
|
343
|
-
def test_output_contains_remaining_count(self, panel: SprintPanel) -> None:
|
|
344
|
-
"""Output should display remaining points count."""
|
|
345
|
-
result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
|
|
346
|
-
rendered_str = _render_to_string(result)
|
|
347
|
-
assert "128" in rendered_str, "Remaining count (128) should appear in output"
|
|
348
|
-
|
|
349
|
-
def test_metrics_update_with_new_data(self, panel: SprintPanel) -> None:
|
|
350
|
-
"""Metrics should reflect updated payload values."""
|
|
351
|
-
result = panel.render_panel(SAMPLE_UPDATE_PAYLOAD)
|
|
352
|
-
rendered_str = _render_to_string(result)
|
|
353
|
-
assert "9" in rendered_str, "Updated velocity (9) should appear in output"
|
|
354
|
-
assert "73" in rendered_str, "Updated done count (73) should appear in output"
|
|
496
|
+
"""AC4: Metrics are included in sprint header text."""
|
|
497
|
+
|
|
498
|
+
def test_header_format(self) -> None:
|
|
499
|
+
"""Header text builder includes key metrics."""
|
|
500
|
+
# Test by checking the header text that _rebuild_tree would produce
|
|
501
|
+
sprint = SAMPLE_INIT_PAYLOAD["sprint"]
|
|
502
|
+
metrics = SAMPLE_INIT_PAYLOAD["metrics"]
|
|
503
|
+
header = Text.from_markup(
|
|
504
|
+
f"Sprint {sprint.get('number', '')} "
|
|
505
|
+
f"[green]Done: {sprint.get('done', 0)}[/green] | "
|
|
506
|
+
f"Remaining: {sprint.get('remaining', 0)} | "
|
|
507
|
+
f"In Progress: {sprint.get('inProgress', 0)} | "
|
|
508
|
+
f"Velocity: {metrics.get('velocity', 0)}"
|
|
509
|
+
)
|
|
510
|
+
plain = header.plain
|
|
511
|
+
assert "2606" in plain
|
|
512
|
+
assert "71" in plain
|
|
513
|
+
assert "128" in plain
|
|
514
|
+
assert "8" in plain
|
|
355
515
|
|
|
356
516
|
|
|
357
517
|
# ---------------------------------------------------------------------------
|
|
@@ -373,8 +533,6 @@ class TestDefaultPanel:
|
|
|
373
533
|
async with app.run_test():
|
|
374
534
|
panels = app.query(SprintPanel)
|
|
375
535
|
assert len(panels) > 0, "SprintPanel should be mounted as default panel"
|
|
376
|
-
panel = panels.first()
|
|
377
|
-
assert panel.id == "sprint-panel"
|
|
378
536
|
|
|
379
537
|
|
|
380
538
|
# ---------------------------------------------------------------------------
|
|
@@ -385,55 +543,30 @@ class TestDefaultPanel:
|
|
|
385
543
|
class TestSprintPanelRealtime:
|
|
386
544
|
"""AC6: Updates in real-time when data changes on channel."""
|
|
387
545
|
|
|
388
|
-
def
|
|
389
|
-
"""
|
|
390
|
-
panel._mounted = True
|
|
391
|
-
with patch.object(panel, "render_panel", return_value="rendered") as mock_render:
|
|
392
|
-
with patch.object(panel, "update"):
|
|
393
|
-
panel.handle_message(SAMPLE_INIT_PAYLOAD)
|
|
394
|
-
mock_render.assert_called_once_with(SAMPLE_INIT_PAYLOAD)
|
|
395
|
-
|
|
396
|
-
def test_handle_message_updates_widget(self, panel: SprintPanel) -> None:
|
|
397
|
-
"""handle_message should call self.update() with rendered output."""
|
|
398
|
-
panel._mounted = True
|
|
399
|
-
with patch.object(panel, "render_panel", return_value="rendered"):
|
|
400
|
-
with patch.object(panel, "update") as mock_update:
|
|
401
|
-
panel.handle_message(SAMPLE_INIT_PAYLOAD)
|
|
402
|
-
mock_update.assert_called_once_with("rendered")
|
|
403
|
-
|
|
404
|
-
def test_sequential_updates_re_render(self, panel: SprintPanel) -> None:
|
|
405
|
-
"""Multiple messages should each trigger a re-render."""
|
|
406
|
-
panel._mounted = True
|
|
407
|
-
with patch.object(panel, "render_panel", return_value="rendered"):
|
|
408
|
-
with patch.object(panel, "update") as mock_update:
|
|
409
|
-
panel.handle_message(SAMPLE_INIT_PAYLOAD)
|
|
410
|
-
panel.handle_message(SAMPLE_UPDATE_PAYLOAD)
|
|
411
|
-
assert mock_update.call_count == 2
|
|
412
|
-
|
|
413
|
-
def test_ignores_none_messages(self, panel: SprintPanel) -> None:
|
|
414
|
-
"""None messages should be silently ignored."""
|
|
546
|
+
def test_handle_message_posts_data_received(self, panel: SprintPanel) -> None:
|
|
547
|
+
"""_handle_ws_message should post DataReceived message."""
|
|
415
548
|
panel._mounted = True
|
|
416
|
-
with patch.object(panel, "
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
549
|
+
with patch.object(panel, "post_message") as mock_post:
|
|
550
|
+
panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
|
|
551
|
+
assert mock_post.call_count == 1
|
|
552
|
+
event = mock_post.call_args[0][0]
|
|
553
|
+
assert isinstance(event, SprintPanel.DataReceived)
|
|
554
|
+
assert event.payload == SAMPLE_INIT_PAYLOAD
|
|
555
|
+
|
|
556
|
+
def test_sequential_updates(self, panel: SprintPanel) -> None:
|
|
557
|
+
"""Multiple messages should each trigger a post_message."""
|
|
423
558
|
panel._mounted = True
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
mock_render.assert_not_called()
|
|
559
|
+
with patch.object(panel, "post_message") as mock_post:
|
|
560
|
+
panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
|
|
561
|
+
panel._handle_ws_message(SAMPLE_UPDATE_PAYLOAD)
|
|
562
|
+
assert mock_post.call_count == 2
|
|
429
563
|
|
|
430
564
|
def test_stores_last_payload(self, panel: SprintPanel) -> None:
|
|
431
|
-
"""
|
|
565
|
+
"""_handle_ws_message should store the last payload."""
|
|
432
566
|
panel._mounted = True
|
|
433
|
-
with patch.object(panel, "
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
assert panel._last_payload == SAMPLE_INIT_PAYLOAD
|
|
567
|
+
with patch.object(panel, "post_message"):
|
|
568
|
+
panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
|
|
569
|
+
assert panel._last_payload == SAMPLE_INIT_PAYLOAD
|
|
437
570
|
|
|
438
571
|
|
|
439
572
|
# ---------------------------------------------------------------------------
|
|
@@ -444,46 +577,27 @@ class TestSprintPanelRealtime:
|
|
|
444
577
|
class TestSprintPanelEdgeCases:
|
|
445
578
|
"""Edge cases and robustness tests."""
|
|
446
579
|
|
|
447
|
-
def test_handles_missing_metrics(self
|
|
448
|
-
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
assert
|
|
468
|
-
|
|
469
|
-
def test_handles_story_with_null_jira_key(self, panel: SprintPanel) -> None:
|
|
470
|
-
"""render_panel should handle story with null jiraKey."""
|
|
471
|
-
payload = {
|
|
472
|
-
**SAMPLE_INIT_PAYLOAD,
|
|
473
|
-
"epics": [
|
|
474
|
-
{
|
|
475
|
-
"id": "103",
|
|
476
|
-
"title": "Test",
|
|
477
|
-
"jiraKey": None,
|
|
478
|
-
"stories": [
|
|
479
|
-
{"id": "103-99", "title": "No Jira", "points": 1, "status": "backlog", "jiraKey": None},
|
|
480
|
-
],
|
|
481
|
-
},
|
|
482
|
-
],
|
|
483
|
-
}
|
|
484
|
-
# Should not raise
|
|
485
|
-
result = panel.render_panel(payload)
|
|
486
|
-
assert result is not None
|
|
580
|
+
def test_handles_missing_metrics(self) -> None:
|
|
581
|
+
"""Label builder handles payload without metrics key."""
|
|
582
|
+
sprint = {"number": "2606", "done": 0, "remaining": 0, "inProgress": 0}
|
|
583
|
+
header = Text.from_markup(
|
|
584
|
+
f"Sprint {sprint.get('number', '')} "
|
|
585
|
+
f"[green]Done: {sprint.get('done', 0)}[/green] | "
|
|
586
|
+
f"Remaining: {sprint.get('remaining', 0)} | "
|
|
587
|
+
f"Velocity: 0"
|
|
588
|
+
)
|
|
589
|
+
assert header.plain is not None
|
|
590
|
+
|
|
591
|
+
def test_status_badge_empty_string(self) -> None:
|
|
592
|
+
"""Status badge handles empty string."""
|
|
593
|
+
badge = _status_badge("")
|
|
594
|
+
assert badge is not None
|
|
595
|
+
|
|
596
|
+
def test_story_label_missing_fields(self) -> None:
|
|
597
|
+
"""Story label handles minimal story dict."""
|
|
598
|
+
story: dict[str, Any] = {"id": "X", "title": "", "points": 0, "status": "", "jiraKey": None}
|
|
599
|
+
label = _build_story_label(story, "")
|
|
600
|
+
assert label is not None
|
|
487
601
|
|
|
488
602
|
|
|
489
603
|
# ---------------------------------------------------------------------------
|
|
@@ -491,41 +605,6 @@ class TestSprintPanelEdgeCases:
|
|
|
491
605
|
# ---------------------------------------------------------------------------
|
|
492
606
|
|
|
493
607
|
|
|
494
|
-
def _extract_table(result: Any) -> Table:
|
|
495
|
-
"""Extract a Rich Table from render_panel output.
|
|
496
|
-
|
|
497
|
-
Handles both direct Table returns and Group/container returns.
|
|
498
|
-
Raises AssertionError if no Table found.
|
|
499
|
-
"""
|
|
500
|
-
if isinstance(result, Table):
|
|
501
|
-
return result
|
|
502
|
-
|
|
503
|
-
# Check renderables in Group
|
|
504
|
-
renderables = getattr(result, "renderables", [])
|
|
505
|
-
for r in renderables:
|
|
506
|
-
if isinstance(r, Table):
|
|
507
|
-
return r
|
|
508
|
-
|
|
509
|
-
raise AssertionError(
|
|
510
|
-
f"Expected Rich Table in output, got {type(result).__name__}: {result!r}"
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
|
|
514
608
|
async def _noop_coroutine() -> None:
|
|
515
609
|
"""No-op coroutine for mocking async client.connect()."""
|
|
516
610
|
pass
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
def _render_to_string(result: Any) -> str:
|
|
520
|
-
"""Render a Rich renderable to plain string for content assertions.
|
|
521
|
-
|
|
522
|
-
Uses Rich Console with no color to get plain text output.
|
|
523
|
-
"""
|
|
524
|
-
from io import StringIO
|
|
525
|
-
|
|
526
|
-
from rich.console import Console
|
|
527
|
-
|
|
528
|
-
buffer = StringIO()
|
|
529
|
-
console = Console(file=buffer, no_color=True, width=120)
|
|
530
|
-
console.print(result)
|
|
531
|
-
return buffer.getvalue()
|