@pennyfarthing/core 11.1.0 → 11.2.0
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 +8 -8
- package/package.json +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- 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/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/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- 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/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.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__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- 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__/portrait.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__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- package/pennyfarthing_scripts/consultation/__init__.py +1 -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 +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- 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__/hooks_installer.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/__pycache__/worktree.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- 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__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -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_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- 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/cli.py +1099 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""Tests for Story 86-16: Port dialogue manager from TS/bash to Python.
|
|
2
|
+
|
|
3
|
+
RED state tests for the Python dialogue file persistence layer.
|
|
4
|
+
Ported from packages/core/src/consultation/dialogue-manager.test.ts.
|
|
5
|
+
|
|
6
|
+
Acceptance Criteria:
|
|
7
|
+
AC1: pennyfarthing_scripts/consultation/ package with dialogue_manager.py
|
|
8
|
+
implementing: create, format, parse, summary, append, update_outcome,
|
|
9
|
+
refresh_summary, archive
|
|
10
|
+
AC2: pf consultation Click CLI group with subcommands: init, append,
|
|
11
|
+
outcome, summarize, archive
|
|
12
|
+
AC3: All 6 ACs from 86-3 still pass — same file format, behavior, output
|
|
13
|
+
AC4: Tests ported to pytest in this file
|
|
14
|
+
|
|
15
|
+
Run with: pytest pennyfarthing_scripts/tests/test_dialogue_manager.py -v
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
from click.testing import CliRunner
|
|
24
|
+
|
|
25
|
+
from pennyfarthing_scripts.consultation.dialogue_manager import (
|
|
26
|
+
DialogueExchange,
|
|
27
|
+
DialogueHeader,
|
|
28
|
+
DialogueResult,
|
|
29
|
+
append_exchange_to_file,
|
|
30
|
+
archive_dialogue,
|
|
31
|
+
create_dialogue_content,
|
|
32
|
+
format_exchange,
|
|
33
|
+
generate_summary,
|
|
34
|
+
parse_dialogue_exchanges,
|
|
35
|
+
refresh_summary,
|
|
36
|
+
update_outcome_in_file,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Fixtures
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
VALID_HEADER = DialogueHeader(
|
|
44
|
+
story_id="86-3",
|
|
45
|
+
workflow="tdd-tandem",
|
|
46
|
+
leader="dev",
|
|
47
|
+
leader_character="Jack Torrance",
|
|
48
|
+
partner="architect",
|
|
49
|
+
partner_character="Andy Dufresne",
|
|
50
|
+
started_at="2026-02-16T10:00:00Z",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
VALID_EXCHANGE = DialogueExchange(
|
|
54
|
+
number=1,
|
|
55
|
+
timestamp="10:05",
|
|
56
|
+
leader="dev",
|
|
57
|
+
partner="architect",
|
|
58
|
+
question="Should we use a class or functional approach for the dialogue manager?",
|
|
59
|
+
recommendation="Use pure functions — they are easier to test and align with the existing consultation-protocol.ts pattern",
|
|
60
|
+
confidence="high",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
SECOND_EXCHANGE = DialogueExchange(
|
|
64
|
+
number=2,
|
|
65
|
+
timestamp="10:20",
|
|
66
|
+
leader="dev",
|
|
67
|
+
partner="architect",
|
|
68
|
+
question="Should the shell wrapper call Node.js or use pure bash?",
|
|
69
|
+
recommendation="Pure bash for the shell wrapper — keeps it dependency-free and consistent with other core scripts",
|
|
70
|
+
confidence="medium",
|
|
71
|
+
outcome="applied",
|
|
72
|
+
outcome_note="Implemented with sed/awk",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# AC1 + AC3-AC1: Dialogue file creation on first consultation
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestCreateDialogueContent:
|
|
82
|
+
"""AC1: create_dialogue_content — initial file header."""
|
|
83
|
+
|
|
84
|
+
def test_includes_story_id_in_title(self):
|
|
85
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
86
|
+
assert "# Tandem Dialogue: 86-3" in content
|
|
87
|
+
|
|
88
|
+
def test_includes_workflow(self):
|
|
89
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
90
|
+
assert "**Workflow:** tdd-tandem" in content
|
|
91
|
+
|
|
92
|
+
def test_includes_leader_agent(self):
|
|
93
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
94
|
+
assert "**Leader:** dev" in content
|
|
95
|
+
|
|
96
|
+
def test_includes_partner_agent(self):
|
|
97
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
98
|
+
assert "**Partner:** architect" in content
|
|
99
|
+
|
|
100
|
+
def test_includes_start_timestamp(self):
|
|
101
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
102
|
+
assert "**Started:** 2026-02-16T10:00:00Z" in content
|
|
103
|
+
|
|
104
|
+
def test_includes_character_names_when_provided(self):
|
|
105
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
106
|
+
assert "Jack Torrance" in content
|
|
107
|
+
assert "Andy Dufresne" in content
|
|
108
|
+
|
|
109
|
+
def test_handles_missing_character_names(self):
|
|
110
|
+
header = DialogueHeader(
|
|
111
|
+
story_id="86-3",
|
|
112
|
+
workflow="tdd-tandem",
|
|
113
|
+
leader="dev",
|
|
114
|
+
partner="architect",
|
|
115
|
+
started_at="2026-02-16T10:00:00Z",
|
|
116
|
+
)
|
|
117
|
+
content = create_dialogue_content(header)
|
|
118
|
+
assert "**Leader:** dev" in content
|
|
119
|
+
assert "**Partner:** architect" in content
|
|
120
|
+
|
|
121
|
+
def test_includes_empty_summary_section(self):
|
|
122
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
123
|
+
assert "## Summary" in content
|
|
124
|
+
assert "**Total exchanges:** 0" in content
|
|
125
|
+
|
|
126
|
+
def test_includes_horizontal_rule_separator(self):
|
|
127
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
128
|
+
assert "---" in content
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# AC1 + AC3-AC2: Exchange format and appending
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestFormatExchange:
|
|
137
|
+
"""AC1: format_exchange — single exchange as markdown."""
|
|
138
|
+
|
|
139
|
+
def test_includes_exchange_number(self):
|
|
140
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
141
|
+
assert "## Exchange 1" in formatted
|
|
142
|
+
|
|
143
|
+
def test_includes_timestamp_and_direction(self):
|
|
144
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
145
|
+
assert "**[10:05] dev \u2192 architect**" in formatted
|
|
146
|
+
|
|
147
|
+
def test_includes_question_as_blockquote(self):
|
|
148
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
149
|
+
assert "> Should we use a class or functional approach" in formatted
|
|
150
|
+
|
|
151
|
+
def test_includes_partner_response_header(self):
|
|
152
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
153
|
+
assert "**[10:05] architect:**" in formatted
|
|
154
|
+
|
|
155
|
+
def test_includes_recommendation_text(self):
|
|
156
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
157
|
+
assert "Use pure functions" in formatted
|
|
158
|
+
|
|
159
|
+
def test_includes_confidence_level(self):
|
|
160
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
161
|
+
assert "**Confidence:** high" in formatted
|
|
162
|
+
|
|
163
|
+
def test_shows_pending_when_no_outcome(self):
|
|
164
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
165
|
+
assert "**Outcome:** _pending_" in formatted
|
|
166
|
+
|
|
167
|
+
def test_includes_outcome_when_present(self):
|
|
168
|
+
formatted = format_exchange(SECOND_EXCHANGE)
|
|
169
|
+
assert "**Outcome:** applied" in formatted
|
|
170
|
+
|
|
171
|
+
def test_includes_outcome_note(self):
|
|
172
|
+
formatted = format_exchange(SECOND_EXCHANGE)
|
|
173
|
+
assert "Implemented with sed/awk" in formatted
|
|
174
|
+
|
|
175
|
+
def test_includes_separator(self):
|
|
176
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
177
|
+
assert "---" in formatted
|
|
178
|
+
|
|
179
|
+
def test_outcome_without_note(self):
|
|
180
|
+
exchange = DialogueExchange(
|
|
181
|
+
number=3,
|
|
182
|
+
timestamp="11:00",
|
|
183
|
+
leader="dev",
|
|
184
|
+
partner="architect",
|
|
185
|
+
question="Q?",
|
|
186
|
+
recommendation="R.",
|
|
187
|
+
confidence="low",
|
|
188
|
+
outcome="rejected",
|
|
189
|
+
)
|
|
190
|
+
formatted = format_exchange(exchange)
|
|
191
|
+
assert "**Outcome:** rejected" in formatted
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# =============================================================================
|
|
195
|
+
# AC1 + AC3-AC2: File append operations
|
|
196
|
+
# =============================================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestAppendExchangeToFile:
|
|
200
|
+
"""AC1: append_exchange_to_file — file creation and exchange appending."""
|
|
201
|
+
|
|
202
|
+
def test_creates_file_on_first_append(self, tmp_path: Path):
|
|
203
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
204
|
+
result = append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
205
|
+
|
|
206
|
+
assert result.success is True
|
|
207
|
+
assert dialogue_path.exists()
|
|
208
|
+
|
|
209
|
+
def test_file_contains_header(self, tmp_path: Path):
|
|
210
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
211
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
212
|
+
|
|
213
|
+
content = dialogue_path.read_text()
|
|
214
|
+
assert "# Tandem Dialogue: 86-3" in content
|
|
215
|
+
|
|
216
|
+
def test_file_contains_first_exchange(self, tmp_path: Path):
|
|
217
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
218
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
219
|
+
|
|
220
|
+
content = dialogue_path.read_text()
|
|
221
|
+
assert "## Exchange 1" in content
|
|
222
|
+
|
|
223
|
+
def test_requires_header_for_new_file(self, tmp_path: Path):
|
|
224
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
225
|
+
result = append_exchange_to_file(dialogue_path, VALID_EXCHANGE)
|
|
226
|
+
|
|
227
|
+
assert result.success is False
|
|
228
|
+
assert result.error is not None
|
|
229
|
+
assert "Header required" in result.error or "header" in result.error.lower()
|
|
230
|
+
|
|
231
|
+
def test_appends_multiple_exchanges(self, tmp_path: Path):
|
|
232
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
233
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
234
|
+
append_exchange_to_file(dialogue_path, SECOND_EXCHANGE)
|
|
235
|
+
|
|
236
|
+
content = dialogue_path.read_text()
|
|
237
|
+
assert "## Exchange 1" in content
|
|
238
|
+
assert "## Exchange 2" in content
|
|
239
|
+
|
|
240
|
+
def test_exchanges_appear_before_summary(self, tmp_path: Path):
|
|
241
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
242
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
243
|
+
|
|
244
|
+
content = dialogue_path.read_text()
|
|
245
|
+
exchange_idx = content.index("## Exchange 1")
|
|
246
|
+
summary_idx = content.index("## Summary")
|
|
247
|
+
assert exchange_idx < summary_idx
|
|
248
|
+
|
|
249
|
+
def test_creates_parent_directories(self, tmp_path: Path):
|
|
250
|
+
dialogue_path = tmp_path / "nested" / "dir" / "86-3-dialogue.md"
|
|
251
|
+
result = append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
252
|
+
|
|
253
|
+
assert result.success is True
|
|
254
|
+
assert dialogue_path.exists()
|
|
255
|
+
|
|
256
|
+
def test_returns_exchange_number_in_data(self, tmp_path: Path):
|
|
257
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
258
|
+
result = append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
259
|
+
|
|
260
|
+
assert result.success is True
|
|
261
|
+
assert result.data is not None
|
|
262
|
+
assert result.data.get("exchangeNumber") == 1
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# =============================================================================
|
|
266
|
+
# AC1 + AC3-AC3: Outcome tracking
|
|
267
|
+
# =============================================================================
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestUpdateOutcomeInFile:
|
|
271
|
+
"""AC1: update_outcome_in_file — outcome tracking."""
|
|
272
|
+
|
|
273
|
+
def test_updates_to_applied(self, tmp_path: Path):
|
|
274
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
275
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
276
|
+
|
|
277
|
+
result = update_outcome_in_file(dialogue_path, 1, "applied", "Used pure functions")
|
|
278
|
+
|
|
279
|
+
assert result.success is True
|
|
280
|
+
content = dialogue_path.read_text()
|
|
281
|
+
assert "**Outcome:** applied" in content
|
|
282
|
+
assert "Used pure functions" in content
|
|
283
|
+
|
|
284
|
+
def test_updates_to_deferred(self, tmp_path: Path):
|
|
285
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
286
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
287
|
+
|
|
288
|
+
update_outcome_in_file(dialogue_path, 1, "deferred", "Revisit in next phase")
|
|
289
|
+
|
|
290
|
+
content = dialogue_path.read_text()
|
|
291
|
+
assert "**Outcome:** deferred" in content
|
|
292
|
+
|
|
293
|
+
def test_updates_to_rejected(self, tmp_path: Path):
|
|
294
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
295
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
296
|
+
|
|
297
|
+
update_outcome_in_file(dialogue_path, 1, "rejected", "Went with class approach")
|
|
298
|
+
|
|
299
|
+
content = dialogue_path.read_text()
|
|
300
|
+
assert "**Outcome:** rejected" in content
|
|
301
|
+
|
|
302
|
+
def test_updates_without_note(self, tmp_path: Path):
|
|
303
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
304
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
305
|
+
|
|
306
|
+
result = update_outcome_in_file(dialogue_path, 1, "applied")
|
|
307
|
+
|
|
308
|
+
assert result.success is True
|
|
309
|
+
content = dialogue_path.read_text()
|
|
310
|
+
assert "**Outcome:** applied" in content
|
|
311
|
+
|
|
312
|
+
def test_fails_for_nonexistent_exchange(self, tmp_path: Path):
|
|
313
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
314
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
315
|
+
|
|
316
|
+
result = update_outcome_in_file(dialogue_path, 99, "applied")
|
|
317
|
+
|
|
318
|
+
assert result.success is False
|
|
319
|
+
assert result.error is not None
|
|
320
|
+
assert "99" in result.error
|
|
321
|
+
|
|
322
|
+
def test_fails_for_nonexistent_file(self, tmp_path: Path):
|
|
323
|
+
dialogue_path = tmp_path / "nonexistent-dialogue.md"
|
|
324
|
+
|
|
325
|
+
result = update_outcome_in_file(dialogue_path, 1, "applied")
|
|
326
|
+
|
|
327
|
+
assert result.success is False
|
|
328
|
+
|
|
329
|
+
def test_updates_correct_exchange_when_multiple_exist(self, tmp_path: Path):
|
|
330
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
331
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
332
|
+
append_exchange_to_file(dialogue_path, SECOND_EXCHANGE)
|
|
333
|
+
|
|
334
|
+
update_outcome_in_file(dialogue_path, 1, "rejected", "Changed mind")
|
|
335
|
+
|
|
336
|
+
content = dialogue_path.read_text()
|
|
337
|
+
# Exchange 1 should be rejected
|
|
338
|
+
# Exchange 2 should still be applied (from SECOND_EXCHANGE)
|
|
339
|
+
lines = content.split("\n")
|
|
340
|
+
in_exchange_1 = False
|
|
341
|
+
in_exchange_2 = False
|
|
342
|
+
exchange_1_outcome = ""
|
|
343
|
+
exchange_2_outcome = ""
|
|
344
|
+
for line in lines:
|
|
345
|
+
if "## Exchange 1" in line:
|
|
346
|
+
in_exchange_1 = True
|
|
347
|
+
in_exchange_2 = False
|
|
348
|
+
elif "## Exchange 2" in line:
|
|
349
|
+
in_exchange_1 = False
|
|
350
|
+
in_exchange_2 = True
|
|
351
|
+
elif "**Outcome:**" in line:
|
|
352
|
+
if in_exchange_1:
|
|
353
|
+
exchange_1_outcome = line
|
|
354
|
+
in_exchange_1 = False
|
|
355
|
+
elif in_exchange_2:
|
|
356
|
+
exchange_2_outcome = line
|
|
357
|
+
in_exchange_2 = False
|
|
358
|
+
|
|
359
|
+
assert "rejected" in exchange_1_outcome
|
|
360
|
+
assert "applied" in exchange_2_outcome
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# =============================================================================
|
|
364
|
+
# AC1 + AC3-AC4: Summary generation
|
|
365
|
+
# =============================================================================
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class TestGenerateSummary:
|
|
369
|
+
"""AC1: generate_summary — auto-generated summary section."""
|
|
370
|
+
|
|
371
|
+
def test_includes_total_exchange_count(self):
|
|
372
|
+
exchanges = [VALID_EXCHANGE, SECOND_EXCHANGE]
|
|
373
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
374
|
+
|
|
375
|
+
assert "**Total exchanges:** 2" in summary
|
|
376
|
+
|
|
377
|
+
def test_includes_applied_decisions(self):
|
|
378
|
+
exchanges = [
|
|
379
|
+
DialogueExchange(
|
|
380
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
381
|
+
question="Q1", recommendation="R1", confidence="high",
|
|
382
|
+
outcome="applied", outcome_note="Went with pure functions",
|
|
383
|
+
),
|
|
384
|
+
DialogueExchange(
|
|
385
|
+
number=2, timestamp="10:20", leader="dev", partner="architect",
|
|
386
|
+
question="Q2", recommendation="R2", confidence="medium",
|
|
387
|
+
outcome="applied", outcome_note="Implemented with sed/awk",
|
|
388
|
+
),
|
|
389
|
+
]
|
|
390
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
391
|
+
|
|
392
|
+
assert "Went with pure functions" in summary
|
|
393
|
+
assert "Implemented with sed/awk" in summary
|
|
394
|
+
|
|
395
|
+
def test_excludes_rejected_from_decisions(self):
|
|
396
|
+
exchanges = [
|
|
397
|
+
DialogueExchange(
|
|
398
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
399
|
+
question="Q1", recommendation="R1", confidence="high",
|
|
400
|
+
outcome="rejected", outcome_note="Did not adopt this",
|
|
401
|
+
),
|
|
402
|
+
DialogueExchange(
|
|
403
|
+
number=2, timestamp="10:20", leader="dev", partner="architect",
|
|
404
|
+
question="Q2", recommendation="R2", confidence="medium",
|
|
405
|
+
outcome="applied", outcome_note="Adopted this one",
|
|
406
|
+
),
|
|
407
|
+
]
|
|
408
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
409
|
+
|
|
410
|
+
assert "Did not adopt this" not in summary
|
|
411
|
+
assert "Adopted this one" in summary
|
|
412
|
+
|
|
413
|
+
def test_calculates_time_span(self):
|
|
414
|
+
exchanges = [
|
|
415
|
+
DialogueExchange(
|
|
416
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
417
|
+
question="Q1", recommendation="R1", confidence="high",
|
|
418
|
+
),
|
|
419
|
+
DialogueExchange(
|
|
420
|
+
number=2, timestamp="10:35", leader="dev", partner="architect",
|
|
421
|
+
question="Q2", recommendation="R2", confidence="medium",
|
|
422
|
+
),
|
|
423
|
+
]
|
|
424
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
425
|
+
|
|
426
|
+
assert "30m" in summary
|
|
427
|
+
|
|
428
|
+
def test_single_exchange_time(self):
|
|
429
|
+
exchanges = [VALID_EXCHANGE]
|
|
430
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
431
|
+
|
|
432
|
+
assert "**Time in tandem:**" in summary
|
|
433
|
+
|
|
434
|
+
def test_no_applied_shows_none(self):
|
|
435
|
+
exchanges = [
|
|
436
|
+
DialogueExchange(
|
|
437
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
438
|
+
question="Q1", recommendation="R1", confidence="high",
|
|
439
|
+
outcome="deferred",
|
|
440
|
+
),
|
|
441
|
+
]
|
|
442
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
443
|
+
|
|
444
|
+
assert "None" in summary
|
|
445
|
+
|
|
446
|
+
def test_includes_summary_marker(self):
|
|
447
|
+
exchanges = [VALID_EXCHANGE]
|
|
448
|
+
summary = generate_summary(exchanges, "2026-02-16T10:00:00Z")
|
|
449
|
+
|
|
450
|
+
assert "## Summary" in summary
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# =============================================================================
|
|
454
|
+
# AC1: refresh_summary (file operation)
|
|
455
|
+
# =============================================================================
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TestRefreshSummary:
|
|
459
|
+
"""AC1: refresh_summary — regenerate summary in existing file."""
|
|
460
|
+
|
|
461
|
+
def test_refreshes_summary_with_exchange_count(self, tmp_path: Path):
|
|
462
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
463
|
+
exchange_with_outcome = DialogueExchange(
|
|
464
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
465
|
+
question="Q?", recommendation="R.", confidence="high",
|
|
466
|
+
outcome="applied", outcome_note="Adopted functional approach",
|
|
467
|
+
)
|
|
468
|
+
append_exchange_to_file(dialogue_path, exchange_with_outcome, VALID_HEADER)
|
|
469
|
+
|
|
470
|
+
result = refresh_summary(dialogue_path)
|
|
471
|
+
|
|
472
|
+
assert result.success is True
|
|
473
|
+
content = dialogue_path.read_text()
|
|
474
|
+
assert "**Total exchanges:** 1" in content
|
|
475
|
+
|
|
476
|
+
def test_refreshed_summary_includes_applied_decisions(self, tmp_path: Path):
|
|
477
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
478
|
+
exchange_with_outcome = DialogueExchange(
|
|
479
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
480
|
+
question="Q?", recommendation="R.", confidence="high",
|
|
481
|
+
outcome="applied", outcome_note="Adopted functional approach",
|
|
482
|
+
)
|
|
483
|
+
append_exchange_to_file(dialogue_path, exchange_with_outcome, VALID_HEADER)
|
|
484
|
+
|
|
485
|
+
refresh_summary(dialogue_path)
|
|
486
|
+
|
|
487
|
+
content = dialogue_path.read_text()
|
|
488
|
+
assert "Adopted functional approach" in content
|
|
489
|
+
|
|
490
|
+
def test_fails_for_nonexistent_file(self, tmp_path: Path):
|
|
491
|
+
dialogue_path = tmp_path / "nonexistent-dialogue.md"
|
|
492
|
+
|
|
493
|
+
result = refresh_summary(dialogue_path)
|
|
494
|
+
|
|
495
|
+
assert result.success is False
|
|
496
|
+
|
|
497
|
+
def test_returns_total_in_data(self, tmp_path: Path):
|
|
498
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
499
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
500
|
+
append_exchange_to_file(dialogue_path, SECOND_EXCHANGE)
|
|
501
|
+
|
|
502
|
+
result = refresh_summary(dialogue_path)
|
|
503
|
+
|
|
504
|
+
assert result.success is True
|
|
505
|
+
assert result.data is not None
|
|
506
|
+
assert result.data.get("totalExchanges") == 2
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# =============================================================================
|
|
510
|
+
# AC1 + AC3-AC5: Dialogue archival
|
|
511
|
+
# =============================================================================
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class TestArchiveDialogue:
|
|
515
|
+
"""AC1: archive_dialogue — copy to archive directory."""
|
|
516
|
+
|
|
517
|
+
def test_copies_to_archive_with_jira_key(self, tmp_path: Path):
|
|
518
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
519
|
+
archive_dir = tmp_path / "archive"
|
|
520
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
521
|
+
|
|
522
|
+
result = archive_dialogue(dialogue_path, archive_dir, jira_key="MSSCI-15200")
|
|
523
|
+
|
|
524
|
+
assert result.success is True
|
|
525
|
+
assert (archive_dir / "MSSCI-15200-dialogue.md").exists()
|
|
526
|
+
|
|
527
|
+
def test_uses_story_id_when_no_jira_key(self, tmp_path: Path):
|
|
528
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
529
|
+
archive_dir = tmp_path / "archive"
|
|
530
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
531
|
+
|
|
532
|
+
result = archive_dialogue(dialogue_path, archive_dir, story_id="86-3")
|
|
533
|
+
|
|
534
|
+
assert result.success is True
|
|
535
|
+
assert (archive_dir / "86-3-dialogue.md").exists()
|
|
536
|
+
|
|
537
|
+
def test_preserves_content(self, tmp_path: Path):
|
|
538
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
539
|
+
archive_dir = tmp_path / "archive"
|
|
540
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
541
|
+
original = dialogue_path.read_text()
|
|
542
|
+
|
|
543
|
+
archive_dialogue(dialogue_path, archive_dir, jira_key="MSSCI-15200")
|
|
544
|
+
|
|
545
|
+
archived = (archive_dir / "MSSCI-15200-dialogue.md").read_text()
|
|
546
|
+
assert archived == original
|
|
547
|
+
|
|
548
|
+
def test_creates_archive_directory(self, tmp_path: Path):
|
|
549
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
550
|
+
archive_dir = tmp_path / "new-archive"
|
|
551
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
552
|
+
|
|
553
|
+
result = archive_dialogue(dialogue_path, archive_dir, jira_key="MSSCI-15200")
|
|
554
|
+
|
|
555
|
+
assert result.success is True
|
|
556
|
+
assert archive_dir.exists()
|
|
557
|
+
|
|
558
|
+
def test_fails_for_nonexistent_source(self, tmp_path: Path):
|
|
559
|
+
dialogue_path = tmp_path / "nonexistent-dialogue.md"
|
|
560
|
+
archive_dir = tmp_path / "archive"
|
|
561
|
+
|
|
562
|
+
result = archive_dialogue(dialogue_path, archive_dir, jira_key="MSSCI-15200")
|
|
563
|
+
|
|
564
|
+
assert result.success is False
|
|
565
|
+
assert result.error is not None
|
|
566
|
+
|
|
567
|
+
def test_uses_unknown_prefix_when_no_key_or_id(self, tmp_path: Path):
|
|
568
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
569
|
+
archive_dir = tmp_path / "archive"
|
|
570
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
571
|
+
|
|
572
|
+
result = archive_dialogue(dialogue_path, archive_dir)
|
|
573
|
+
|
|
574
|
+
assert result.success is True
|
|
575
|
+
assert (archive_dir / "unknown-dialogue.md").exists()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# =============================================================================
|
|
579
|
+
# AC1 + AC3-AC6: Readable format / round-trip parsing
|
|
580
|
+
# =============================================================================
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class TestParseDialogueExchanges:
|
|
584
|
+
"""AC1: parse_dialogue_exchanges — round-trip format fidelity."""
|
|
585
|
+
|
|
586
|
+
def test_parses_single_exchange(self, tmp_path: Path):
|
|
587
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
588
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
589
|
+
|
|
590
|
+
content = dialogue_path.read_text()
|
|
591
|
+
parsed = parse_dialogue_exchanges(content)
|
|
592
|
+
|
|
593
|
+
assert len(parsed) == 1
|
|
594
|
+
assert parsed[0].number == 1
|
|
595
|
+
|
|
596
|
+
def test_parses_multiple_exchanges(self, tmp_path: Path):
|
|
597
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
598
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
599
|
+
append_exchange_to_file(dialogue_path, SECOND_EXCHANGE)
|
|
600
|
+
|
|
601
|
+
content = dialogue_path.read_text()
|
|
602
|
+
parsed = parse_dialogue_exchanges(content)
|
|
603
|
+
|
|
604
|
+
assert len(parsed) == 2
|
|
605
|
+
assert parsed[0].number == 1
|
|
606
|
+
assert parsed[1].number == 2
|
|
607
|
+
|
|
608
|
+
def test_parses_exchange_fields(self, tmp_path: Path):
|
|
609
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
610
|
+
append_exchange_to_file(dialogue_path, SECOND_EXCHANGE, VALID_HEADER)
|
|
611
|
+
|
|
612
|
+
content = dialogue_path.read_text()
|
|
613
|
+
parsed = parse_dialogue_exchanges(content)
|
|
614
|
+
|
|
615
|
+
assert parsed[0].leader == "dev"
|
|
616
|
+
assert parsed[0].partner == "architect"
|
|
617
|
+
assert parsed[0].timestamp == "10:20"
|
|
618
|
+
assert parsed[0].outcome == "applied"
|
|
619
|
+
|
|
620
|
+
def test_parses_pending_outcome_as_none(self, tmp_path: Path):
|
|
621
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
622
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
623
|
+
|
|
624
|
+
content = dialogue_path.read_text()
|
|
625
|
+
parsed = parse_dialogue_exchanges(content)
|
|
626
|
+
|
|
627
|
+
assert parsed[0].outcome is None
|
|
628
|
+
|
|
629
|
+
def test_round_trip_question(self, tmp_path: Path):
|
|
630
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
631
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
632
|
+
|
|
633
|
+
content = dialogue_path.read_text()
|
|
634
|
+
parsed = parse_dialogue_exchanges(content)
|
|
635
|
+
|
|
636
|
+
assert parsed[0].question == VALID_EXCHANGE.question
|
|
637
|
+
|
|
638
|
+
def test_round_trip_recommendation(self, tmp_path: Path):
|
|
639
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
640
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
641
|
+
|
|
642
|
+
content = dialogue_path.read_text()
|
|
643
|
+
parsed = parse_dialogue_exchanges(content)
|
|
644
|
+
|
|
645
|
+
assert parsed[0].recommendation == VALID_EXCHANGE.recommendation
|
|
646
|
+
|
|
647
|
+
def test_round_trip_outcome_with_note(self, tmp_path: Path):
|
|
648
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
649
|
+
exchange = DialogueExchange(
|
|
650
|
+
number=1, timestamp="10:05", leader="dev", partner="architect",
|
|
651
|
+
question="Q?", recommendation="R.", confidence="high",
|
|
652
|
+
outcome="applied", outcome_note="Adopted this approach",
|
|
653
|
+
)
|
|
654
|
+
append_exchange_to_file(dialogue_path, exchange, VALID_HEADER)
|
|
655
|
+
|
|
656
|
+
content = dialogue_path.read_text()
|
|
657
|
+
parsed = parse_dialogue_exchanges(content)
|
|
658
|
+
|
|
659
|
+
assert parsed[0].outcome == "applied"
|
|
660
|
+
assert parsed[0].outcome_note == "Adopted this approach"
|
|
661
|
+
|
|
662
|
+
def test_parses_from_pure_formatted_content(self):
|
|
663
|
+
"""Parse directly from format_exchange output, no file I/O."""
|
|
664
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
665
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
666
|
+
# Insert before summary
|
|
667
|
+
summary_idx = content.index("## Summary")
|
|
668
|
+
full_content = content[:summary_idx] + formatted + "\n" + content[summary_idx:]
|
|
669
|
+
|
|
670
|
+
parsed = parse_dialogue_exchanges(full_content)
|
|
671
|
+
|
|
672
|
+
assert len(parsed) == 1
|
|
673
|
+
assert parsed[0].number == 1
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# =============================================================================
|
|
677
|
+
# AC1: Result format compliance ({success, data?, error?})
|
|
678
|
+
# =============================================================================
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class TestResultFormat:
|
|
682
|
+
"""All file operations return DialogueResult with success/data/error."""
|
|
683
|
+
|
|
684
|
+
def test_append_returns_result(self, tmp_path: Path):
|
|
685
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
686
|
+
result = append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
687
|
+
|
|
688
|
+
assert isinstance(result, DialogueResult)
|
|
689
|
+
assert isinstance(result.success, bool)
|
|
690
|
+
|
|
691
|
+
def test_update_outcome_returns_result(self, tmp_path: Path):
|
|
692
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
693
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
694
|
+
result = update_outcome_in_file(dialogue_path, 1, "applied")
|
|
695
|
+
|
|
696
|
+
assert isinstance(result, DialogueResult)
|
|
697
|
+
assert isinstance(result.success, bool)
|
|
698
|
+
|
|
699
|
+
def test_refresh_summary_returns_result(self, tmp_path: Path):
|
|
700
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
701
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
702
|
+
result = refresh_summary(dialogue_path)
|
|
703
|
+
|
|
704
|
+
assert isinstance(result, DialogueResult)
|
|
705
|
+
assert isinstance(result.success, bool)
|
|
706
|
+
|
|
707
|
+
def test_archive_returns_result(self, tmp_path: Path):
|
|
708
|
+
dialogue_path = tmp_path / "86-3-dialogue.md"
|
|
709
|
+
archive_dir = tmp_path / "archive"
|
|
710
|
+
append_exchange_to_file(dialogue_path, VALID_EXCHANGE, VALID_HEADER)
|
|
711
|
+
result = archive_dialogue(dialogue_path, archive_dir, jira_key="MSSCI-15200")
|
|
712
|
+
|
|
713
|
+
assert isinstance(result, DialogueResult)
|
|
714
|
+
assert isinstance(result.success, bool)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# =============================================================================
|
|
718
|
+
# AC1: Markdown structure (well-formed output)
|
|
719
|
+
# =============================================================================
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class TestMarkdownStructure:
|
|
723
|
+
"""Verify output is well-formed markdown."""
|
|
724
|
+
|
|
725
|
+
def test_content_starts_with_h1(self):
|
|
726
|
+
content = create_dialogue_content(VALID_HEADER)
|
|
727
|
+
assert content.startswith("# Tandem Dialogue:")
|
|
728
|
+
|
|
729
|
+
def test_exchange_starts_with_h2(self):
|
|
730
|
+
formatted = format_exchange(VALID_EXCHANGE)
|
|
731
|
+
assert formatted.startswith("## Exchange")
|
|
732
|
+
|
|
733
|
+
def test_summary_starts_with_h2(self):
|
|
734
|
+
summary = generate_summary([VALID_EXCHANGE], "2026-02-16T10:00:00Z")
|
|
735
|
+
assert summary.startswith("## Summary")
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
# =============================================================================
|
|
739
|
+
# AC2: Click CLI subcommands
|
|
740
|
+
# =============================================================================
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class TestConsultationCLI:
|
|
744
|
+
"""AC2: pf consultation CLI group with subcommands."""
|
|
745
|
+
|
|
746
|
+
def setup_method(self):
|
|
747
|
+
self.runner = CliRunner()
|
|
748
|
+
|
|
749
|
+
def test_consultation_group_exists(self):
|
|
750
|
+
from pennyfarthing_scripts.cli import cli
|
|
751
|
+
|
|
752
|
+
result = self.runner.invoke(cli, ["consultation", "--help"])
|
|
753
|
+
assert result.exit_code == 0
|
|
754
|
+
assert "consultation" in result.output.lower() or "dialogue" in result.output.lower()
|
|
755
|
+
|
|
756
|
+
def test_init_subcommand_exists(self):
|
|
757
|
+
from pennyfarthing_scripts.cli import cli
|
|
758
|
+
|
|
759
|
+
result = self.runner.invoke(cli, ["consultation", "init", "--help"])
|
|
760
|
+
assert result.exit_code == 0
|
|
761
|
+
|
|
762
|
+
def test_append_subcommand_exists(self):
|
|
763
|
+
from pennyfarthing_scripts.cli import cli
|
|
764
|
+
|
|
765
|
+
result = self.runner.invoke(cli, ["consultation", "append", "--help"])
|
|
766
|
+
assert result.exit_code == 0
|
|
767
|
+
|
|
768
|
+
def test_outcome_subcommand_exists(self):
|
|
769
|
+
from pennyfarthing_scripts.cli import cli
|
|
770
|
+
|
|
771
|
+
result = self.runner.invoke(cli, ["consultation", "outcome", "--help"])
|
|
772
|
+
assert result.exit_code == 0
|
|
773
|
+
|
|
774
|
+
def test_summarize_subcommand_exists(self):
|
|
775
|
+
from pennyfarthing_scripts.cli import cli
|
|
776
|
+
|
|
777
|
+
result = self.runner.invoke(cli, ["consultation", "summarize", "--help"])
|
|
778
|
+
assert result.exit_code == 0
|
|
779
|
+
|
|
780
|
+
def test_archive_subcommand_exists(self):
|
|
781
|
+
from pennyfarthing_scripts.cli import cli
|
|
782
|
+
|
|
783
|
+
result = self.runner.invoke(cli, ["consultation", "archive", "--help"])
|
|
784
|
+
assert result.exit_code == 0
|
|
785
|
+
|
|
786
|
+
def test_init_creates_dialogue_file(self, tmp_path: Path):
|
|
787
|
+
"""CLI init should create a dialogue file in .session/."""
|
|
788
|
+
from pennyfarthing_scripts.cli import cli
|
|
789
|
+
|
|
790
|
+
with self.runner.isolated_filesystem(temp_dir=tmp_path):
|
|
791
|
+
session_dir = Path(".session")
|
|
792
|
+
session_dir.mkdir()
|
|
793
|
+
result = self.runner.invoke(
|
|
794
|
+
cli,
|
|
795
|
+
["consultation", "init", "86-3", "tdd-tandem", "dev", "architect"],
|
|
796
|
+
)
|
|
797
|
+
# Should succeed (exit 0) and create the file
|
|
798
|
+
assert result.exit_code == 0
|
|
799
|
+
dialogue_file = session_dir / "86-3-dialogue.md"
|
|
800
|
+
assert dialogue_file.exists()
|
|
801
|
+
|
|
802
|
+
def test_init_output_confirms_creation(self, tmp_path: Path):
|
|
803
|
+
from pennyfarthing_scripts.cli import cli
|
|
804
|
+
|
|
805
|
+
with self.runner.isolated_filesystem(temp_dir=tmp_path):
|
|
806
|
+
Path(".session").mkdir()
|
|
807
|
+
result = self.runner.invoke(
|
|
808
|
+
cli,
|
|
809
|
+
["consultation", "init", "86-3", "tdd-tandem", "dev", "architect"],
|
|
810
|
+
)
|
|
811
|
+
assert "Created" in result.output or "created" in result.output
|