@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh.md +173 -0
- package/bin/cli.js +434 -0
- package/knowledge-base/index.yaml +454 -0
- package/knowledge-base/platform-slice-template.md +233 -0
- package/knowledge-base/scenario-spec.md +350 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
- package/knowledge-base/slice-spec.md +546 -0
- package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
- package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
- package/knowledge-base/slices/conference/web/device-control.md +338 -0
- package/knowledge-base/slices/conference/web/login-auth.md +261 -0
- package/knowledge-base/slices/conference/web/network-quality.md +190 -0
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
- package/knowledge-base/slices/conference/web/participant-list.md +238 -0
- package/knowledge-base/slices/conference/web/participant-management.md +718 -0
- package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
- package/knowledge-base/slices/conference/web/room-call.md +213 -0
- package/knowledge-base/slices/conference/web/room-chat.md +426 -0
- package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
- package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
- package/knowledge-base/slices/conference/web/screen-share.md +211 -0
- package/knowledge-base/slices/conference/web/video-layout.md +675 -0
- package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
- package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
- package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
- package/knowledge-base/slices/live/anchor-preview.md +90 -0
- package/knowledge-base/slices/live/anchor-room-config.md +104 -0
- package/knowledge-base/slices/live/audience-list.md +86 -0
- package/knowledge-base/slices/live/audience-manage.md +92 -0
- package/knowledge-base/slices/live/audience-watch.md +85 -0
- package/knowledge-base/slices/live/audio.md +116 -0
- package/knowledge-base/slices/live/barrage.md +88 -0
- package/knowledge-base/slices/live/beauty.md +99 -0
- package/knowledge-base/slices/live/coguest-apply.md +105 -0
- package/knowledge-base/slices/live/device-control.md +91 -0
- package/knowledge-base/slices/live/error-codes.md +167 -0
- package/knowledge-base/slices/live/gift.md +84 -0
- package/knowledge-base/slices/live/ios/.gitkeep +0 -0
- package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
- package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
- package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
- package/knowledge-base/slices/live/ios/audience-list.md +353 -0
- package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
- package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
- package/knowledge-base/slices/live/ios/audio.md +373 -0
- package/knowledge-base/slices/live/ios/barrage.md +285 -0
- package/knowledge-base/slices/live/ios/beauty.md +323 -0
- package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
- package/knowledge-base/slices/live/ios/device-control.md +286 -0
- package/knowledge-base/slices/live/ios/error-codes.md +270 -0
- package/knowledge-base/slices/live/ios/gift.md +315 -0
- package/knowledge-base/slices/live/ios/live-list.md +269 -0
- package/knowledge-base/slices/live/ios/login-auth.md +247 -0
- package/knowledge-base/slices/live/live-list.md +82 -0
- package/knowledge-base/slices/live/login-auth.md +78 -0
- package/package.json +34 -0
- package/skills/trtc/SKILL.md +326 -0
- package/skills/trtc/room-builder/SKILL.md +138 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
- package/skills/trtc-apply/SKILL.md +97 -0
- package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
- package/skills/trtc-docs/SKILL.md +207 -0
- package/skills/trtc-onboarding/SKILL.md +839 -0
- package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
- package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
- package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
- package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
- package/skills/trtc-search/SKILL.md +221 -0
- package/skills/trtc-topic/SKILL.md +638 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
- package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
- package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
- package/skills/trtc-topic/references/execution-units.yaml +58 -0
- package/skills/trtc-topic/runtime/README.md +50 -0
- package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
- package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
- package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
- package/skills/trtc-topic/runtime/package-lock.json +1211 -0
- package/skills/trtc-topic/runtime/package.json +13 -0
- package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
- package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
- package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/apply.py +581 -0
- package/skills/trtc-topic/scripts/finalize_session.py +113 -0
- package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
- package/skills/trtc-topic/scripts/next_slice.py +137 -0
- package/skills/trtc-topic/tests/README.md +70 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/conftest.py +72 -0
- package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
- package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
- package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
- package/skills/trtc-topic/tests/test_gates.py +316 -0
- package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
- package/skills/trtc-topic/tests/test_state_machine.py +414 -0
- package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
- 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()
|