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.
Files changed (66) hide show
  1. package/README.md +4 -12
  2. package/agents/ms-plan-checker.md +30 -30
  3. package/agents/ms-plan-writer.md +1 -1
  4. package/agents/ms-product-researcher.md +4 -2
  5. package/agents/ms-verifier.md +25 -117
  6. package/commands/ms/add-phase.md +3 -4
  7. package/commands/ms/add-todo.md +3 -4
  8. package/commands/ms/adhoc.md +3 -4
  9. package/commands/ms/audit-milestone.md +4 -3
  10. package/commands/ms/complete-milestone.md +2 -2
  11. package/commands/ms/config.md +36 -9
  12. package/commands/ms/create-roadmap.md +3 -4
  13. package/commands/ms/debug.md +3 -4
  14. package/commands/ms/design-phase.md +8 -5
  15. package/commands/ms/discuss-phase.md +2 -2
  16. package/commands/ms/doctor.md +9 -6
  17. package/commands/ms/execute-phase.md +2 -5
  18. package/commands/ms/help.md +2 -2
  19. package/commands/ms/insert-phase.md +3 -4
  20. package/commands/ms/map-codebase.md +1 -2
  21. package/commands/ms/new-milestone.md +1 -3
  22. package/commands/ms/new-project.md +3 -5
  23. package/commands/ms/plan-milestone-gaps.md +3 -4
  24. package/commands/ms/plan-phase.md +2 -3
  25. package/commands/ms/progress.md +1 -0
  26. package/commands/ms/remove-phase.md +3 -4
  27. package/commands/ms/research-phase.md +4 -4
  28. package/commands/ms/research-project.md +9 -16
  29. package/commands/ms/review-design.md +4 -2
  30. package/commands/ms/verify-work.md +6 -8
  31. package/mindsystem/references/verification-patterns.md +0 -37
  32. package/mindsystem/templates/config.json +2 -1
  33. package/mindsystem/templates/roadmap.md +1 -1
  34. package/mindsystem/templates/state.md +2 -2
  35. package/mindsystem/templates/verification-report.md +3 -26
  36. package/mindsystem/workflows/discuss-phase.md +7 -3
  37. package/mindsystem/workflows/execute-phase.md +2 -18
  38. package/mindsystem/workflows/map-codebase.md +6 -12
  39. package/mindsystem/workflows/mockup-generation.md +46 -22
  40. package/mindsystem/workflows/plan-phase.md +12 -5
  41. package/mindsystem/workflows/verify-work.md +96 -69
  42. package/package.json +1 -1
  43. package/scripts/__pycache__/ms-tools.cpython-314.pyc +0 -0
  44. package/scripts/__pycache__/test_ms_tools.cpython-314-pytest-9.0.2.pyc +0 -0
  45. package/scripts/ms-tools.py +751 -6
  46. package/scripts/test_ms_tools.py +786 -0
  47. package/agents/ms-flutter-code-quality.md +0 -169
  48. package/agents/ms-flutter-reviewer.md +0 -211
  49. package/agents/ms-flutter-simplifier.md +0 -79
  50. package/mindsystem/workflows/verify-phase.md +0 -625
  51. package/skills/flutter-code-quality/SKILL.md +0 -143
  52. package/skills/flutter-code-simplification/SKILL.md +0 -102
  53. package/skills/flutter-senior-review/AGENTS.md +0 -869
  54. package/skills/flutter-senior-review/SKILL.md +0 -205
  55. package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +0 -75
  56. package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +0 -85
  57. package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +0 -97
  58. package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +0 -130
  59. package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +0 -91
  60. package/skills/flutter-senior-review/principles/state-data-clumps.md +0 -64
  61. package/skills/flutter-senior-review/principles/state-invalid-states.md +0 -53
  62. package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +0 -68
  63. package/skills/flutter-senior-review/principles/state-type-hierarchies.md +0 -75
  64. package/skills/flutter-senior-review/principles/structure-composition-over-config.md +0 -105
  65. package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +0 -107
  66. package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +0 -90
@@ -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