@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,305 @@
|
|
|
1
|
+
"""End-to-end test: run the full slice loop through every component.
|
|
2
|
+
|
|
3
|
+
Simulates one full slice's lifecycle by exercising the actual CLI scripts
|
|
4
|
+
and gate scripts, not the in-process state_machine module. This catches
|
|
5
|
+
regressions where the moving parts agree on the field shape in unit tests
|
|
6
|
+
but disagree in integration.
|
|
7
|
+
|
|
8
|
+
Flow:
|
|
9
|
+
|
|
10
|
+
init_slice_queue -> next_slice status (cursor at execution step 0)
|
|
11
|
+
→ gate_slice_read (current slice OK, next slice BLOCKED)
|
|
12
|
+
→ next_slice advance mark_slice_read
|
|
13
|
+
→ gate_slice_write (project file ALLOWED)
|
|
14
|
+
→ write code
|
|
15
|
+
→ next_slice advance mark_code_written
|
|
16
|
+
→ stop hook (BLOCKED — apply hasn't run)
|
|
17
|
+
→ apply.py (PASS → state = apply_passed)
|
|
18
|
+
→ stop hook (ALLOWED)
|
|
19
|
+
→ gate_slice_write (BLOCKED — must wait for confirm)
|
|
20
|
+
→ next_slice advance mark_user_confirmed
|
|
21
|
+
→ next_slice status (cursor at slice 1)
|
|
22
|
+
→ gate_slice_read (slice 1 OK, slice 0 BLOCKED)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import pytest
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
35
|
+
INIT = ROOT / "scripts" / "init_slice_queue.py"
|
|
36
|
+
NEXT = ROOT / "scripts" / "next_slice.py"
|
|
37
|
+
APPLY = ROOT / "scripts" / "apply.py"
|
|
38
|
+
GATE_READ = ROOT / "guardrails" / "gate_slice_read.py"
|
|
39
|
+
GATE_WRITE = ROOT / "guardrails" / "gate_slice_write.py"
|
|
40
|
+
STOP = ROOT / "guardrails" / "stop_require_apply_evidence.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_cmd(*args, **kwargs) -> subprocess.CompletedProcess:
|
|
44
|
+
return subprocess.run(
|
|
45
|
+
[sys.executable, *map(str, args)],
|
|
46
|
+
text=True,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
**kwargs,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _gate(script: Path, payload: dict, session_path: Path) -> subprocess.CompletedProcess:
|
|
53
|
+
return subprocess.run(
|
|
54
|
+
[sys.executable, str(script)],
|
|
55
|
+
input=json.dumps(payload),
|
|
56
|
+
text=True,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _stop(session_path: Path) -> subprocess.CompletedProcess:
|
|
63
|
+
return subprocess.run(
|
|
64
|
+
[sys.executable, str(STOP)],
|
|
65
|
+
text=True,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Same VUE we used in test_apply_cli.py — wires up the login-auth entry
|
|
72
|
+
# (useLoginState), so the entry-symbol gate passes.
|
|
73
|
+
_PASSING_LOGIN_VUE = '''<template><div /></template>
|
|
74
|
+
<script setup lang="ts">
|
|
75
|
+
import { useLoginState, LoginEvent } from "@trtc/tuikit-atomicx-vue3";
|
|
76
|
+
const { login, setSelfInfo, subscribeEvent } = useLoginState();
|
|
77
|
+
await login({ sdkAppId: 0, userId: "u", userSig: "x", scene: 5001 });
|
|
78
|
+
setSelfInfo({ nickName: "Alice" });
|
|
79
|
+
subscribeEvent(LoginEvent.onLoginExpired, () => {});
|
|
80
|
+
subscribeEvent(LoginEvent.onKickedOffline, () => {});
|
|
81
|
+
</script>
|
|
82
|
+
'''
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_full_slice_loop_through_real_scripts(session_factory, project_factory):
|
|
86
|
+
session = session_factory(
|
|
87
|
+
confirmed_plan=["conference/login-auth", "conference/room-lifecycle"]
|
|
88
|
+
)
|
|
89
|
+
proj = project_factory()
|
|
90
|
+
target = proj / "src" / "views" / "MeetingRoom.vue"
|
|
91
|
+
|
|
92
|
+
# 1. init queue.
|
|
93
|
+
r = _run_cmd(INIT, "--session", session)
|
|
94
|
+
assert r.returncode == 0, r.stderr
|
|
95
|
+
data = yaml.safe_load(session.read_text())
|
|
96
|
+
assert data["current_execution_index"] == 0
|
|
97
|
+
assert data["current_execution_state"] == "not_started"
|
|
98
|
+
|
|
99
|
+
# 2. status.
|
|
100
|
+
r = _run_cmd(NEXT, "--session", session, "--json", "status")
|
|
101
|
+
assert r.returncode == 0
|
|
102
|
+
st = json.loads(r.stdout)
|
|
103
|
+
assert st["current_slice_id"] == "conference/login-auth"
|
|
104
|
+
assert st["state"] == "not_started"
|
|
105
|
+
|
|
106
|
+
# 3. read gate: current slice OK.
|
|
107
|
+
r = _gate(GATE_READ, {
|
|
108
|
+
"tool_name": "Read",
|
|
109
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
|
|
110
|
+
}, session)
|
|
111
|
+
assert r.returncode == 0, r.stderr
|
|
112
|
+
|
|
113
|
+
# 3b. read gate: next slice BLOCKED.
|
|
114
|
+
r = _gate(GATE_READ, {
|
|
115
|
+
"tool_name": "Read",
|
|
116
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
117
|
+
}, session)
|
|
118
|
+
assert r.returncode == 2
|
|
119
|
+
assert "current slice" in r.stderr.lower() or "login-auth" in r.stderr
|
|
120
|
+
|
|
121
|
+
# 4. write gate: not_started → BLOCKED for project files.
|
|
122
|
+
r = _gate(GATE_WRITE, {
|
|
123
|
+
"tool_name": "Write",
|
|
124
|
+
"tool_input": {"file_path": str(target)},
|
|
125
|
+
}, session)
|
|
126
|
+
assert r.returncode == 2
|
|
127
|
+
|
|
128
|
+
# 5. advance mark_slice_read.
|
|
129
|
+
r = _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read")
|
|
130
|
+
assert r.returncode == 0, r.stderr
|
|
131
|
+
|
|
132
|
+
# 6. write gate: now ALLOWED.
|
|
133
|
+
r = _gate(GATE_WRITE, {
|
|
134
|
+
"tool_name": "Write",
|
|
135
|
+
"tool_input": {"file_path": str(target)},
|
|
136
|
+
}, session)
|
|
137
|
+
assert r.returncode == 0, r.stderr
|
|
138
|
+
|
|
139
|
+
# Actually write the file.
|
|
140
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
target.write_text(_PASSING_LOGIN_VUE)
|
|
142
|
+
|
|
143
|
+
# 7. advance mark_code_written.
|
|
144
|
+
r = _run_cmd(NEXT, "--session", session, "advance", "mark_code_written")
|
|
145
|
+
assert r.returncode == 0, r.stderr
|
|
146
|
+
|
|
147
|
+
# 8. stop hook: BLOCKED, apply hasn't run.
|
|
148
|
+
r = _stop(session)
|
|
149
|
+
assert r.returncode == 2
|
|
150
|
+
assert "apply" in r.stderr.lower()
|
|
151
|
+
|
|
152
|
+
# 9. apply.py: pass.
|
|
153
|
+
r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
|
|
154
|
+
assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}"
|
|
155
|
+
|
|
156
|
+
# State must be apply_passed.
|
|
157
|
+
data = yaml.safe_load(session.read_text())
|
|
158
|
+
assert data["current_execution_state"] == "apply_passed"
|
|
159
|
+
|
|
160
|
+
# 10. stop hook: ALLOWED now.
|
|
161
|
+
r = _stop(session)
|
|
162
|
+
assert r.returncode == 0
|
|
163
|
+
|
|
164
|
+
# 11. write gate: apply_passed → BLOCKED (must confirm first).
|
|
165
|
+
r = _gate(GATE_WRITE, {
|
|
166
|
+
"tool_name": "Write",
|
|
167
|
+
"tool_input": {"file_path": str(target)},
|
|
168
|
+
}, session)
|
|
169
|
+
assert r.returncode == 2
|
|
170
|
+
assert "apply_passed" in r.stderr or "confirm" in r.stderr.lower()
|
|
171
|
+
|
|
172
|
+
# 11b. evidence file is present BEFORE confirmation (apply.py wrote it).
|
|
173
|
+
ev_dir = session.parent / ".trtc-apply-evidence"
|
|
174
|
+
ev_files = list(ev_dir.glob("*.json"))
|
|
175
|
+
assert len(ev_files) == 1
|
|
176
|
+
ev = json.loads(ev_files[0].read_text())
|
|
177
|
+
assert ev["status"] == "pass"
|
|
178
|
+
assert ev["slice_id"] == "conference/login-auth"
|
|
179
|
+
|
|
180
|
+
# 12. user confirms.
|
|
181
|
+
r = _run_cmd(NEXT, "--session", session, "advance", "mark_user_confirmed")
|
|
182
|
+
assert r.returncode == 0, r.stderr
|
|
183
|
+
|
|
184
|
+
# 13. status: cursor advanced to slice 1.
|
|
185
|
+
r = _run_cmd(NEXT, "--session", session, "--json", "status")
|
|
186
|
+
st = json.loads(r.stdout)
|
|
187
|
+
assert st["current_slice_id"] == "conference/room-lifecycle"
|
|
188
|
+
assert st["state"] == "not_started"
|
|
189
|
+
|
|
190
|
+
# 14. read gate: slice 1 now OK, slice 0 now BLOCKED.
|
|
191
|
+
r = _gate(GATE_READ, {
|
|
192
|
+
"tool_name": "Read",
|
|
193
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
194
|
+
}, session)
|
|
195
|
+
assert r.returncode == 0, r.stderr
|
|
196
|
+
|
|
197
|
+
r = _gate(GATE_READ, {
|
|
198
|
+
"tool_name": "Read",
|
|
199
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
|
|
200
|
+
}, session)
|
|
201
|
+
assert r.returncode == 2
|
|
202
|
+
|
|
203
|
+
# 15. evidence file was cleaned up by mark_user_confirmed.
|
|
204
|
+
ev_files = list((session.parent / ".trtc-apply-evidence").glob("*.json"))
|
|
205
|
+
assert ev_files == [], (
|
|
206
|
+
"evidence directory should be empty after a clean confirm; "
|
|
207
|
+
f"found {[f.name for f in ev_files]}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_apply_failure_path(session_factory, project_factory):
|
|
212
|
+
"""Code that never wires up the slice entry → apply_failed → Stop blocked → Edit allowed."""
|
|
213
|
+
session = session_factory(confirmed_plan=["conference/login-auth"])
|
|
214
|
+
proj = project_factory()
|
|
215
|
+
target = proj / "src" / "views" / "MeetingRoom.vue"
|
|
216
|
+
|
|
217
|
+
_run_cmd(INIT, "--session", session)
|
|
218
|
+
_run_cmd(NEXT, "--session", session, "advance", "mark_slice_read")
|
|
219
|
+
|
|
220
|
+
# Write real code that never references the login-auth entry (useLoginState).
|
|
221
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
target.write_text(
|
|
223
|
+
'<template><div /></template>\n<script setup lang="ts">\n'
|
|
224
|
+
'import { ref } from "vue";\nconst ready = ref(true);\n</script>'
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
_run_cmd(NEXT, "--session", session, "advance", "mark_code_written")
|
|
228
|
+
|
|
229
|
+
r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
|
|
230
|
+
assert r.returncode == 1
|
|
231
|
+
data = yaml.safe_load(session.read_text())
|
|
232
|
+
assert data["current_execution_state"] == "apply_failed"
|
|
233
|
+
|
|
234
|
+
# Stop blocked.
|
|
235
|
+
r = _stop(session)
|
|
236
|
+
assert r.returncode == 2
|
|
237
|
+
|
|
238
|
+
# Edit gate ALLOWED in apply_failed (V1 walk-back: no slice-reread gate).
|
|
239
|
+
r = _gate(GATE_WRITE, {
|
|
240
|
+
"tool_name": "Edit",
|
|
241
|
+
"tool_input": {"file_path": str(target)},
|
|
242
|
+
}, session)
|
|
243
|
+
assert r.returncode == 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_auto_advance_loop_skips_user_confirm(session_factory, project_factory):
|
|
247
|
+
"""With pause_on_failure, apply pass auto-advances cursor to the next slice.
|
|
248
|
+
|
|
249
|
+
The AI never has to call mark_user_confirmed for clean slices — apply.py
|
|
250
|
+
does it. The state machine, gates and evidence file all still operate.
|
|
251
|
+
"""
|
|
252
|
+
session = session_factory(
|
|
253
|
+
confirmed_plan=["conference/login-auth", "conference/room-lifecycle"]
|
|
254
|
+
)
|
|
255
|
+
proj = project_factory()
|
|
256
|
+
target = proj / "src" / "views" / "MeetingRoom.vue"
|
|
257
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
target.write_text(_PASSING_LOGIN_VUE)
|
|
259
|
+
|
|
260
|
+
# Set policy on the session.
|
|
261
|
+
data = yaml.safe_load(session.read_text())
|
|
262
|
+
data["auto_advance_policy"] = "pause_on_failure"
|
|
263
|
+
session.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
|
|
264
|
+
|
|
265
|
+
# init + read + code_written + apply (pass).
|
|
266
|
+
assert _run_cmd(INIT, "--session", session).returncode == 0
|
|
267
|
+
assert _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read").returncode == 0
|
|
268
|
+
assert _run_cmd(NEXT, "--session", session, "advance", "mark_code_written").returncode == 0
|
|
269
|
+
|
|
270
|
+
r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
|
|
271
|
+
assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}"
|
|
272
|
+
assert "auto-advanced" in r.stdout
|
|
273
|
+
|
|
274
|
+
# Cursor should be on slice 1, not_started — no user confirm needed.
|
|
275
|
+
data = yaml.safe_load(session.read_text())
|
|
276
|
+
assert data["current_execution_index"] == 1
|
|
277
|
+
assert data["current_execution_state"] == "not_started"
|
|
278
|
+
|
|
279
|
+
# Read gate now allows slice 1's file (and blocks slice 0's).
|
|
280
|
+
r = _gate(GATE_READ, {
|
|
281
|
+
"tool_name": "Read",
|
|
282
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
283
|
+
}, session)
|
|
284
|
+
assert r.returncode == 0
|
|
285
|
+
|
|
286
|
+
r = _gate(GATE_READ, {
|
|
287
|
+
"tool_name": "Read",
|
|
288
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
|
|
289
|
+
}, session)
|
|
290
|
+
assert r.returncode == 2
|
|
291
|
+
|
|
292
|
+
# Stop hook: in not_started → allowed.
|
|
293
|
+
assert _stop(session).returncode == 0
|
|
294
|
+
|
|
295
|
+
# Slice 1: write code that never wires up the room-lifecycle entry
|
|
296
|
+
# (useRoomState); apply must still pause on failure even with auto-advance.
|
|
297
|
+
target.write_text("<template><div /></template>\n<script setup>\n// no room APIs\n</script>")
|
|
298
|
+
assert _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read").returncode == 0
|
|
299
|
+
assert _run_cmd(NEXT, "--session", session, "advance", "mark_code_written").returncode == 0
|
|
300
|
+
r = _run_cmd(APPLY, "--slice", "conference/room-lifecycle", "--session", session, "--project", proj)
|
|
301
|
+
assert r.returncode == 1
|
|
302
|
+
data = yaml.safe_load(session.read_text())
|
|
303
|
+
assert data["current_execution_state"] == "apply_failed"
|
|
304
|
+
# Stop is still blocked on apply_failed — the safety net survives.
|
|
305
|
+
assert _stop(session).returncode == 2
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for finalize_session.py session normalization."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FINALIZE_SCRIPT = (
|
|
12
|
+
Path(__file__).resolve().parents[1] / "scripts" / "finalize_session.py"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_finalize_session_normalizes_completed_topic_session(session_factory):
|
|
17
|
+
path = session_factory(
|
|
18
|
+
status="active",
|
|
19
|
+
current_step="topic-handoff",
|
|
20
|
+
completed_steps=[
|
|
21
|
+
"conference/login-auth",
|
|
22
|
+
"conference/room-lifecycle",
|
|
23
|
+
"conference/room-lifecycle",
|
|
24
|
+
],
|
|
25
|
+
execution_queue=[
|
|
26
|
+
{"id": "conference/login-auth", "type": "slice", "slices": ["conference/login-auth"], "status": "done"},
|
|
27
|
+
{"id": "foundation", "type": "unit", "slices": ["conference/room-lifecycle", "conference/video-layout"], "status": "pending"},
|
|
28
|
+
],
|
|
29
|
+
current_execution_index=2,
|
|
30
|
+
current_execution_state="all_done",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
[sys.executable, str(FINALIZE_SCRIPT), "--session", str(path)],
|
|
35
|
+
text=True,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert result.returncode == 0, result.stderr
|
|
40
|
+
data = yaml.safe_load(path.read_text())
|
|
41
|
+
assert data["status"] == "completed"
|
|
42
|
+
assert data["current_step"] == "completed"
|
|
43
|
+
assert data["current_execution_index"] == 2
|
|
44
|
+
assert data["current_execution_state"] == "all_done"
|
|
45
|
+
assert data["completed_steps"] == [
|
|
46
|
+
"conference/login-auth",
|
|
47
|
+
"conference/room-lifecycle",
|
|
48
|
+
"conference/video-layout",
|
|
49
|
+
]
|
|
50
|
+
assert [entry["status"] for entry in data["execution_queue"]] == ["done", "done"]
|
|
51
|
+
assert "last_recap" in data
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Tests for the PreToolUse gates that enforce the slice loop.
|
|
2
|
+
|
|
3
|
+
Both gates read a JSON object on stdin (Claude Code's PreToolUse contract):
|
|
4
|
+
{
|
|
5
|
+
"tool_name": "Read" | "Write" | "Edit" | ...,
|
|
6
|
+
"tool_input": { "file_path": "...", ... }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
They exit:
|
|
10
|
+
0 — allow the tool call (out of scope, or in-bounds)
|
|
11
|
+
2 — block the tool call; stderr explains why
|
|
12
|
+
other — treated as configuration error; AI sees stderr but Claude Code
|
|
13
|
+
does not block (we never produce these intentionally).
|
|
14
|
+
|
|
15
|
+
Out-of-scope cases that MUST exit 0:
|
|
16
|
+
- session file missing
|
|
17
|
+
- execution_queue not initialised (topic flow not active)
|
|
18
|
+
- tool_name doesn't match what the gate guards
|
|
19
|
+
- file_path falls outside the gate's domain
|
|
20
|
+
(slice-read gate: only guards knowledge-base/slices/*.md)
|
|
21
|
+
(slice-write gate: only guards user-project source files)
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
GUARDRAILS_DIR = Path(__file__).resolve().parents[1] / "guardrails"
|
|
33
|
+
GATE_READ = GUARDRAILS_DIR / "gate_slice_read.py"
|
|
34
|
+
GATE_WRITE = GUARDRAILS_DIR / "gate_slice_write.py"
|
|
35
|
+
|
|
36
|
+
# state_machine helper for advancing test sessions to a specific state
|
|
37
|
+
STATE_MACHINE_DIR = Path(__file__).resolve().parents[1] / "scripts" / "lib"
|
|
38
|
+
sys.path.insert(0, str(STATE_MACHINE_DIR))
|
|
39
|
+
import state_machine # noqa: E402
|
|
40
|
+
sys.path.pop(0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_gate(script: Path, payload: dict, session_path: Path) -> subprocess.CompletedProcess:
|
|
44
|
+
"""Invoke a gate script with the given JSON stdin payload.
|
|
45
|
+
|
|
46
|
+
The session path is passed via TRTC_SESSION_PATH env var so the gate
|
|
47
|
+
doesn't have to walk the filesystem looking for one. Real hooks use the
|
|
48
|
+
project's repo root via $CLAUDE_PROJECT_DIR; tests use a tmp_path session.
|
|
49
|
+
"""
|
|
50
|
+
return subprocess.run(
|
|
51
|
+
[sys.executable, str(script)],
|
|
52
|
+
input=json.dumps(payload),
|
|
53
|
+
text=True,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------- gate_slice_read ------------------------------------------------
|
|
60
|
+
|
|
61
|
+
class TestGateSliceRead:
|
|
62
|
+
def test_allows_when_session_missing(self, tmp_path):
|
|
63
|
+
"""No session file → not in topic flow → must allow."""
|
|
64
|
+
payload = {
|
|
65
|
+
"tool_name": "Read",
|
|
66
|
+
"tool_input": {"file_path": str(tmp_path / "knowledge-base/slices/conference/web/room-chat.md")},
|
|
67
|
+
}
|
|
68
|
+
result = _run_gate(GATE_READ, payload, tmp_path / "missing.yaml")
|
|
69
|
+
assert result.returncode == 0, result.stderr
|
|
70
|
+
|
|
71
|
+
def test_allows_when_queue_not_initialised(self, session_factory):
|
|
72
|
+
"""Session exists but no execution_queue -> topic flow not active."""
|
|
73
|
+
path = session_factory()
|
|
74
|
+
payload = {
|
|
75
|
+
"tool_name": "Read",
|
|
76
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
|
|
77
|
+
}
|
|
78
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
79
|
+
assert result.returncode == 0, result.stderr
|
|
80
|
+
|
|
81
|
+
def test_allows_non_read_tool(self, session_factory):
|
|
82
|
+
"""Gate only guards Read; Write should pass through."""
|
|
83
|
+
path = session_factory()
|
|
84
|
+
state_machine.init_queue(path)
|
|
85
|
+
payload = {
|
|
86
|
+
"tool_name": "Write",
|
|
87
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
|
|
88
|
+
}
|
|
89
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
90
|
+
assert result.returncode == 0
|
|
91
|
+
|
|
92
|
+
def test_allows_non_slice_file(self, session_factory):
|
|
93
|
+
"""Reading a non-slice file (e.g., README) is none of the gate's business."""
|
|
94
|
+
path = session_factory()
|
|
95
|
+
state_machine.init_queue(path)
|
|
96
|
+
payload = {
|
|
97
|
+
"tool_name": "Read",
|
|
98
|
+
"tool_input": {"file_path": "README.md"},
|
|
99
|
+
}
|
|
100
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
101
|
+
assert result.returncode == 0
|
|
102
|
+
|
|
103
|
+
def test_allows_current_slice_overview(self, session_factory):
|
|
104
|
+
"""Product-level overview for the current slice is allowed."""
|
|
105
|
+
path = session_factory()
|
|
106
|
+
state_machine.init_queue(path)
|
|
107
|
+
payload = {
|
|
108
|
+
"tool_name": "Read",
|
|
109
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/login-auth.md"},
|
|
110
|
+
}
|
|
111
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
112
|
+
assert result.returncode == 0, result.stderr
|
|
113
|
+
|
|
114
|
+
def test_allows_current_slice_platform_file(self, session_factory):
|
|
115
|
+
"""Platform-specific file for the current slice is allowed."""
|
|
116
|
+
path = session_factory()
|
|
117
|
+
state_machine.init_queue(path)
|
|
118
|
+
payload = {
|
|
119
|
+
"tool_name": "Read",
|
|
120
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
|
|
121
|
+
}
|
|
122
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
123
|
+
assert result.returncode == 0, result.stderr
|
|
124
|
+
|
|
125
|
+
def test_blocks_next_slice_read(self, session_factory):
|
|
126
|
+
"""Reading the SECOND slice while cursor is on the first → exit 2."""
|
|
127
|
+
path = session_factory()
|
|
128
|
+
state_machine.init_queue(path)
|
|
129
|
+
payload = {
|
|
130
|
+
"tool_name": "Read",
|
|
131
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
132
|
+
}
|
|
133
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
134
|
+
assert result.returncode == 2
|
|
135
|
+
assert "current slice" in result.stderr.lower() or "login-auth" in result.stderr
|
|
136
|
+
|
|
137
|
+
def test_blocks_unrelated_slice(self, session_factory):
|
|
138
|
+
"""Reading a slice not even in the queue → exit 2."""
|
|
139
|
+
path = session_factory()
|
|
140
|
+
state_machine.init_queue(path)
|
|
141
|
+
payload = {
|
|
142
|
+
"tool_name": "Read",
|
|
143
|
+
"tool_input": {"file_path": "knowledge-base/slices/live/coguest-apply.md"},
|
|
144
|
+
}
|
|
145
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
146
|
+
assert result.returncode == 2
|
|
147
|
+
|
|
148
|
+
def test_unit_mode_allows_any_slice_in_current_unit(self, session_factory):
|
|
149
|
+
path = session_factory(
|
|
150
|
+
execution_granularity="unit",
|
|
151
|
+
confirmed_plan=[
|
|
152
|
+
"conference/login-auth",
|
|
153
|
+
"conference/room-lifecycle",
|
|
154
|
+
"conference/room-chat",
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
state_machine.init_queue(path)
|
|
158
|
+
payload = {
|
|
159
|
+
"tool_name": "Read",
|
|
160
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
161
|
+
}
|
|
162
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
163
|
+
assert result.returncode == 0, result.stderr
|
|
164
|
+
|
|
165
|
+
def test_unit_mode_blocks_slice_outside_current_unit(self, session_factory):
|
|
166
|
+
path = session_factory(
|
|
167
|
+
execution_granularity="unit",
|
|
168
|
+
confirmed_plan=[
|
|
169
|
+
"conference/login-auth",
|
|
170
|
+
"conference/room-lifecycle",
|
|
171
|
+
"conference/room-chat",
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
state_machine.init_queue(path)
|
|
175
|
+
payload = {
|
|
176
|
+
"tool_name": "Read",
|
|
177
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
|
|
178
|
+
}
|
|
179
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
180
|
+
assert result.returncode == 2
|
|
181
|
+
assert "current unit" in result.stderr or "foundation" in result.stderr
|
|
182
|
+
|
|
183
|
+
def test_allows_absolute_path_to_current_slice(self, session_factory, tmp_path):
|
|
184
|
+
"""Absolute paths must also be matched, not just relative ones."""
|
|
185
|
+
path = session_factory()
|
|
186
|
+
state_machine.init_queue(path)
|
|
187
|
+
abs_path = tmp_path / "anywhere/knowledge-base/slices/conference/web/login-auth.md"
|
|
188
|
+
payload = {"tool_name": "Read", "tool_input": {"file_path": str(abs_path)}}
|
|
189
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
190
|
+
assert result.returncode == 0, result.stderr
|
|
191
|
+
|
|
192
|
+
def test_advances_to_next_slice_after_confirm(self, session_factory):
|
|
193
|
+
"""After full cycle, the second slice should be readable."""
|
|
194
|
+
path = session_factory()
|
|
195
|
+
state_machine.init_queue(path)
|
|
196
|
+
for transition in [
|
|
197
|
+
"mark_slice_read",
|
|
198
|
+
"mark_code_written",
|
|
199
|
+
"mark_apply_passed",
|
|
200
|
+
"mark_user_confirmed",
|
|
201
|
+
]:
|
|
202
|
+
state_machine.advance(path, transition)
|
|
203
|
+
# Now cursor is on conference/room-lifecycle.
|
|
204
|
+
payload = {
|
|
205
|
+
"tool_name": "Read",
|
|
206
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
|
|
207
|
+
}
|
|
208
|
+
result = _run_gate(GATE_READ, payload, path)
|
|
209
|
+
assert result.returncode == 0, result.stderr
|
|
210
|
+
|
|
211
|
+
# And login-auth (the previous slice) is now blocked, since we've moved on.
|
|
212
|
+
payload2 = {
|
|
213
|
+
"tool_name": "Read",
|
|
214
|
+
"tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
|
|
215
|
+
}
|
|
216
|
+
result2 = _run_gate(GATE_READ, payload2, path)
|
|
217
|
+
assert result2.returncode == 2
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------- gate_slice_write ------------------------------------------------
|
|
221
|
+
|
|
222
|
+
class TestGateSliceWrite:
|
|
223
|
+
def _project_file(self, session_factory) -> tuple[Path, Path]:
|
|
224
|
+
"""Return (session_path, file_path-inside-project-src)."""
|
|
225
|
+
session_path = session_factory()
|
|
226
|
+
data = yaml.safe_load(session_path.read_text())
|
|
227
|
+
proj_root = Path(data["project_state"]["project_root"])
|
|
228
|
+
proj_root.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
return session_path, proj_root / "src" / "views" / "MeetingRoom.vue"
|
|
230
|
+
|
|
231
|
+
def test_allows_when_session_missing(self, tmp_path):
|
|
232
|
+
payload = {
|
|
233
|
+
"tool_name": "Write",
|
|
234
|
+
"tool_input": {"file_path": str(tmp_path / "user-project/src/foo.vue")},
|
|
235
|
+
}
|
|
236
|
+
result = _run_gate(GATE_WRITE, payload, tmp_path / "missing.yaml")
|
|
237
|
+
assert result.returncode == 0
|
|
238
|
+
|
|
239
|
+
def test_allows_when_queue_not_initialised(self, session_factory):
|
|
240
|
+
path, target = self._project_file(session_factory)
|
|
241
|
+
payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
|
|
242
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
243
|
+
assert result.returncode == 0
|
|
244
|
+
|
|
245
|
+
def test_allows_non_write_tool(self, session_factory):
|
|
246
|
+
path, target = self._project_file(session_factory)
|
|
247
|
+
state_machine.init_queue(path)
|
|
248
|
+
payload = {"tool_name": "Read", "tool_input": {"file_path": str(target)}}
|
|
249
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
250
|
+
assert result.returncode == 0
|
|
251
|
+
|
|
252
|
+
def test_allows_writes_outside_user_project(self, session_factory, tmp_path):
|
|
253
|
+
"""Writing to .claude/, knowledge-base/, /tmp etc. is not the gate's domain."""
|
|
254
|
+
path, _target = self._project_file(session_factory)
|
|
255
|
+
state_machine.init_queue(path)
|
|
256
|
+
# current_execution_state == not_started would block a project write,
|
|
257
|
+
# but this write is OUTSIDE the project root.
|
|
258
|
+
outside = tmp_path / "elsewhere" / "scratch.md"
|
|
259
|
+
payload = {"tool_name": "Write", "tool_input": {"file_path": str(outside)}}
|
|
260
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
261
|
+
assert result.returncode == 0, result.stderr
|
|
262
|
+
|
|
263
|
+
def test_blocks_write_when_state_not_started(self, session_factory):
|
|
264
|
+
"""Cursor sits on not_started → AI hasn't read the slice yet → block."""
|
|
265
|
+
path, target = self._project_file(session_factory)
|
|
266
|
+
state_machine.init_queue(path)
|
|
267
|
+
payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
|
|
268
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
269
|
+
assert result.returncode == 2
|
|
270
|
+
assert "not_started" in result.stderr or "read" in result.stderr.lower()
|
|
271
|
+
|
|
272
|
+
def test_allows_write_when_state_slice_read(self, session_factory):
|
|
273
|
+
path, target = self._project_file(session_factory)
|
|
274
|
+
state_machine.init_queue(path)
|
|
275
|
+
state_machine.advance(path, "mark_slice_read")
|
|
276
|
+
payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
|
|
277
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
278
|
+
assert result.returncode == 0, result.stderr
|
|
279
|
+
|
|
280
|
+
def test_allows_edit_when_state_code_written(self, session_factory):
|
|
281
|
+
"""Edit (during apply retry) is allowed in code_written state."""
|
|
282
|
+
path, target = self._project_file(session_factory)
|
|
283
|
+
state_machine.init_queue(path)
|
|
284
|
+
state_machine.advance(path, "mark_slice_read")
|
|
285
|
+
state_machine.advance(path, "mark_code_written")
|
|
286
|
+
payload = {"tool_name": "Edit", "tool_input": {"file_path": str(target)}}
|
|
287
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
288
|
+
assert result.returncode == 0, result.stderr
|
|
289
|
+
|
|
290
|
+
def test_allows_edit_in_apply_failed(self, session_factory):
|
|
291
|
+
"""apply_failed → Edit is ALLOWED so the AI can retry after a fail.
|
|
292
|
+
|
|
293
|
+
We deliberately do NOT enforce a "must re-read the slice first" rule
|
|
294
|
+
here — the V1 walk-back removed that conditional gate (see
|
|
295
|
+
docs/apply-skill-long-term-design.md §6.4 'walked back' list).
|
|
296
|
+
"""
|
|
297
|
+
path, target = self._project_file(session_factory)
|
|
298
|
+
state_machine.init_queue(path)
|
|
299
|
+
state_machine.advance(path, "mark_slice_read")
|
|
300
|
+
state_machine.advance(path, "mark_code_written")
|
|
301
|
+
state_machine.advance(path, "mark_apply_failed")
|
|
302
|
+
payload = {"tool_name": "Edit", "tool_input": {"file_path": str(target)}}
|
|
303
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
304
|
+
assert result.returncode == 0, result.stderr
|
|
305
|
+
|
|
306
|
+
def test_blocks_write_in_apply_passed(self, session_factory):
|
|
307
|
+
"""apply_passed → AI must wait for user confirmation, not keep editing."""
|
|
308
|
+
path, target = self._project_file(session_factory)
|
|
309
|
+
state_machine.init_queue(path)
|
|
310
|
+
state_machine.advance(path, "mark_slice_read")
|
|
311
|
+
state_machine.advance(path, "mark_code_written")
|
|
312
|
+
state_machine.advance(path, "mark_apply_passed")
|
|
313
|
+
payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
|
|
314
|
+
result = _run_gate(GATE_WRITE, payload, path)
|
|
315
|
+
assert result.returncode == 2
|
|
316
|
+
assert "apply_passed" in result.stderr or "confirm" in result.stderr.lower()
|