@tencent-rtc/trtc-agent-skills 0.1.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh.md +173 -0
  4. package/bin/cli.js +434 -0
  5. package/knowledge-base/index.yaml +454 -0
  6. package/knowledge-base/platform-slice-template.md +233 -0
  7. package/knowledge-base/scenario-spec.md +350 -0
  8. package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
  9. package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
  10. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
  11. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
  12. package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
  13. package/knowledge-base/slice-spec.md +546 -0
  14. package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
  15. package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
  16. package/knowledge-base/slices/conference/web/device-control.md +338 -0
  17. package/knowledge-base/slices/conference/web/login-auth.md +261 -0
  18. package/knowledge-base/slices/conference/web/network-quality.md +190 -0
  19. package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
  20. package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
  21. package/knowledge-base/slices/conference/web/participant-list.md +238 -0
  22. package/knowledge-base/slices/conference/web/participant-management.md +718 -0
  23. package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
  24. package/knowledge-base/slices/conference/web/room-call.md +213 -0
  25. package/knowledge-base/slices/conference/web/room-chat.md +426 -0
  26. package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
  27. package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
  28. package/knowledge-base/slices/conference/web/screen-share.md +211 -0
  29. package/knowledge-base/slices/conference/web/video-layout.md +675 -0
  30. package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
  31. package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
  32. package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
  33. package/knowledge-base/slices/live/anchor-preview.md +90 -0
  34. package/knowledge-base/slices/live/anchor-room-config.md +104 -0
  35. package/knowledge-base/slices/live/audience-list.md +86 -0
  36. package/knowledge-base/slices/live/audience-manage.md +92 -0
  37. package/knowledge-base/slices/live/audience-watch.md +85 -0
  38. package/knowledge-base/slices/live/audio.md +116 -0
  39. package/knowledge-base/slices/live/barrage.md +88 -0
  40. package/knowledge-base/slices/live/beauty.md +99 -0
  41. package/knowledge-base/slices/live/coguest-apply.md +105 -0
  42. package/knowledge-base/slices/live/device-control.md +91 -0
  43. package/knowledge-base/slices/live/error-codes.md +167 -0
  44. package/knowledge-base/slices/live/gift.md +84 -0
  45. package/knowledge-base/slices/live/ios/.gitkeep +0 -0
  46. package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
  47. package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
  48. package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
  49. package/knowledge-base/slices/live/ios/audience-list.md +353 -0
  50. package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
  51. package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
  52. package/knowledge-base/slices/live/ios/audio.md +373 -0
  53. package/knowledge-base/slices/live/ios/barrage.md +285 -0
  54. package/knowledge-base/slices/live/ios/beauty.md +323 -0
  55. package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
  56. package/knowledge-base/slices/live/ios/device-control.md +286 -0
  57. package/knowledge-base/slices/live/ios/error-codes.md +270 -0
  58. package/knowledge-base/slices/live/ios/gift.md +315 -0
  59. package/knowledge-base/slices/live/ios/live-list.md +269 -0
  60. package/knowledge-base/slices/live/ios/login-auth.md +247 -0
  61. package/knowledge-base/slices/live/live-list.md +82 -0
  62. package/knowledge-base/slices/live/login-auth.md +78 -0
  63. package/package.json +34 -0
  64. package/skills/trtc/SKILL.md +326 -0
  65. package/skills/trtc/room-builder/SKILL.md +138 -0
  66. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
  67. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
  68. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
  69. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
  70. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
  71. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
  72. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
  73. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
  74. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
  75. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
  76. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
  77. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
  78. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
  79. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
  80. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
  81. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
  82. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
  83. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
  84. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
  85. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  86. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  87. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
  88. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
  89. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
  90. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
  91. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
  92. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
  93. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
  94. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
  95. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
  96. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
  97. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
  98. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
  99. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
  100. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
  101. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
  102. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
  103. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
  104. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
  105. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
  106. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
  107. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
  108. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
  109. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
  110. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
  111. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
  112. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
  113. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
  114. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
  115. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
  116. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
  117. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
  118. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
  119. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
  120. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
  121. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
  122. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
  123. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
  124. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
  125. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
  126. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
  127. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
  128. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
  129. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
  130. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
  131. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
  132. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
  133. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
  134. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
  135. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
  136. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
  137. package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
  138. package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
  139. package/skills/trtc-apply/SKILL.md +97 -0
  140. package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
  141. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  142. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  143. package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
  144. package/skills/trtc-docs/SKILL.md +207 -0
  145. package/skills/trtc-onboarding/SKILL.md +839 -0
  146. package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
  147. package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
  148. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
  149. package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
  150. package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
  151. package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
  152. package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
  153. package/skills/trtc-search/SKILL.md +221 -0
  154. package/skills/trtc-topic/SKILL.md +638 -0
  155. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  156. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  157. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  158. package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
  159. package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
  160. package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
  161. package/skills/trtc-topic/references/execution-units.yaml +58 -0
  162. package/skills/trtc-topic/runtime/README.md +50 -0
  163. package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
  164. package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
  165. package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
  166. package/skills/trtc-topic/runtime/package-lock.json +1211 -0
  167. package/skills/trtc-topic/runtime/package.json +13 -0
  168. package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
  169. package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
  170. package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
  171. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  172. package/skills/trtc-topic/scripts/apply.py +581 -0
  173. package/skills/trtc-topic/scripts/finalize_session.py +113 -0
  174. package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
  175. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  176. package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
  177. package/skills/trtc-topic/scripts/next_slice.py +137 -0
  178. package/skills/trtc-topic/tests/README.md +70 -0
  179. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  180. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  182. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  184. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  186. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  187. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  188. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  190. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  191. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  192. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  194. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  196. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/skills/trtc-topic/tests/conftest.py +72 -0
  198. package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
  199. package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
  200. package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
  201. package/skills/trtc-topic/tests/test_gates.py +316 -0
  202. package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
  203. package/skills/trtc-topic/tests/test_state_machine.py +414 -0
  204. package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
  205. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
