mindsystem-cc 3.21.0 → 3.22.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 +4 -12
- package/agents/ms-plan-checker.md +30 -30
- package/agents/ms-plan-writer.md +1 -1
- package/agents/ms-product-researcher.md +4 -2
- package/agents/ms-verifier.md +25 -117
- package/commands/ms/add-phase.md +3 -4
- package/commands/ms/add-todo.md +3 -4
- package/commands/ms/adhoc.md +3 -4
- package/commands/ms/audit-milestone.md +4 -3
- package/commands/ms/complete-milestone.md +2 -2
- package/commands/ms/config.md +36 -9
- package/commands/ms/create-roadmap.md +3 -4
- package/commands/ms/debug.md +3 -4
- package/commands/ms/design-phase.md +8 -5
- package/commands/ms/discuss-phase.md +2 -2
- package/commands/ms/doctor.md +9 -6
- package/commands/ms/execute-phase.md +2 -5
- package/commands/ms/help.md +2 -2
- package/commands/ms/insert-phase.md +3 -4
- package/commands/ms/map-codebase.md +1 -2
- package/commands/ms/new-milestone.md +1 -3
- package/commands/ms/new-project.md +3 -5
- package/commands/ms/plan-milestone-gaps.md +3 -4
- package/commands/ms/plan-phase.md +2 -3
- package/commands/ms/progress.md +1 -0
- package/commands/ms/remove-phase.md +3 -4
- package/commands/ms/research-phase.md +4 -4
- package/commands/ms/research-project.md +9 -16
- package/commands/ms/review-design.md +4 -2
- package/commands/ms/verify-work.md +6 -8
- package/mindsystem/references/verification-patterns.md +0 -37
- package/mindsystem/templates/config.json +2 -1
- package/mindsystem/templates/roadmap.md +1 -1
- package/mindsystem/templates/state.md +2 -2
- package/mindsystem/templates/verification-report.md +3 -26
- package/mindsystem/workflows/discuss-phase.md +7 -3
- package/mindsystem/workflows/execute-phase.md +2 -18
- package/mindsystem/workflows/map-codebase.md +6 -12
- package/mindsystem/workflows/mockup-generation.md +46 -22
- package/mindsystem/workflows/plan-phase.md +12 -5
- package/mindsystem/workflows/verify-work.md +96 -69
- package/package.json +1 -1
- package/scripts/__pycache__/ms-tools.cpython-314.pyc +0 -0
- package/scripts/__pycache__/test_ms_tools.cpython-314-pytest-9.0.2.pyc +0 -0
- package/scripts/ms-tools.py +751 -6
- package/scripts/test_ms_tools.py +786 -0
- package/agents/ms-flutter-code-quality.md +0 -169
- package/agents/ms-flutter-reviewer.md +0 -211
- package/agents/ms-flutter-simplifier.md +0 -79
- package/mindsystem/workflows/verify-phase.md +0 -625
- package/skills/flutter-code-quality/SKILL.md +0 -143
- package/skills/flutter-code-simplification/SKILL.md +0 -102
- package/skills/flutter-senior-review/AGENTS.md +0 -869
- package/skills/flutter-senior-review/SKILL.md +0 -205
- package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +0 -75
- package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +0 -85
- package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +0 -97
- package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +0 -130
- package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +0 -91
- package/skills/flutter-senior-review/principles/state-data-clumps.md +0 -64
- package/skills/flutter-senior-review/principles/state-invalid-states.md +0 -53
- package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +0 -68
- package/skills/flutter-senior-review/principles/state-type-hierarchies.md +0 -75
- package/skills/flutter-senior-review/principles/structure-composition-over-config.md +0 -105
- package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +0 -107
- package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +0 -90
package/scripts/test_ms_tools.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""Tests for ms-tools.py pure logic layer and scan-planning-context integration."""
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime
|
|
3
5
|
import importlib.util
|
|
6
|
+
import io
|
|
4
7
|
import json
|
|
5
8
|
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
6
10
|
|
|
7
11
|
import pytest
|
|
8
12
|
|
|
@@ -36,6 +40,7 @@ _scan_knowledge_files = _mod._scan_knowledge_files
|
|
|
36
40
|
_detect_versioned_milestone_dirs = _mod._detect_versioned_milestone_dirs
|
|
37
41
|
_parse_milestone_name_mapping = _mod._parse_milestone_name_mapping
|
|
38
42
|
_SafeEncoder = _mod._SafeEncoder
|
|
43
|
+
cmd_set_last_command = _mod.cmd_set_last_command
|
|
39
44
|
|
|
40
45
|
# ---------------------------------------------------------------------------
|
|
41
46
|
# Fixtures
|
|
@@ -834,3 +839,784 @@ class TestParseMilestoneNameMapping:
|
|
|
834
839
|
assert len(result) == 1
|
|
835
840
|
assert result[0]["version"] == "v0.2"
|
|
836
841
|
assert result[0]["name"] == "Infrastructure"
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# ---------------------------------------------------------------------------
|
|
845
|
+
# Tests: cmd_set_last_command
|
|
846
|
+
# ---------------------------------------------------------------------------
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _make_args(command_string: str) -> argparse.Namespace:
|
|
850
|
+
return argparse.Namespace(command_string=command_string)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
class TestSetLastCommand:
|
|
854
|
+
"""Tests for the set-last-command subcommand."""
|
|
855
|
+
|
|
856
|
+
def _patch_git_root(self, tmp_path):
|
|
857
|
+
return mock.patch.object(_mod, "find_git_root", return_value=tmp_path)
|
|
858
|
+
|
|
859
|
+
def test_replaces_existing_last_command(self, tmp_path):
|
|
860
|
+
state = tmp_path / ".planning" / "STATE.md"
|
|
861
|
+
state.parent.mkdir(parents=True)
|
|
862
|
+
state.write_text(
|
|
863
|
+
"# State\n"
|
|
864
|
+
"Status: In progress\n"
|
|
865
|
+
"Last Command: ms:old-cmd | 2025-01-01 00:00\n"
|
|
866
|
+
"Phase: 10\n"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
with self._patch_git_root(tmp_path):
|
|
870
|
+
cmd_set_last_command(_make_args("ms:plan-phase 10"))
|
|
871
|
+
|
|
872
|
+
text = state.read_text()
|
|
873
|
+
assert "ms:plan-phase 10 |" in text
|
|
874
|
+
assert "ms:old-cmd" not in text
|
|
875
|
+
# Verify only one Last Command line
|
|
876
|
+
assert text.count("Last Command:") == 1
|
|
877
|
+
|
|
878
|
+
def test_inserts_after_status_when_missing(self, tmp_path):
|
|
879
|
+
state = tmp_path / ".planning" / "STATE.md"
|
|
880
|
+
state.parent.mkdir(parents=True)
|
|
881
|
+
state.write_text(
|
|
882
|
+
"# State\n"
|
|
883
|
+
"Status: In progress\n"
|
|
884
|
+
"Phase: 10\n"
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
with self._patch_git_root(tmp_path):
|
|
888
|
+
cmd_set_last_command(_make_args("ms:execute-phase 10"))
|
|
889
|
+
|
|
890
|
+
text = state.read_text()
|
|
891
|
+
assert "Last Command: ms:execute-phase 10 |" in text
|
|
892
|
+
# Should appear after Status line
|
|
893
|
+
lines = text.splitlines()
|
|
894
|
+
status_idx = next(i for i, l in enumerate(lines) if l.startswith("Status:"))
|
|
895
|
+
last_cmd_idx = next(i for i, l in enumerate(lines) if l.startswith("Last Command:"))
|
|
896
|
+
assert last_cmd_idx == status_idx + 1
|
|
897
|
+
|
|
898
|
+
def test_missing_state_file_warns(self, tmp_path, capsys):
|
|
899
|
+
with self._patch_git_root(tmp_path):
|
|
900
|
+
cmd_set_last_command(_make_args("ms:plan-phase 10"))
|
|
901
|
+
|
|
902
|
+
captured = capsys.readouterr()
|
|
903
|
+
assert "Warning: STATE.md not found" in captured.err
|
|
904
|
+
assert captured.out == ""
|
|
905
|
+
|
|
906
|
+
def test_missing_both_lines_warns(self, tmp_path, capsys):
|
|
907
|
+
state = tmp_path / ".planning" / "STATE.md"
|
|
908
|
+
state.parent.mkdir(parents=True)
|
|
909
|
+
original = "# State\nPhase: 10\n"
|
|
910
|
+
state.write_text(original)
|
|
911
|
+
|
|
912
|
+
with self._patch_git_root(tmp_path):
|
|
913
|
+
cmd_set_last_command(_make_args("ms:adhoc"))
|
|
914
|
+
|
|
915
|
+
captured = capsys.readouterr()
|
|
916
|
+
assert "Warning:" in captured.err
|
|
917
|
+
# File should be unchanged
|
|
918
|
+
assert state.read_text() == original
|
|
919
|
+
|
|
920
|
+
def test_timestamp_format(self, tmp_path):
|
|
921
|
+
state = tmp_path / ".planning" / "STATE.md"
|
|
922
|
+
state.parent.mkdir(parents=True)
|
|
923
|
+
state.write_text("# State\nStatus: Idle\nLast Command: old\n")
|
|
924
|
+
|
|
925
|
+
fake_dt = mock.MagicMock()
|
|
926
|
+
fake_dt.datetime.now.return_value.strftime.return_value = "2026-02-24 14:30"
|
|
927
|
+
with self._patch_git_root(tmp_path), \
|
|
928
|
+
mock.patch.object(_mod, "datetime", fake_dt):
|
|
929
|
+
cmd_set_last_command(_make_args("ms:verify-work 10"))
|
|
930
|
+
|
|
931
|
+
text = state.read_text()
|
|
932
|
+
assert "Last Command: ms:verify-work 10 | 2026-02-24 14:30" in text
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ===================================================================
|
|
936
|
+
# Part 4: UAT File Management Tests
|
|
937
|
+
# ===================================================================
|
|
938
|
+
|
|
939
|
+
UATFile = _mod.UATFile
|
|
940
|
+
cmd_uat_init = _mod.cmd_uat_init
|
|
941
|
+
cmd_uat_update = _mod.cmd_uat_update
|
|
942
|
+
cmd_uat_status = _mod.cmd_uat_status
|
|
943
|
+
|
|
944
|
+
# Shared fixture: a complete UAT.md matching the template format
|
|
945
|
+
UAT_FIXTURE = """\
|
|
946
|
+
---
|
|
947
|
+
status: testing
|
|
948
|
+
phase: 05-auth
|
|
949
|
+
source: [05-01-SUMMARY.md]
|
|
950
|
+
started: '2026-02-24 10:00'
|
|
951
|
+
updated: '2026-02-24 10:30'
|
|
952
|
+
current_batch: 2
|
|
953
|
+
mocked_files: [auth_service.dart]
|
|
954
|
+
pre_work_stash: null
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## Progress
|
|
958
|
+
|
|
959
|
+
total: 5
|
|
960
|
+
tested: 3
|
|
961
|
+
passed: 2
|
|
962
|
+
issues: 1
|
|
963
|
+
fixing: 0
|
|
964
|
+
pending: 2
|
|
965
|
+
skipped: 0
|
|
966
|
+
|
|
967
|
+
## Current Batch
|
|
968
|
+
|
|
969
|
+
batch: 2 of 3
|
|
970
|
+
name: "Error States"
|
|
971
|
+
mock_type: error_state
|
|
972
|
+
tests: [3, 4]
|
|
973
|
+
status: testing
|
|
974
|
+
|
|
975
|
+
## Tests
|
|
976
|
+
|
|
977
|
+
### 1. Login with valid credentials
|
|
978
|
+
expected: User sees dashboard after entering valid email/password
|
|
979
|
+
mock_required: false
|
|
980
|
+
mock_type: null
|
|
981
|
+
result: pass
|
|
982
|
+
|
|
983
|
+
### 2. View profile page
|
|
984
|
+
expected: Profile shows user name and email
|
|
985
|
+
mock_required: false
|
|
986
|
+
mock_type: null
|
|
987
|
+
result: pass
|
|
988
|
+
|
|
989
|
+
### 3. Login with invalid password
|
|
990
|
+
expected: Error banner shows "Invalid credentials"
|
|
991
|
+
mock_required: true
|
|
992
|
+
mock_type: error_state
|
|
993
|
+
result: issue
|
|
994
|
+
reported: "Shows generic error instead of specific message"
|
|
995
|
+
severity: major
|
|
996
|
+
fix_status: applied
|
|
997
|
+
fix_commit: abc1234
|
|
998
|
+
retry_count: 0
|
|
999
|
+
|
|
1000
|
+
### 4. Login with expired token
|
|
1001
|
+
expected: Redirect to login page with session expired message
|
|
1002
|
+
mock_required: true
|
|
1003
|
+
mock_type: error_state
|
|
1004
|
+
result: [pending]
|
|
1005
|
+
|
|
1006
|
+
### 5. Premium feature access
|
|
1007
|
+
expected: Shows upgrade prompt for free users
|
|
1008
|
+
mock_required: true
|
|
1009
|
+
mock_type: premium_user
|
|
1010
|
+
result: [pending]
|
|
1011
|
+
|
|
1012
|
+
## Fixes Applied
|
|
1013
|
+
|
|
1014
|
+
- commit: abc1234
|
|
1015
|
+
test: 3
|
|
1016
|
+
description: "Fixed error message to show specific auth error"
|
|
1017
|
+
files: [auth_service.dart, login_page.dart]
|
|
1018
|
+
|
|
1019
|
+
## Batches
|
|
1020
|
+
|
|
1021
|
+
### Batch 1: No Mocks Required
|
|
1022
|
+
tests: [1, 2]
|
|
1023
|
+
status: complete
|
|
1024
|
+
mock_type: null
|
|
1025
|
+
passed: 2
|
|
1026
|
+
issues: 0
|
|
1027
|
+
|
|
1028
|
+
### Batch 2: Error States
|
|
1029
|
+
tests: [3, 4]
|
|
1030
|
+
status: testing
|
|
1031
|
+
mock_type: error_state
|
|
1032
|
+
|
|
1033
|
+
### Batch 3: Premium Features
|
|
1034
|
+
tests: [5]
|
|
1035
|
+
status: pending
|
|
1036
|
+
mock_type: premium_user
|
|
1037
|
+
|
|
1038
|
+
## Assumptions
|
|
1039
|
+
"""
|
|
1040
|
+
|
|
1041
|
+
# Minimal UAT.md with empty fixes/assumptions
|
|
1042
|
+
UAT_MINIMAL = """\
|
|
1043
|
+
---
|
|
1044
|
+
status: testing
|
|
1045
|
+
phase: 03-setup
|
|
1046
|
+
source: [03-01-SUMMARY.md]
|
|
1047
|
+
started: '2026-02-24 10:00'
|
|
1048
|
+
updated: '2026-02-24 10:00'
|
|
1049
|
+
current_batch: 1
|
|
1050
|
+
mocked_files: []
|
|
1051
|
+
pre_work_stash: null
|
|
1052
|
+
---
|
|
1053
|
+
|
|
1054
|
+
## Progress
|
|
1055
|
+
|
|
1056
|
+
total: 2
|
|
1057
|
+
tested: 0
|
|
1058
|
+
passed: 0
|
|
1059
|
+
issues: 0
|
|
1060
|
+
fixing: 0
|
|
1061
|
+
pending: 2
|
|
1062
|
+
skipped: 0
|
|
1063
|
+
|
|
1064
|
+
## Current Batch
|
|
1065
|
+
|
|
1066
|
+
batch: 1 of 1
|
|
1067
|
+
name: "No Mocks"
|
|
1068
|
+
mock_type: null
|
|
1069
|
+
tests: [1, 2]
|
|
1070
|
+
status: pending
|
|
1071
|
+
|
|
1072
|
+
## Tests
|
|
1073
|
+
|
|
1074
|
+
### 1. Basic setup check
|
|
1075
|
+
expected: App starts without errors
|
|
1076
|
+
mock_required: false
|
|
1077
|
+
mock_type: null
|
|
1078
|
+
result: [pending]
|
|
1079
|
+
|
|
1080
|
+
### 2. Config loads
|
|
1081
|
+
expected: Config values appear in settings
|
|
1082
|
+
mock_required: false
|
|
1083
|
+
mock_type: null
|
|
1084
|
+
result: [pending]
|
|
1085
|
+
|
|
1086
|
+
## Fixes Applied
|
|
1087
|
+
|
|
1088
|
+
## Batches
|
|
1089
|
+
|
|
1090
|
+
### Batch 1: No Mocks
|
|
1091
|
+
tests: [1, 2]
|
|
1092
|
+
status: pending
|
|
1093
|
+
mock_type: null
|
|
1094
|
+
|
|
1095
|
+
## Assumptions
|
|
1096
|
+
"""
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
class TestUATFileParse:
|
|
1100
|
+
"""Test UATFile.parse with complete and minimal fixtures."""
|
|
1101
|
+
|
|
1102
|
+
def test_parse_complete_file(self):
|
|
1103
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1104
|
+
assert uat.frontmatter["status"] == "testing"
|
|
1105
|
+
assert uat.frontmatter["phase"] == "05-auth"
|
|
1106
|
+
assert uat.frontmatter["current_batch"] == 2
|
|
1107
|
+
assert uat.frontmatter["mocked_files"] == ["auth_service.dart"]
|
|
1108
|
+
assert len(uat.tests) == 5
|
|
1109
|
+
assert len(uat.batches) == 3
|
|
1110
|
+
assert len(uat.fixes) == 1
|
|
1111
|
+
assert len(uat.assumptions) == 0
|
|
1112
|
+
|
|
1113
|
+
def test_parse_minimal_file(self):
|
|
1114
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1115
|
+
assert uat.frontmatter["phase"] == "03-setup"
|
|
1116
|
+
assert len(uat.tests) == 2
|
|
1117
|
+
assert len(uat.batches) == 1
|
|
1118
|
+
assert len(uat.fixes) == 0
|
|
1119
|
+
assert len(uat.assumptions) == 0
|
|
1120
|
+
|
|
1121
|
+
def test_parse_test_with_issue_fields(self):
|
|
1122
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1123
|
+
t3 = next(t for t in uat.tests if t["num"] == "3")
|
|
1124
|
+
assert t3["result"] == "issue"
|
|
1125
|
+
assert "Shows generic error" in t3["reported"]
|
|
1126
|
+
assert t3["severity"] == "major"
|
|
1127
|
+
assert t3["fix_status"] == "applied"
|
|
1128
|
+
assert t3["fix_commit"] == "abc1234"
|
|
1129
|
+
assert t3["retry_count"] == "0"
|
|
1130
|
+
|
|
1131
|
+
def test_parse_progress(self):
|
|
1132
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1133
|
+
assert uat.progress["total"] == "5"
|
|
1134
|
+
assert uat.progress["passed"] == "2"
|
|
1135
|
+
assert uat.progress["pending"] == "2"
|
|
1136
|
+
|
|
1137
|
+
def test_parse_current_batch(self):
|
|
1138
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1139
|
+
assert uat.current_batch["batch"] == "2 of 3"
|
|
1140
|
+
assert uat.current_batch["mock_type"] == "error_state"
|
|
1141
|
+
assert uat.current_batch["status"] == "testing"
|
|
1142
|
+
|
|
1143
|
+
def test_parse_fixes(self):
|
|
1144
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1145
|
+
fix = uat.fixes[0]
|
|
1146
|
+
assert fix["commit"] == "abc1234"
|
|
1147
|
+
assert fix["test"] == "3"
|
|
1148
|
+
assert "Fixed error message" in fix["description"]
|
|
1149
|
+
|
|
1150
|
+
def test_parse_batches(self):
|
|
1151
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1152
|
+
b1 = uat.batches[0]
|
|
1153
|
+
assert b1["name"] == "No Mocks Required"
|
|
1154
|
+
assert b1["status"] == "complete"
|
|
1155
|
+
assert b1["passed"] == "2"
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
class TestUATFileRoundtrip:
|
|
1159
|
+
"""Test parse -> serialize roundtrip."""
|
|
1160
|
+
|
|
1161
|
+
def test_roundtrip_preserves_structure(self):
|
|
1162
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1163
|
+
output = uat.serialize()
|
|
1164
|
+
# Re-parse the output
|
|
1165
|
+
uat2 = UATFile.parse(output)
|
|
1166
|
+
assert len(uat2.tests) == len(uat.tests)
|
|
1167
|
+
assert len(uat2.batches) == len(uat.batches)
|
|
1168
|
+
assert len(uat2.fixes) == len(uat.fixes)
|
|
1169
|
+
assert uat2.frontmatter["phase"] == "05-auth"
|
|
1170
|
+
# Test names preserved
|
|
1171
|
+
for t1, t2 in zip(uat.tests, uat2.tests):
|
|
1172
|
+
assert t1["name"] == t2["name"]
|
|
1173
|
+
assert t1["result"] == t2["result"]
|
|
1174
|
+
|
|
1175
|
+
def test_roundtrip_minimal(self):
|
|
1176
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1177
|
+
output = uat.serialize()
|
|
1178
|
+
uat2 = UATFile.parse(output)
|
|
1179
|
+
assert len(uat2.tests) == 2
|
|
1180
|
+
assert len(uat2.fixes) == 0
|
|
1181
|
+
assert len(uat2.assumptions) == 0
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
class TestUATFileRecalcProgress:
|
|
1185
|
+
"""Test recalc_progress with various result combinations."""
|
|
1186
|
+
|
|
1187
|
+
def test_all_pending(self):
|
|
1188
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1189
|
+
uat.recalc_progress()
|
|
1190
|
+
assert uat.progress["total"] == "2"
|
|
1191
|
+
assert uat.progress["pending"] == "2"
|
|
1192
|
+
assert uat.progress["tested"] == "0"
|
|
1193
|
+
assert uat.progress["passed"] == "0"
|
|
1194
|
+
|
|
1195
|
+
def test_mixed_results(self):
|
|
1196
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1197
|
+
uat.recalc_progress()
|
|
1198
|
+
assert uat.progress["total"] == "5"
|
|
1199
|
+
assert uat.progress["passed"] == "2"
|
|
1200
|
+
# Test 3 has fix_status=applied → fixing
|
|
1201
|
+
assert uat.progress["fixing"] == "1"
|
|
1202
|
+
assert uat.progress["pending"] == "2"
|
|
1203
|
+
|
|
1204
|
+
def test_verified_fix_counts_as_passed(self):
|
|
1205
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1206
|
+
# Change test 3's fix_status to verified
|
|
1207
|
+
t3 = next(t for t in uat.tests if t["num"] == "3")
|
|
1208
|
+
t3["fix_status"] = "verified"
|
|
1209
|
+
uat.recalc_progress()
|
|
1210
|
+
assert uat.progress["passed"] == "3" # 2 pass + 1 verified
|
|
1211
|
+
assert uat.progress["fixing"] == "0"
|
|
1212
|
+
|
|
1213
|
+
def test_blocked_counts_as_pending(self):
|
|
1214
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1215
|
+
uat.tests[0]["result"] = "blocked"
|
|
1216
|
+
uat.recalc_progress()
|
|
1217
|
+
assert uat.progress["pending"] == "2" # 1 blocked + 1 [pending]
|
|
1218
|
+
assert uat.progress["tested"] == "0"
|
|
1219
|
+
|
|
1220
|
+
def test_skipped(self):
|
|
1221
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1222
|
+
uat.tests[0]["result"] = "skipped"
|
|
1223
|
+
uat.recalc_progress()
|
|
1224
|
+
assert uat.progress["skipped"] == "1"
|
|
1225
|
+
assert uat.progress["pending"] == "1"
|
|
1226
|
+
assert uat.progress["tested"] == "1"
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
class TestUATFileMutations:
|
|
1230
|
+
"""Test update_test, update_batch, update_session."""
|
|
1231
|
+
|
|
1232
|
+
def test_update_test(self):
|
|
1233
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1234
|
+
uat.update_test(1, {"result": "pass"})
|
|
1235
|
+
t1 = next(t for t in uat.tests if t["num"] == "1")
|
|
1236
|
+
assert t1["result"] == "pass"
|
|
1237
|
+
|
|
1238
|
+
def test_update_test_not_found(self):
|
|
1239
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1240
|
+
with pytest.raises(ValueError, match="Test 99 not found"):
|
|
1241
|
+
uat.update_test(99, {"result": "pass"})
|
|
1242
|
+
|
|
1243
|
+
def test_update_batch(self):
|
|
1244
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1245
|
+
uat.update_batch(1, {"passed": "3", "issues": "0"})
|
|
1246
|
+
b1 = next(b for b in uat.batches if b["num"] == "1")
|
|
1247
|
+
assert b1["passed"] == "3"
|
|
1248
|
+
|
|
1249
|
+
def test_update_session(self):
|
|
1250
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1251
|
+
uat.update_session({"status": "fixing"})
|
|
1252
|
+
assert uat.frontmatter["status"] == "fixing"
|
|
1253
|
+
|
|
1254
|
+
def test_update_session_mocked_files(self):
|
|
1255
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1256
|
+
uat.update_session({"mocked_files": "a.dart,b.dart"})
|
|
1257
|
+
assert uat.frontmatter["mocked_files"] == ["a.dart", "b.dart"]
|
|
1258
|
+
|
|
1259
|
+
def test_update_session_clear_mocked_files(self):
|
|
1260
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1261
|
+
uat.update_session({"mocked_files": ""})
|
|
1262
|
+
assert uat.frontmatter["mocked_files"] == []
|
|
1263
|
+
|
|
1264
|
+
def test_update_session_current_batch_syncs(self):
|
|
1265
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1266
|
+
uat.update_session({"current_batch": "3"})
|
|
1267
|
+
assert uat.frontmatter["current_batch"] == 3
|
|
1268
|
+
assert uat.current_batch["batch"] == "3 of 3"
|
|
1269
|
+
assert "Premium" in uat.current_batch["name"]
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
class TestUATFileAppendFix:
|
|
1273
|
+
"""Test append_fix new and in-place update."""
|
|
1274
|
+
|
|
1275
|
+
def test_append_new_fix(self):
|
|
1276
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1277
|
+
uat.append_fix({
|
|
1278
|
+
"commit": "def567",
|
|
1279
|
+
"test": 1,
|
|
1280
|
+
"description": "Fixed setup",
|
|
1281
|
+
"files": ["setup.dart"],
|
|
1282
|
+
})
|
|
1283
|
+
assert len(uat.fixes) == 1
|
|
1284
|
+
assert uat.fixes[0]["commit"] == "def567"
|
|
1285
|
+
assert uat.fixes[0]["files"] == "[setup.dart]"
|
|
1286
|
+
|
|
1287
|
+
def test_append_fix_same_test_updates_in_place(self):
|
|
1288
|
+
uat = UATFile.parse(UAT_FIXTURE)
|
|
1289
|
+
assert len(uat.fixes) == 1
|
|
1290
|
+
uat.append_fix({
|
|
1291
|
+
"commit": "new999",
|
|
1292
|
+
"test": 3,
|
|
1293
|
+
"description": "Better fix for auth error",
|
|
1294
|
+
"files": ["auth_service.dart"],
|
|
1295
|
+
})
|
|
1296
|
+
# Still 1 fix, updated in place
|
|
1297
|
+
assert len(uat.fixes) == 1
|
|
1298
|
+
assert uat.fixes[0]["commit"] == "new999"
|
|
1299
|
+
assert "Better fix" in uat.fixes[0]["description"]
|
|
1300
|
+
|
|
1301
|
+
def test_append_assumption(self):
|
|
1302
|
+
uat = UATFile.parse(UAT_MINIMAL)
|
|
1303
|
+
uat.append_assumption({
|
|
1304
|
+
"test": 2,
|
|
1305
|
+
"name": "Config loads",
|
|
1306
|
+
"expected": "Config values appear",
|
|
1307
|
+
"reason": "No config file available",
|
|
1308
|
+
})
|
|
1309
|
+
assert len(uat.assumptions) == 1
|
|
1310
|
+
assert uat.assumptions[0]["test"] == "2"
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
class TestCmdUatInit:
|
|
1314
|
+
"""Tests for uat-init command."""
|
|
1315
|
+
|
|
1316
|
+
def _patch_git_root(self, tmp_path):
|
|
1317
|
+
return mock.patch.object(_mod, "find_git_root", return_value=tmp_path)
|
|
1318
|
+
|
|
1319
|
+
def test_creates_file_from_valid_json(self, tmp_path, capsys):
|
|
1320
|
+
# Create phase dir
|
|
1321
|
+
phase_dir = tmp_path / ".planning" / "phases" / "05-auth"
|
|
1322
|
+
phase_dir.mkdir(parents=True)
|
|
1323
|
+
|
|
1324
|
+
input_json = json.dumps({
|
|
1325
|
+
"source": ["05-01-SUMMARY.md"],
|
|
1326
|
+
"tests": [
|
|
1327
|
+
{"name": "Login works", "expected": "User sees dashboard", "mock_required": False, "mock_type": None},
|
|
1328
|
+
{"name": "Logout works", "expected": "User sees login page", "mock_required": False, "mock_type": None},
|
|
1329
|
+
],
|
|
1330
|
+
"batches": [
|
|
1331
|
+
{"name": "No Mocks", "mock_type": None, "tests": [1, 2]},
|
|
1332
|
+
],
|
|
1333
|
+
})
|
|
1334
|
+
|
|
1335
|
+
args = argparse.Namespace(phase="5")
|
|
1336
|
+
with self._patch_git_root(tmp_path), \
|
|
1337
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO(input_json)):
|
|
1338
|
+
cmd_uat_init(args)
|
|
1339
|
+
|
|
1340
|
+
captured = capsys.readouterr()
|
|
1341
|
+
assert "2 tests" in captured.out
|
|
1342
|
+
assert "1 batches" in captured.out
|
|
1343
|
+
|
|
1344
|
+
uat_path = phase_dir / "05-auth-UAT.md"
|
|
1345
|
+
assert uat_path.is_file()
|
|
1346
|
+
content = uat_path.read_text()
|
|
1347
|
+
assert "Login works" in content
|
|
1348
|
+
assert "Logout works" in content
|
|
1349
|
+
|
|
1350
|
+
def test_auto_creates_phase_dir(self, tmp_path, capsys):
|
|
1351
|
+
(tmp_path / ".planning" / "phases").mkdir(parents=True)
|
|
1352
|
+
|
|
1353
|
+
input_json = json.dumps({
|
|
1354
|
+
"source": [],
|
|
1355
|
+
"tests": [{"name": "Test", "expected": "Works"}],
|
|
1356
|
+
"batches": [{"name": "B1", "tests": [1]}],
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
args = argparse.Namespace(phase="99")
|
|
1360
|
+
with self._patch_git_root(tmp_path), \
|
|
1361
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO(input_json)):
|
|
1362
|
+
cmd_uat_init(args)
|
|
1363
|
+
|
|
1364
|
+
captured = capsys.readouterr()
|
|
1365
|
+
assert "1 tests" in captured.out
|
|
1366
|
+
assert (tmp_path / ".planning" / "phases" / "99").is_dir()
|
|
1367
|
+
|
|
1368
|
+
def test_invalid_json_exits(self, tmp_path):
|
|
1369
|
+
(tmp_path / ".planning" / "phases" / "05-auth").mkdir(parents=True)
|
|
1370
|
+
|
|
1371
|
+
args = argparse.Namespace(phase="5")
|
|
1372
|
+
with self._patch_git_root(tmp_path), \
|
|
1373
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO("not json")), \
|
|
1374
|
+
pytest.raises(SystemExit) as exc:
|
|
1375
|
+
cmd_uat_init(args)
|
|
1376
|
+
assert exc.value.code == 1
|
|
1377
|
+
|
|
1378
|
+
def test_stdout_contains_path_and_counts(self, tmp_path, capsys):
|
|
1379
|
+
phase_dir = tmp_path / ".planning" / "phases" / "03-setup"
|
|
1380
|
+
phase_dir.mkdir(parents=True)
|
|
1381
|
+
|
|
1382
|
+
input_json = json.dumps({
|
|
1383
|
+
"source": ["03-01-SUMMARY.md"],
|
|
1384
|
+
"tests": [
|
|
1385
|
+
{"name": "T1", "expected": "E1"},
|
|
1386
|
+
{"name": "T2", "expected": "E2"},
|
|
1387
|
+
{"name": "T3", "expected": "E3"},
|
|
1388
|
+
],
|
|
1389
|
+
"batches": [
|
|
1390
|
+
{"name": "B1", "tests": [1, 2]},
|
|
1391
|
+
{"name": "B2", "tests": [3]},
|
|
1392
|
+
],
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
args = argparse.Namespace(phase="3")
|
|
1396
|
+
with self._patch_git_root(tmp_path), \
|
|
1397
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO(input_json)):
|
|
1398
|
+
cmd_uat_init(args)
|
|
1399
|
+
|
|
1400
|
+
captured = capsys.readouterr()
|
|
1401
|
+
assert "3 tests" in captured.out
|
|
1402
|
+
assert "2 batches" in captured.out
|
|
1403
|
+
assert "03-setup-UAT.md" in captured.out
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
class TestCmdUatUpdate:
|
|
1407
|
+
"""Tests for uat-update command."""
|
|
1408
|
+
|
|
1409
|
+
def _patch_git_root(self, tmp_path):
|
|
1410
|
+
return mock.patch.object(_mod, "find_git_root", return_value=tmp_path)
|
|
1411
|
+
|
|
1412
|
+
def _setup_uat(self, tmp_path, content=UAT_FIXTURE):
|
|
1413
|
+
phase_dir = tmp_path / ".planning" / "phases" / "05-auth"
|
|
1414
|
+
phase_dir.mkdir(parents=True)
|
|
1415
|
+
uat_path = phase_dir / "05-auth-UAT.md"
|
|
1416
|
+
uat_path.write_text(content)
|
|
1417
|
+
return uat_path
|
|
1418
|
+
|
|
1419
|
+
def test_update_test_result_pass(self, tmp_path, capsys):
|
|
1420
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1421
|
+
|
|
1422
|
+
args = argparse.Namespace(
|
|
1423
|
+
phase="5", test=4, batch=None, session=False,
|
|
1424
|
+
append_fix=False, append_assumption=False,
|
|
1425
|
+
fields=["result=pass"],
|
|
1426
|
+
)
|
|
1427
|
+
with self._patch_git_root(tmp_path):
|
|
1428
|
+
cmd_uat_update(args)
|
|
1429
|
+
|
|
1430
|
+
captured = capsys.readouterr()
|
|
1431
|
+
assert "Updated test 4" in captured.out
|
|
1432
|
+
assert "Progress:" in captured.out
|
|
1433
|
+
|
|
1434
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1435
|
+
t4 = next(t for t in uat.tests if t["num"] == "4")
|
|
1436
|
+
assert t4["result"] == "pass"
|
|
1437
|
+
|
|
1438
|
+
def test_update_test_issue_with_fields(self, tmp_path, capsys):
|
|
1439
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1440
|
+
|
|
1441
|
+
args = argparse.Namespace(
|
|
1442
|
+
phase="5", test=4, batch=None, session=False,
|
|
1443
|
+
append_fix=False, append_assumption=False,
|
|
1444
|
+
fields=["result=issue", "severity=major", "fix_status=investigating", "retry_count=0"],
|
|
1445
|
+
)
|
|
1446
|
+
with self._patch_git_root(tmp_path):
|
|
1447
|
+
cmd_uat_update(args)
|
|
1448
|
+
|
|
1449
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1450
|
+
t4 = next(t for t in uat.tests if t["num"] == "4")
|
|
1451
|
+
assert t4["result"] == "issue"
|
|
1452
|
+
assert t4["severity"] == "major"
|
|
1453
|
+
assert t4["fix_status"] == "investigating"
|
|
1454
|
+
|
|
1455
|
+
def test_update_batch_complete(self, tmp_path, capsys):
|
|
1456
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1457
|
+
|
|
1458
|
+
args = argparse.Namespace(
|
|
1459
|
+
phase="5", test=None, batch=2, session=False,
|
|
1460
|
+
append_fix=False, append_assumption=False,
|
|
1461
|
+
fields=["status=complete", "passed=1", "issues=1"],
|
|
1462
|
+
)
|
|
1463
|
+
with self._patch_git_root(tmp_path):
|
|
1464
|
+
cmd_uat_update(args)
|
|
1465
|
+
|
|
1466
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1467
|
+
b2 = next(b for b in uat.batches if b["num"] == "2")
|
|
1468
|
+
assert b2["status"] == "complete"
|
|
1469
|
+
assert b2["passed"] == "1"
|
|
1470
|
+
|
|
1471
|
+
def test_update_session_frontmatter(self, tmp_path, capsys):
|
|
1472
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1473
|
+
|
|
1474
|
+
args = argparse.Namespace(
|
|
1475
|
+
phase="5", test=None, batch=None, session=True,
|
|
1476
|
+
append_fix=False, append_assumption=False,
|
|
1477
|
+
fields=["status=fixing"],
|
|
1478
|
+
)
|
|
1479
|
+
with self._patch_git_root(tmp_path):
|
|
1480
|
+
cmd_uat_update(args)
|
|
1481
|
+
|
|
1482
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1483
|
+
assert uat.frontmatter["status"] == "fixing"
|
|
1484
|
+
|
|
1485
|
+
def test_append_fix_from_stdin(self, tmp_path, capsys):
|
|
1486
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1487
|
+
|
|
1488
|
+
fix_json = json.dumps({
|
|
1489
|
+
"commit": "xyz789",
|
|
1490
|
+
"test": 4,
|
|
1491
|
+
"description": "Fixed token expiry redirect",
|
|
1492
|
+
"files": ["auth_middleware.dart"],
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
args = argparse.Namespace(
|
|
1496
|
+
phase="5", test=None, batch=None, session=False,
|
|
1497
|
+
append_fix=True, append_assumption=False,
|
|
1498
|
+
fields=[],
|
|
1499
|
+
)
|
|
1500
|
+
with self._patch_git_root(tmp_path), \
|
|
1501
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO(fix_json)):
|
|
1502
|
+
cmd_uat_update(args)
|
|
1503
|
+
|
|
1504
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1505
|
+
assert len(uat.fixes) == 2
|
|
1506
|
+
assert uat.fixes[1]["commit"] == "xyz789"
|
|
1507
|
+
|
|
1508
|
+
def test_append_assumption_from_stdin(self, tmp_path, capsys):
|
|
1509
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1510
|
+
|
|
1511
|
+
assumption_json = json.dumps({
|
|
1512
|
+
"test": 5,
|
|
1513
|
+
"name": "Premium feature access",
|
|
1514
|
+
"expected": "Shows upgrade prompt",
|
|
1515
|
+
"reason": "No premium account available",
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
args = argparse.Namespace(
|
|
1519
|
+
phase="5", test=None, batch=None, session=False,
|
|
1520
|
+
append_fix=False, append_assumption=True,
|
|
1521
|
+
fields=[],
|
|
1522
|
+
)
|
|
1523
|
+
with self._patch_git_root(tmp_path), \
|
|
1524
|
+
mock.patch.object(_mod.sys, "stdin", io.StringIO(assumption_json)):
|
|
1525
|
+
cmd_uat_update(args)
|
|
1526
|
+
|
|
1527
|
+
uat = UATFile.parse(uat_path.read_text())
|
|
1528
|
+
assert len(uat.assumptions) == 1
|
|
1529
|
+
assert uat.assumptions[0]["test"] == "5"
|
|
1530
|
+
|
|
1531
|
+
def test_progress_auto_recalculated(self, tmp_path, capsys):
|
|
1532
|
+
uat_path = self._setup_uat(tmp_path, UAT_MINIMAL)
|
|
1533
|
+
|
|
1534
|
+
args = argparse.Namespace(
|
|
1535
|
+
phase="3", test=1, batch=None, session=False,
|
|
1536
|
+
append_fix=False, append_assumption=False,
|
|
1537
|
+
fields=["result=pass"],
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
# Adjust path for phase 03-setup
|
|
1541
|
+
phase_dir = tmp_path / ".planning" / "phases" / "03-setup"
|
|
1542
|
+
phase_dir.mkdir(parents=True, exist_ok=True)
|
|
1543
|
+
(phase_dir / "03-setup-UAT.md").write_text(UAT_MINIMAL)
|
|
1544
|
+
|
|
1545
|
+
with self._patch_git_root(tmp_path):
|
|
1546
|
+
cmd_uat_update(args)
|
|
1547
|
+
|
|
1548
|
+
uat = UATFile.parse((phase_dir / "03-setup-UAT.md").read_text())
|
|
1549
|
+
assert uat.progress["passed"] == "1"
|
|
1550
|
+
assert uat.progress["tested"] == "1"
|
|
1551
|
+
assert uat.progress["pending"] == "1"
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
class TestCmdUatStatus:
|
|
1555
|
+
"""Tests for uat-status command."""
|
|
1556
|
+
|
|
1557
|
+
def _patch_git_root(self, tmp_path):
|
|
1558
|
+
return mock.patch.object(_mod, "find_git_root", return_value=tmp_path)
|
|
1559
|
+
|
|
1560
|
+
def _setup_uat(self, tmp_path, content=UAT_FIXTURE):
|
|
1561
|
+
phase_dir = tmp_path / ".planning" / "phases" / "05-auth"
|
|
1562
|
+
phase_dir.mkdir(parents=True)
|
|
1563
|
+
uat_path = phase_dir / "05-auth-UAT.md"
|
|
1564
|
+
uat_path.write_text(content)
|
|
1565
|
+
return uat_path
|
|
1566
|
+
|
|
1567
|
+
def test_outputs_valid_json(self, tmp_path, capsys):
|
|
1568
|
+
self._setup_uat(tmp_path)
|
|
1569
|
+
|
|
1570
|
+
args = argparse.Namespace(phase="5")
|
|
1571
|
+
with self._patch_git_root(tmp_path):
|
|
1572
|
+
cmd_uat_status(args)
|
|
1573
|
+
|
|
1574
|
+
captured = capsys.readouterr()
|
|
1575
|
+
output = json.loads(captured.out)
|
|
1576
|
+
assert output["status"] == "testing"
|
|
1577
|
+
assert output["current_batch"] == 2
|
|
1578
|
+
assert output["total_batches"] == 3
|
|
1579
|
+
assert output["progress"]["total"] == 5
|
|
1580
|
+
assert output["progress"]["passed"] == 2
|
|
1581
|
+
assert isinstance(output["mocked_files"], list)
|
|
1582
|
+
|
|
1583
|
+
def test_fixing_tests_listed(self, tmp_path, capsys):
|
|
1584
|
+
self._setup_uat(tmp_path)
|
|
1585
|
+
|
|
1586
|
+
args = argparse.Namespace(phase="5")
|
|
1587
|
+
with self._patch_git_root(tmp_path):
|
|
1588
|
+
cmd_uat_status(args)
|
|
1589
|
+
|
|
1590
|
+
captured = capsys.readouterr()
|
|
1591
|
+
output = json.loads(captured.out)
|
|
1592
|
+
# Test 3 has fix_status=applied
|
|
1593
|
+
assert len(output["fixing_tests"]) == 1
|
|
1594
|
+
assert output["fixing_tests"][0]["num"] == 3
|
|
1595
|
+
assert output["fixing_tests"][0]["fix_status"] == "applied"
|
|
1596
|
+
assert output["fixing_tests"][0]["fix_commit"] == "abc1234"
|
|
1597
|
+
|
|
1598
|
+
def test_fixing_tests_without_commit(self, tmp_path, capsys):
|
|
1599
|
+
"""Tests with fix_status=investigating have empty fix_commit."""
|
|
1600
|
+
uat_path = self._setup_uat(tmp_path)
|
|
1601
|
+
# Change test 3 to investigating (no fix_commit yet)
|
|
1602
|
+
content = uat_path.read_text()
|
|
1603
|
+
content = content.replace("fix_status: applied", "fix_status: investigating")
|
|
1604
|
+
content = content.replace("fix_commit: abc1234\n", "")
|
|
1605
|
+
uat_path.write_text(content)
|
|
1606
|
+
|
|
1607
|
+
args = argparse.Namespace(phase="5")
|
|
1608
|
+
with self._patch_git_root(tmp_path):
|
|
1609
|
+
cmd_uat_status(args)
|
|
1610
|
+
|
|
1611
|
+
captured = capsys.readouterr()
|
|
1612
|
+
output = json.loads(captured.out)
|
|
1613
|
+
assert output["fixing_tests"][0]["fix_commit"] == ""
|
|
1614
|
+
|
|
1615
|
+
def test_missing_file_exits(self, tmp_path):
|
|
1616
|
+
(tmp_path / ".planning" / "phases" / "05-auth").mkdir(parents=True)
|
|
1617
|
+
|
|
1618
|
+
args = argparse.Namespace(phase="5")
|
|
1619
|
+
with self._patch_git_root(tmp_path), \
|
|
1620
|
+
pytest.raises(SystemExit) as exc:
|
|
1621
|
+
cmd_uat_status(args)
|
|
1622
|
+
assert exc.value.code == 1
|