@@ -0,0 +1,260 @@
1
+ """Regression tests for the session-path resolver shared by all topic CLIs.
2
+
3
+ Background:
4
+ Before commit 1981bac, init_slice_queue.py / next_slice.py / apply.py
5
+ each computed their default `--session` path as `HERE.parents[4] /
6
+ ".trtc-session.yaml"` — i.e. the skill repo root. That's wrong:
7
+ `.trtc-session.yaml` lives in the *user project* (e.g. `demo-0518/`),
8
+ not in the skill repo. When AI ran the script from the user project
9
+ without passing `--session`, it errored with:
10
+
11
+ error: session file not found at <skill-repo>/.trtc-session.yaml
12
+
13
+ Fix: each CLI now resolves the session path the same way the
14
+ PreToolUse hooks already did:
15
+
16
+ 1. --session flag, if given (explicit override)
17
+ 2. $TRTC_SESSION_PATH env var
18
+ 3. $CLAUDE_PROJECT_DIR/.trtc-session.yaml
19
+ 4. ./.trtc-session.yaml (cwd fallback)
20
+
21
+ These tests pin the chain so a future refactor can't silently break it
22
+ again. We exercise the actual subprocess (not the import-level resolver)
23
+ because the bug surfaced at the subprocess boundary — the shipped script
24
+ is what AI invokes.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import subprocess
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ import pytest
33
+ import yaml
34
+
35
+ ROOT = Path(__file__).resolve().parents[1]
36
+ INIT = ROOT / "scripts" / "init_slice_queue.py"
37
+ NEXT = ROOT / "scripts" / "next_slice.py"
38
+ APPLY = ROOT / "scripts" / "apply.py"
39
+
40
+
41
+ def _seed_session(project_root: Path, confirmed_plan=None) -> Path:
42
+ """Drop a minimal .trtc-session.yaml into project_root and return its path."""
43
+ project_root.mkdir(parents=True, exist_ok=True)
44
+ session = project_root / ".trtc-session.yaml"
45
+ session.write_text(
46
+ yaml.safe_dump(
47
+ {
48
+ "schema_version": 1,
49
+ "status": "active",
50
+ "product": "conference",
51
+ "platform": "web",
52
+ "intent": "integrate-scenario",
53
+ "scenario": "general-conference",
54
+ "confirmed_plan": confirmed_plan or ["conference/login-auth"],
55
+ "project_state": {"project_root": str(project_root)},
56
+ },
57
+ sort_keys=False,
58
+ allow_unicode=True,
59
+ )
60
+ )
61
+ return session
62
+
63
+
64
+ def _run(script: Path, *cli_args, cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess:
65
+ """Run a topic CLI with a clean env (so parent-process env vars don't leak in)."""
66
+ base_env = {"PATH": "/usr/bin:/bin"}
67
+ if env:
68
+ base_env.update(env)
69
+ return subprocess.run(
70
+ [sys.executable, str(script), *map(str, cli_args)],
71
+ cwd=str(cwd),
72
+ text=True,
73
+ capture_output=True,
74
+ env=base_env,
75
+ )
76
+
77
+
78
+ # ---------- Resolver chain --------------------------------------------------
79
+
80
+
81
+ class TestSessionResolverChain:
82
+ """The four resolution paths, in priority order."""
83
+
84
+ def test_cwd_fallback_finds_session_in_user_project(self, tmp_path):
85
+ """AI's typical setup: cd to user project, no flag, no env vars.
86
+
87
+ This is the exact bug the user hit on demo-0518. Without the
88
+ cwd fallback, the script looked at the skill repo root and
89
+ errored.
90
+ """
91
+ project = tmp_path / "demo-0518"
92
+ _seed_session(project)
93
+
94
+ # No --session, no env vars, cwd = user project.
95
+ r = _run(INIT, cwd=project)
96
+ assert r.returncode == 0, (
97
+ f"cwd fallback should let init find ./trtc-session.yaml; "
98
+ f"stderr={r.stderr}"
99
+ )
100
+ assert "queue initialised" in r.stdout
101
+
102
+ def test_claude_project_dir_env_resolves_when_cwd_is_elsewhere(self, tmp_path):
103
+ """Claude Code sets $CLAUDE_PROJECT_DIR to the user project root.
104
+
105
+ The CLI must honour that even when AI's cwd is somewhere else
106
+ (e.g. the skill repo, or `/tmp`). We use init_slice_queue here
107
+ because its output is deterministic ("queue initialised — N slices");
108
+ next_slice.py status would also work but emits "queue not
109
+ initialised" — which would still prove session resolution worked,
110
+ but the assertion is fuzzier.
111
+ """
112
+ project = tmp_path / "user-proj"
113
+ _seed_session(project)
114
+
115
+ r = _run(
116
+ INIT,
117
+ cwd=tmp_path, # not the project
118
+ env={"CLAUDE_PROJECT_DIR": str(project)},
119
+ )
120
+ assert r.returncode == 0, r.stderr
121
+ assert "queue initialised" in r.stdout
122
+ assert "conference/login-auth" in r.stdout
123
+
124
+ def test_trtc_session_path_env_wins_over_claude_project_dir(self, tmp_path):
125
+ """Explicit env var beats $CLAUDE_PROJECT_DIR.
126
+
127
+ Useful for tests, custom setups, multi-project workflows.
128
+ Strategy: point $TRTC_SESSION_PATH at a real session and
129
+ $CLAUDE_PROJECT_DIR at a project that *has no session file at all*.
130
+ If $CLAUDE_PROJECT_DIR is consulted first the script will error
131
+ with "session file not found"; if $TRTC_SESSION_PATH wins, init
132
+ succeeds.
133
+ """
134
+ real_project = tmp_path / "real"
135
+ _seed_session(real_project)
136
+ bogus_project = tmp_path / "bogus"
137
+ bogus_project.mkdir() # exists but contains no .trtc-session.yaml
138
+
139
+ r = _run(
140
+ INIT,
141
+ cwd=tmp_path,
142
+ env={
143
+ "TRTC_SESSION_PATH": str(real_project / ".trtc-session.yaml"),
144
+ "CLAUDE_PROJECT_DIR": str(bogus_project),
145
+ },
146
+ )
147
+ assert r.returncode == 0, (
148
+ f"$TRTC_SESSION_PATH should take precedence; stderr={r.stderr}"
149
+ )
150
+ assert "queue initialised" in r.stdout
151
+
152
+ def test_explicit_session_flag_wins_over_envs(self, tmp_path):
153
+ """--session is the highest-priority override."""
154
+ real_project = tmp_path / "real"
155
+ _seed_session(real_project)
156
+
157
+ r = _run(
158
+ NEXT,
159
+ "--session",
160
+ str(real_project / ".trtc-session.yaml"),
161
+ "status",
162
+ cwd=tmp_path,
163
+ env={
164
+ "TRTC_SESSION_PATH": str(tmp_path / "nonexistent.yaml"),
165
+ "CLAUDE_PROJECT_DIR": str(tmp_path / "also-nonexistent"),
166
+ },
167
+ )
168
+ assert r.returncode == 0, (
169
+ f"--session flag should override env vars; stderr={r.stderr}"
170
+ )
171
+
172
+ def test_no_session_anywhere_errors_with_actionable_hint(self, tmp_path):
173
+ """When nothing resolves, the error must tell the user what to fix.
174
+
175
+ This replaces the silent skill-repo lookup that AI had no way to
176
+ diagnose.
177
+ """
178
+ empty_dir = tmp_path / "empty"
179
+ empty_dir.mkdir()
180
+
181
+ r = _run(NEXT, "status", cwd=empty_dir)
182
+ assert r.returncode == 1
183
+ # The error must point at the resolved path AND tell the user how
184
+ # to fix it. We check both halves so paraphrasing the message
185
+ # later doesn't drop the actionable bit.
186
+ assert "session file not found" in r.stderr
187
+ assert (
188
+ "CLAUDE_PROJECT_DIR" in r.stderr
189
+ or "TRTC_SESSION_PATH" in r.stderr
190
+ or "cd to the user project" in r.stderr
191
+ ), (
192
+ f"stderr should hint how to point the script at the session; "
193
+ f"got: {r.stderr}"
194
+ )
195
+
196
+
197
+ # ---------- All three CLIs honour the chain ---------------------------------
198
+
199
+
200
+ class TestAllCLIsUseSameResolver:
201
+ """Sanity check: init_slice_queue / next_slice / apply all behave the same.
202
+
203
+ They each reimplement `_resolve_session_path` (no shared module yet)
204
+ so we test all three to catch the case where one drifts.
205
+ """
206
+
207
+ @pytest.mark.parametrize("script,subcmd", [
208
+ (INIT, []),
209
+ (NEXT, ["status"]),
210
+ ])
211
+ def test_cli_finds_session_via_cwd(self, script, subcmd, tmp_path):
212
+ project = tmp_path / "user-proj"
213
+ _seed_session(project)
214
+ r = _run(script, *subcmd, cwd=project)
215
+ assert r.returncode == 0, f"{script.name}: {r.stderr}"
216
+
217
+ @pytest.mark.parametrize("script,subcmd", [
218
+ (INIT, []),
219
+ (NEXT, ["status"]),
220
+ ])
221
+ def test_cli_finds_session_via_claude_project_dir(self, script, subcmd, tmp_path):
222
+ project = tmp_path / "user-proj"
223
+ _seed_session(project)
224
+ r = _run(
225
+ script,
226
+ *subcmd,
227
+ cwd=tmp_path,
228
+ env={"CLAUDE_PROJECT_DIR": str(project)},
229
+ )
230
+ assert r.returncode == 0, f"{script.name}: {r.stderr}"
231
+
232
+ def test_apply_finds_session_via_cwd(self, tmp_path):
233
+ """apply.py needs more setup (state machine seeded to code_written)
234
+ before it'll do anything useful, so it gets its own test rather
235
+ than the parametrised pair above."""
236
+ project = tmp_path / "user-proj"
237
+ session = _seed_session(project)
238
+ # Seed state to code_written so apply.py reaches the project scan.
239
+ sys.path.insert(0, str(ROOT / "scripts" / "lib"))
240
+ try:
241
+ import state_machine
242
+ state_machine.init_queue(session)
243
+ state_machine.advance(session, "mark_slice_read")
244
+ state_machine.advance(session, "mark_code_written")
245
+ finally:
246
+ sys.path.pop(0)
247
+
248
+ # apply.py: no --session, no env, cwd = project. Should resolve
249
+ # session via cwd fallback. The actual apply result will be fail
250
+ # (no src/) — but rc != 2 (usage error), and importantly it does
251
+ # NOT error with "session file not found".
252
+ r = _run(APPLY, "--slice", "conference/login-auth", cwd=project)
253
+ assert "session file not found" not in r.stderr, (
254
+ f"apply.py should resolve session via cwd; got: {r.stderr}"
255
+ )
256
+ # rc 1 = apply fail (expected — no src/), rc 0 = apply pass
257
+ # (unexpected here), rc 2 = usage error (regression).
258
+ assert r.returncode in (0, 1), (
259
+ f"unexpected rc={r.returncode}; stderr={r.stderr}"
260
+ )
@@ -0,0 +1,414 @@
1
+ """Tests for the slice-loop state machine.
2
+
3
+ Contract:
4
+ init_queue(session_path) -> None
5
+ Reads `confirmed_plan` from session, writes:
6
+ execution_queue: [{id, type, slices, status}, ...]
7
+ current_execution_index: 0
8
+ current_execution_state: not_started
9
+ Raises RuntimeError if confirmed_plan missing/empty.
10
+ Idempotent: repeat calls with same plan are no-ops; calls with a
11
+ different plan raise RuntimeError (the queue is frozen once set).
12
+
13
+ current_slice(session_path) -> (index, slice_id, state)
14
+ Returns the cursor. (None, None, None) if queue not initialised
15
+ (PreToolUse hooks rely on this to detect "not in topic flow").
16
+
17
+ advance(session_path, transition) -> new_state
18
+ Allowed transitions:
19
+ not_started --mark_slice_read--> slice_read
20
+ slice_read --mark_code_written--> code_written
21
+ code_written --mark_apply_passed--> apply_passed
22
+ code_written --mark_apply_failed--> apply_failed
23
+ apply_failed --mark_code_written--> code_written
24
+ apply_passed --mark_user_confirmed--> not_started (index += 1)
25
+
26
+ At the end of the queue, advancing user_confirmed sets state to
27
+ all_done. Further transitions raise RuntimeError.
28
+
29
+ Any other (state, transition) pair raises RuntimeError.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ import pytest
37
+ import yaml
38
+
39
+ # state_machine lives in topic/scripts/lib/
40
+ STATE_MACHINE_DIR = Path(__file__).resolve().parents[1] / "scripts" / "lib"
41
+ sys.path.insert(0, str(STATE_MACHINE_DIR))
42
+ import state_machine # noqa: E402
43
+ sys.path.pop(0)
44
+
45
+
46
+ # ---------- init_queue ----------
47
+
48
+ class TestInitQueue:
49
+ def test_writes_queue_index_and_state(self, session_factory):
50
+ path = session_factory()
51
+ state_machine.init_queue(path)
52
+ data = yaml.safe_load(path.read_text())
53
+ assert data["execution_queue"] == [
54
+ {"id": "conference/login-auth", "type": "slice", "title": "conference/login-auth", "status": "pending", "slices": ["conference/login-auth"]},
55
+ {"id": "conference/room-lifecycle", "type": "slice", "title": "conference/room-lifecycle", "status": "pending", "slices": ["conference/room-lifecycle"]},
56
+ {"id": "conference/participant-list", "type": "slice", "title": "conference/participant-list", "status": "pending", "slices": ["conference/participant-list"]},
57
+ {"id": "conference/video-layout", "type": "slice", "title": "conference/video-layout", "status": "pending", "slices": ["conference/video-layout"]},
58
+ {"id": "conference/device-control", "type": "slice", "title": "conference/device-control", "status": "pending", "slices": ["conference/device-control"]},
59
+ {"id": "conference/network-quality", "type": "slice", "title": "conference/network-quality", "status": "pending", "slices": ["conference/network-quality"]},
60
+ ]
61
+ assert data["current_execution_index"] == 0
62
+ assert data["current_execution_state"] == "not_started"
63
+
64
+ def test_raises_when_confirmed_plan_missing(self, session_factory):
65
+ path = session_factory(confirmed_plan=None)
66
+ with pytest.raises(RuntimeError, match="confirmed_plan"):
67
+ state_machine.init_queue(path)
68
+
69
+ def test_raises_when_confirmed_plan_empty(self, session_factory):
70
+ path = session_factory(confirmed_plan=[])
71
+ with pytest.raises(RuntimeError, match="confirmed_plan"):
72
+ state_machine.init_queue(path)
73
+
74
+ def test_idempotent_when_plan_unchanged(self, session_factory):
75
+ path = session_factory()
76
+ state_machine.init_queue(path)
77
+ # Advance once, so the cursor moves
78
+ state_machine.advance(path, "mark_slice_read")
79
+ # Calling init_queue again should NOT reset the cursor —
80
+ # the same plan is already installed.
81
+ state_machine.init_queue(path)
82
+ data = yaml.safe_load(path.read_text())
83
+ assert data["current_execution_state"] == "slice_read"
84
+ assert data["current_execution_index"] == 0
85
+
86
+ def test_raises_when_replan_differs(self, session_factory):
87
+ path = session_factory()
88
+ state_machine.init_queue(path)
89
+ # Mutate confirmed_plan and try to re-init — must refuse.
90
+ data = yaml.safe_load(path.read_text())
91
+ data["confirmed_plan"] = ["conference/login-auth"]
92
+ path.write_text(yaml.safe_dump(data, sort_keys=False))
93
+ with pytest.raises(RuntimeError, match="frozen|differs"):
94
+ state_machine.init_queue(path)
95
+
96
+
97
+ class TestInitDeliveryUnitQueue:
98
+ def test_unit_mode_groups_confirmed_plan_without_adding_slices(self, session_factory):
99
+ path = session_factory(
100
+ execution_granularity="unit",
101
+ confirmed_plan=[
102
+ "conference/login-auth",
103
+ "conference/room-lifecycle",
104
+ "conference/video-layout",
105
+ "conference/device-control",
106
+ "conference/room-chat",
107
+ ],
108
+ )
109
+ state_machine.init_queue(path)
110
+ data = yaml.safe_load(path.read_text())
111
+
112
+ assert [step["slices"] for step in data["execution_queue"]] == [
113
+ ["conference/login-auth", "conference/room-lifecycle"],
114
+ ["conference/video-layout"],
115
+ ["conference/device-control"],
116
+ ["conference/room-chat"],
117
+ ]
118
+ flattened = [
119
+ sid
120
+ for step in data["execution_queue"]
121
+ for sid in step["slices"]
122
+ ]
123
+ assert flattened == data["confirmed_plan"]
124
+ assert data["current_execution_index"] == 0
125
+ assert data["current_execution_state"] == "not_started"
126
+
127
+ def test_unit_mode_respects_declared_units_and_fills_missing_singletons(self, session_factory):
128
+ path = session_factory(
129
+ execution_granularity="unit",
130
+ confirmed_plan=[
131
+ "conference/login-auth",
132
+ "conference/room-lifecycle",
133
+ "conference/screen-share",
134
+ ],
135
+ delivery_units=[
136
+ {
137
+ "id": "foundation",
138
+ "title": "基础链路",
139
+ "slices": ["conference/login-auth", "conference/room-lifecycle"],
140
+ }
141
+ ],
142
+ )
143
+ state_machine.init_queue(path)
144
+ data = yaml.safe_load(path.read_text())
145
+ assert [step["id"] for step in data["execution_queue"]] == [
146
+ "foundation",
147
+ "conference/screen-share",
148
+ ]
149
+ assert data["execution_queue"][1]["slices"] == ["conference/screen-share"]
150
+
151
+ def test_unit_mode_without_scenario_config_uses_singletons(self, session_factory):
152
+ path = session_factory(
153
+ scenario="custom-scenario-without-unit-config",
154
+ execution_granularity="unit",
155
+ confirmed_plan=[
156
+ "conference/login-auth",
157
+ "conference/room-lifecycle",
158
+ ],
159
+ )
160
+ state_machine.init_queue(path)
161
+ data = yaml.safe_load(path.read_text())
162
+ assert [step["type"] for step in data["execution_queue"]] == ["slice", "slice"]
163
+ assert [step["slices"] for step in data["execution_queue"]] == [
164
+ ["conference/login-auth"],
165
+ ["conference/room-lifecycle"],
166
+ ]
167
+
168
+
169
+ # ---------- current_slice ----------
170
+
171
+ class TestCurrentSlice:
172
+ def test_returns_none_tuple_before_init(self, session_factory):
173
+ path = session_factory()
174
+ assert state_machine.current_slice(path) == (None, None, None)
175
+
176
+ def test_returns_first_slice_after_init(self, session_factory):
177
+ path = session_factory()
178
+ state_machine.init_queue(path)
179
+ assert state_machine.current_slice(path) == (
180
+ 0,
181
+ "conference/login-auth",
182
+ "not_started",
183
+ )
184
+
185
+ def test_returns_none_when_session_missing(self, tmp_path):
186
+ path = tmp_path / "missing.yaml"
187
+ assert state_machine.current_slice(path) == (None, None, None)
188
+
189
+ def test_current_scope_reports_unit_slice_ids(self, session_factory):
190
+ path = session_factory(
191
+ execution_granularity="unit",
192
+ confirmed_plan=["conference/login-auth", "conference/room-lifecycle"],
193
+ )
194
+ state_machine.init_queue(path)
195
+ scope = state_machine.current_scope(path)
196
+ assert scope["kind"] == "unit"
197
+ assert scope["id"] == "foundation"
198
+ assert scope["slice_ids"] == [
199
+ "conference/login-auth",
200
+ "conference/room-lifecycle",
201
+ ]
202
+
203
+
204
+ # ---------- advance ----------
205
+
206
+ class TestAdvanceHappyPath:
207
+ def test_full_cycle_advances_index(self, session_factory):
208
+ path = session_factory()
209
+ state_machine.init_queue(path)
210
+
211
+ assert state_machine.advance(path, "mark_slice_read") == "slice_read"
212
+ assert state_machine.advance(path, "mark_code_written") == "code_written"
213
+ assert state_machine.advance(path, "mark_apply_passed") == "apply_passed"
214
+ assert state_machine.advance(path, "mark_user_confirmed") == "not_started"
215
+
216
+ idx, sid, st = state_machine.current_slice(path)
217
+ assert idx == 1
218
+ assert sid == "conference/room-lifecycle"
219
+ assert st == "not_started"
220
+
221
+ def test_apply_failed_then_retry(self, session_factory):
222
+ path = session_factory()
223
+ state_machine.init_queue(path)
224
+ state_machine.advance(path, "mark_slice_read")
225
+ state_machine.advance(path, "mark_code_written")
226
+ assert state_machine.advance(path, "mark_apply_failed") == "apply_failed"
227
+ # Retry: regenerate code, mark code_written again
228
+ assert state_machine.advance(path, "mark_code_written") == "code_written"
229
+ assert state_machine.advance(path, "mark_apply_passed") == "apply_passed"
230
+
231
+ def test_reaches_all_done_on_last_confirm(self, session_factory):
232
+ path = session_factory(confirmed_plan=["conference/login-auth"])
233
+ state_machine.init_queue(path)
234
+ state_machine.advance(path, "mark_slice_read")
235
+ state_machine.advance(path, "mark_code_written")
236
+ state_machine.advance(path, "mark_apply_passed")
237
+ # Last slice — confirming should reach all_done, not advance index.
238
+ assert state_machine.advance(path, "mark_user_confirmed") == "all_done"
239
+
240
+ def test_marks_completed_slices_in_queue(self, session_factory):
241
+ """execution_queue[i].status flips pending → done as we confirm each."""
242
+ path = session_factory()
243
+ state_machine.init_queue(path)
244
+ state_machine.advance(path, "mark_slice_read")
245
+ state_machine.advance(path, "mark_code_written")
246
+ state_machine.advance(path, "mark_apply_passed")
247
+ state_machine.advance(path, "mark_user_confirmed")
248
+ data = yaml.safe_load(path.read_text())
249
+ assert data["execution_queue"][0]["status"] == "done"
250
+ assert data["execution_queue"][1]["status"] == "pending"
251
+
252
+ def test_unit_cycle_marks_all_slices_in_unit_done(self, session_factory):
253
+ path = session_factory(
254
+ execution_granularity="unit",
255
+ confirmed_plan=[
256
+ "conference/login-auth",
257
+ "conference/room-lifecycle",
258
+ "conference/room-chat",
259
+ ],
260
+ )
261
+ state_machine.init_queue(path)
262
+ state_machine.advance(path, "mark_slice_read")
263
+ state_machine.advance(path, "mark_code_written")
264
+ state_machine.advance(path, "mark_apply_passed")
265
+ assert state_machine.advance(path, "mark_user_confirmed") == "not_started"
266
+
267
+ data = yaml.safe_load(path.read_text())
268
+ assert data["execution_queue"][0]["status"] == "done"
269
+ assert data["execution_queue"][1]["status"] == "pending"
270
+
271
+
272
+ class TestAdvanceIllegalTransitions:
273
+ @pytest.mark.parametrize("bad_transition", [
274
+ # From not_started: only mark_slice_read is legal.
275
+ "mark_code_written",
276
+ "mark_apply_passed",
277
+ "mark_apply_failed",
278
+ "mark_user_confirmed",
279
+ ])
280
+ def test_not_started_rejects(self, session_factory, bad_transition):
281
+ path = session_factory()
282
+ state_machine.init_queue(path)
283
+ with pytest.raises(RuntimeError):
284
+ state_machine.advance(path, bad_transition)
285
+
286
+ def test_slice_read_rejects_apply(self, session_factory):
287
+ path = session_factory()
288
+ state_machine.init_queue(path)
289
+ state_machine.advance(path, "mark_slice_read")
290
+ with pytest.raises(RuntimeError):
291
+ state_machine.advance(path, "mark_apply_passed")
292
+
293
+ def test_apply_passed_rejects_repeat_apply(self, session_factory):
294
+ path = session_factory()
295
+ state_machine.init_queue(path)
296
+ state_machine.advance(path, "mark_slice_read")
297
+ state_machine.advance(path, "mark_code_written")
298
+ state_machine.advance(path, "mark_apply_passed")
299
+ with pytest.raises(RuntimeError):
300
+ state_machine.advance(path, "mark_apply_passed")
301
+
302
+ def test_unknown_transition_name(self, session_factory):
303
+ path = session_factory()
304
+ state_machine.init_queue(path)
305
+ with pytest.raises(RuntimeError, match="unknown"):
306
+ state_machine.advance(path, "mark_lunch_break")
307
+
308
+ def test_advance_before_init_raises(self, session_factory):
309
+ path = session_factory()
310
+ with pytest.raises(RuntimeError, match="not initialised|init_queue"):
311
+ state_machine.advance(path, "mark_slice_read")
312
+
313
+ def test_advance_after_all_done_raises(self, session_factory):
314
+ path = session_factory(confirmed_plan=["conference/login-auth"])
315
+ state_machine.init_queue(path)
316
+ state_machine.advance(path, "mark_slice_read")
317
+ state_machine.advance(path, "mark_code_written")
318
+ state_machine.advance(path, "mark_apply_passed")
319
+ state_machine.advance(path, "mark_user_confirmed") # all_done
320
+ with pytest.raises(RuntimeError, match="all_done|finished|complete"):
321
+ state_machine.advance(path, "mark_slice_read")
322
+
323
+
324
+ # ---------- Evidence auto-delete on user confirm ----------
325
+ #
326
+ # Successful slices' evidence JSON is deleted by mark_user_confirmed so the
327
+ # .trtc-apply-evidence/ directory ends a clean integration empty. Failed
328
+ # evidence is preserved (the slice still needs work; the next apply.py call
329
+ # will overwrite it).
330
+
331
+ import json
332
+
333
+
334
+ class TestEvidenceCleanup:
335
+ def _evidence_path(self, session_path, slice_id):
336
+ return session_path.parent / ".trtc-apply-evidence" / (
337
+ slice_id.replace("/", "__") + ".json"
338
+ )
339
+
340
+ def _seed_evidence(self, session_path, slice_id, status="pass"):
341
+ ev = self._evidence_path(session_path, slice_id)
342
+ ev.parent.mkdir(parents=True, exist_ok=True)
343
+ ev.write_text(json.dumps({"slice_id": slice_id, "status": status}))
344
+ return ev
345
+
346
+ def test_mark_user_confirmed_deletes_evidence(self, session_factory):
347
+ path = session_factory()
348
+ state_machine.init_queue(path)
349
+ state_machine.advance(path, "mark_slice_read")
350
+ state_machine.advance(path, "mark_code_written")
351
+ state_machine.advance(path, "mark_apply_passed")
352
+
353
+ ev = self._seed_evidence(path, "conference/login-auth")
354
+ assert ev.exists()
355
+
356
+ state_machine.advance(path, "mark_user_confirmed")
357
+ assert not ev.exists(), "evidence should be deleted on confirm"
358
+
359
+ def test_mark_user_confirmed_handles_missing_evidence(self, session_factory):
360
+ """No evidence file → confirm still succeeds (no exception)."""
361
+ path = session_factory()
362
+ state_machine.init_queue(path)
363
+ state_machine.advance(path, "mark_slice_read")
364
+ state_machine.advance(path, "mark_code_written")
365
+ state_machine.advance(path, "mark_apply_passed")
366
+
367
+ # Don't seed an evidence file.
368
+ ev = self._evidence_path(path, "conference/login-auth")
369
+ assert not ev.exists()
370
+
371
+ new_state = state_machine.advance(path, "mark_user_confirmed")
372
+ assert new_state == "not_started"
373
+
374
+ def test_apply_failed_evidence_not_deleted(self, session_factory):
375
+ """apply_failed → mark_code_written retry → next apply overwrites.
376
+
377
+ We don't get to mark_user_confirmed in this path, so evidence stays.
378
+ Confirms the cleanup is bound to confirm, not to advance-in-general.
379
+ """
380
+ path = session_factory()
381
+ state_machine.init_queue(path)
382
+ state_machine.advance(path, "mark_slice_read")
383
+ state_machine.advance(path, "mark_code_written")
384
+ state_machine.advance(path, "mark_apply_failed")
385
+
386
+ ev = self._seed_evidence(path, "conference/login-auth", status="fail")
387
+ assert ev.exists()
388
+
389
+ # Retry: regenerate code → mark_code_written. Evidence still there.
390
+ state_machine.advance(path, "mark_code_written")
391
+ assert ev.exists(), "fail-evidence must persist across retries"
392
+
393
+ def test_evidence_deleted_for_each_slice_in_sequence(self, session_factory):
394
+ """Two slices, both confirmed → both evidence files gone."""
395
+ path = session_factory(
396
+ confirmed_plan=["conference/login-auth", "conference/room-lifecycle"]
397
+ )
398
+ state_machine.init_queue(path)
399
+
400
+ # Slice 0
401
+ state_machine.advance(path, "mark_slice_read")
402
+ state_machine.advance(path, "mark_code_written")
403
+ state_machine.advance(path, "mark_apply_passed")
404
+ ev0 = self._seed_evidence(path, "conference/login-auth")
405
+ state_machine.advance(path, "mark_user_confirmed")
406
+ assert not ev0.exists()
407
+
408
+ # Slice 1
409
+ state_machine.advance(path, "mark_slice_read")
410
+ state_machine.advance(path, "mark_code_written")
411
+ state_machine.advance(path, "mark_apply_passed")
412
+ ev1 = self._seed_evidence(path, "conference/room-lifecycle")
413
+ state_machine.advance(path, "mark_user_confirmed")
414
+ assert not ev1.exists()