@tencent-rtc/trtc-agent-skills 0.1.3 → 0.1.4
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/.cursor/rules/main.mdc +12 -0
- package/AGENTS.md +14 -104
- package/CLAUDE.md +15 -127
- package/CODEBUDDY.md +32 -118
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/bin/cli.js +133 -41
- package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
- package/hooks/cursor-adapter.py +45 -18
- package/hooks/hooks-cursor.json +5 -18
- package/hooks/hooks.json +6 -31
- package/knowledge-base/conference/web/index.yaml +143 -0
- package/knowledge-base/platform-slice-template.md +1041 -133
- package/knowledge-base/products.yaml +76 -0
- package/knowledge-base/scenario-spec.md +1316 -115
- package/knowledge-base/scenarios/conference/base/general-conference.md +22 -2
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +14 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +19 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +13 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +12 -0
- package/knowledge-base/slice-spec.md +2377 -299
- package/knowledge-base/slices/conference/web/integration-audit.md +1 -1
- package/knowledge-base/slices/conference/web/login-auth.md +14 -1
- package/knowledge-base/tooling/aliases.yaml +92 -0
- package/knowledge-base/tooling/intent-signals.yaml +181 -0
- package/knowledge-base/tooling/symptom-keywords.yaml +21 -0
- package/package.json +1 -1
- package/skills/trtc/SKILL.md +202 -245
- package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_read.py +12 -8
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_write.py +16 -12
- package/skills/{trtc-topic/guardrails → trtc/hooks}/stop_require_apply_evidence.py +22 -14
- package/skills/trtc/hooks/topic_phase_gate.py +161 -0
- package/skills/{trtc-topic → trtc}/runtime/README.md +2 -2
- package/skills/{trtc-onboarding/reference/reporting-protocol.md → trtc/runtime/REPORTING.md} +5 -5
- package/skills/{trtc-topic → trtc}/runtime/RUNTIME.md +5 -5
- package/skills/trtc/runtime/lib/__init__.py +0 -0
- package/skills/{trtc-topic → trtc}/runtime/package-lock.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/package.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/telemetry-bridge.mjs +1 -1
- package/skills/{trtc-topic/scripts → trtc/tools}/STATE-MACHINE-GUIDE.md +23 -23
- package/skills/trtc/tools/__init__.py +2 -0
- package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
- package/skills/trtc/tools/apply.py +540 -0
- package/skills/trtc/tools/docs.py +712 -0
- package/skills/trtc/tools/docsbot.py +182 -0
- package/skills/trtc/tools/entry/render_ai_instructions.py +92 -0
- package/skills/trtc/tools/flow.py +1089 -0
- package/skills/{trtc-topic/scripts → trtc/tools}/init_slice_queue.py +5 -4
- package/skills/{trtc-topic/scripts → trtc/tools}/next_slice.py +5 -4
- package/skills/trtc/tools/query_classifier.py +301 -0
- package/skills/trtc/tools/reporting.py +447 -0
- package/skills/trtc/tools/search.py +817 -0
- package/skills/trtc/tools/session.py +1261 -0
- package/skills/trtc/tools/state_machine.py +690 -0
- package/skills/trtc-ai-service/README.md +195 -0
- package/skills/trtc-ai-service/README.zh-CN.md +193 -0
- package/skills/trtc-ai-service/SKILL.md +945 -0
- package/skills/trtc-ai-service/auto_adapters/README.md +40 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/README.md +27 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/angular/voice-agent.component.ts.tpl +131 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/manifest.yaml +57 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/react/VoiceAgent.tsx.tpl +142 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/vue/VoiceAgent.vue.tpl +121 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-backend.md +45 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-frontend.md +51 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-rest-api.md +93 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/manifest.yaml +30 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/quarkus/VoiceAgentFilter.java.tpl +64 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/springboot/VoiceAgentFilter.java.tpl +91 -0
- package/skills/trtc-ai-service/auto_adapters/manifest.yaml +43 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/express.js.tpl +40 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/fastify.js.tpl +27 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/koa.js.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/manifest.yaml +47 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/README.md +22 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/django.py.tpl +32 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/fastapi.py.tpl +35 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/flask.py.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/manifest.yaml +45 -0
- package/skills/trtc-ai-service/capabilities/__init__.py +43 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/.env.example +29 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTEGRATION.md +134 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTERFACE_ADAPT.md +111 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/QUICK_START.md +62 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/manifest.yaml +250 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/requirements.txt +6 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/__init__.py +10 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/_capability_loader.py +218 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/agent.py +231 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +132 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/health.py +355 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/log_filter.py +76 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/modality.py +109 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/server.py +312 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/trtc_client.py +315 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/usersig.py +90 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/tests/test_skeleton.py +216 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/README.md +48 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/app.js +415 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/index.html +68 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/styles.css +136 -0
- package/skills/trtc-ai-service/capabilities/digital-human/README.md +31 -0
- package/skills/trtc-ai-service/capabilities/digital-human/manifest.yaml +64 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/router.py +43 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/INTERFACE_ADAPT.md +353 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/README.md +44 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/manifest.yaml +227 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/default_rest.py +242 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/factory.py +89 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/local_queue.py +258 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/mock.py +132 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/__init__.py +25 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/intent_detector.py +75 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/models.py +163 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/service.py +192 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/feedback_store.py +54 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/handoff_client.py +86 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/queue.py +62 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/router.py +201 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/summary_link.py +77 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/trigger.py +25 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/INTERFACE_ADAPT.md +297 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/README.md +51 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/data/faq.json +20 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/manifest.yaml +211 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/default_rest.py +209 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/factory.py +86 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/local_json.py +172 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/mock.py +91 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/__init__.py +12 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/models.py +77 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/scoring.py +73 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/service.py +78 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/kb_client.py +61 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/retriever.py +56 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/router.py +85 -0
- package/skills/trtc-ai-service/capabilities/session-summary/INTERFACE_ADAPT.md +99 -0
- package/skills/trtc-ai-service/capabilities/session-summary/README.md +47 -0
- package/skills/trtc-ai-service/capabilities/session-summary/data/test_session.json +18 -0
- package/skills/trtc-ai-service/capabilities/session-summary/manifest.yaml +165 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/__init__.py +5 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/base.py +31 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/default_rest.py +67 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/factory.py +51 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/local_json.py +42 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/mock.py +22 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/recorder.py +210 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/router.py +93 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/summarizer.py +163 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/INTERFACE_ADAPT.md +158 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/README.md +50 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/data/tools.yaml +58 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/__init__.py +1 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/local_tools.py +101 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/manifest.yaml +146 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/dispatcher.py +54 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/registry.py +219 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/router.py +50 -0
- package/skills/trtc-ai-service/references/business-contract-spec.md +263 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/README.md +86 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/output-templates/recipe.yaml.j2 +194 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q1-business-scenario.md +43 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q2-io-modality.md +57 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q3-ui-form.md +55 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q4-capabilities.md +78 -0
- package/skills/trtc-ai-service/scenarios/customer-service/README.md +114 -0
- package/skills/trtc-ai-service/scenarios/customer-service/recipe.yaml +154 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/README.md +32 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/faq-sample.json +37 -0
- package/skills/trtc-ai-service/scenarios/customer-service/system-prompt.template.md +94 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/app.js +347 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/index.html +125 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/styles.css +487 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/design-system/DESIGN_GUIDELINES.md +370 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/README.md +68 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/app.js +1307 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/data.js +40 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/index.html +233 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/mock-shop.json +21 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/styles.css +603 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/agent-link.js +323 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/app.js +458 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/index.html +109 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/styles.css +489 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/tokens.css +59 -0
- package/skills/trtc-ai-service/scripts/add-capability.py +364 -0
- package/skills/trtc-ai-service/scripts/contract-adapt.py +334 -0
- package/skills/trtc-ai-service/scripts/detect-stack.py +40 -0
- package/skills/trtc-ai-service/scripts/lib/__init__.py +20 -0
- package/skills/trtc-ai-service/scripts/lib/adapter_codegen.py +509 -0
- package/skills/trtc-ai-service/scripts/lib/arbitrator.py +152 -0
- package/skills/trtc-ai-service/scripts/lib/contract_resolver.py +519 -0
- package/skills/trtc-ai-service/scripts/lib/credential_validators.py +253 -0
- package/skills/trtc-ai-service/scripts/lib/curl_parser.py +303 -0
- package/skills/trtc-ai-service/scripts/lib/degrader.py +140 -0
- package/skills/trtc-ai-service/scripts/lib/injector.py +347 -0
- package/skills/trtc-ai-service/scripts/lib/manifest_resolver.py +288 -0
- package/skills/trtc-ai-service/scripts/lib/openapi_parser.py +289 -0
- package/skills/trtc-ai-service/scripts/lib/stack_detector.py +159 -0
- package/skills/trtc-ai-service/scripts/lib/tokens_compile.py +204 -0
- package/skills/trtc-ai-service/scripts/post-install-patch.py +225 -0
- package/skills/trtc-ai-service/scripts/setup-credentials.py +393 -0
- package/skills/trtc-ai-service/scripts/verify-credentials.py +108 -0
- package/skills/trtc-ai-service/start.sh +111 -0
- package/skills/trtc-ai-service/tests/__init__.py +1 -0
- package/skills/trtc-ai-service/tests/test_arbitrator.py +64 -0
- package/skills/trtc-ai-service/tests/test_capability_overlay.py +85 -0
- package/skills/trtc-ai-service/tests/test_contract_resolver.py +190 -0
- package/skills/trtc-ai-service/tests/test_handoff_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_kb_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_manifest_resolver.py +95 -0
- package/skills/trtc-ai-service/tests/test_recipe_assembly.py +175 -0
- package/skills/trtc-ai-service/tests/test_stack_and_degrader.py +101 -0
- package/skills/trtc-ai-service/tests/test_verify_credentials.py +285 -0
- package/skills/trtc-ai-service/triggers.yaml +29 -0
- package/skills/trtc-conference/SKILL.md +324 -0
- package/skills/trtc-conference/flows/onboarding.md +205 -0
- package/skills/trtc-conference/flows/topic.md +474 -0
- package/skills/trtc-conference/flows/troubleshoot.md +85 -0
- package/skills/trtc-conference/hooks/pretooluse_require_business_decisions.py +213 -0
- package/skills/trtc-conference/playbooks/medical-quickstart.md +84 -0
- package/skills/trtc-conference/playbooks/official-roomkit.md +97 -0
- package/skills/trtc-conference/references/local-usersig/basic-info-config.ts +39 -0
- package/skills/trtc-conference/references/usersig-handling.md +134 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/test_conference_index_contract.py +43 -0
- package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +103 -0
- package/skills/trtc-conference/tests/test_conference_template_contract.py +25 -0
- package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +132 -0
- package/skills/trtc-conference/tools/apply_checks.py +328 -0
- package/skills/trtc-conference/verify_lib/__init__.py +0 -0
- package/skills/{trtc-apply/guardrails/apply_lib → trtc-conference/verify_lib}/rule_parser.py +1 -1
- package/skills/trtc-docs/SKILL.md +91 -119
- package/.cursor/rules/ui-mode.mdc +0 -99
- package/ai-instructions/base.md +0 -13
- package/ai-instructions/ui-mode.md +0 -93
- package/knowledge-base/index.yaml +0 -462
- package/skills/trtc/room-builder/SKILL.md +0 -133
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +0 -108
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +0 -226
- package/skills/trtc-apply/SKILL.md +0 -97
- package/skills/trtc-onboarding/SKILL.md +0 -841
- package/skills/trtc-onboarding/reference/path-a1-demo.md +0 -103
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +0 -737
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +0 -186
- package/skills/trtc-onboarding/reference/path-c-expand.md +0 -43
- package/skills/trtc-onboarding/reference/supported-matrix.md +0 -100
- package/skills/trtc-search/SKILL.md +0 -228
- package/skills/trtc-topic/SKILL.md +0 -622
- package/skills/trtc-topic/scripts/apply.py +0 -581
- package/skills/trtc-topic/scripts/lib/state_machine.py +0 -328
- package/skills/trtc-topic/tests/README.md +0 -70
- package/skills/trtc-topic/tests/conftest.py +0 -72
- package/skills/trtc-topic/tests/test_apply_cli.py +0 -480
- package/skills/trtc-topic/tests/test_end_to_end.py +0 -305
- package/skills/trtc-topic/tests/test_finalize_session.py +0 -51
- package/skills/trtc-topic/tests/test_gates.py +0 -316
- package/skills/trtc-topic/tests/test_session_resolver.py +0 -260
- package/skills/trtc-topic/tests/test_state_machine.py +0 -414
- package/skills/trtc-topic/tests/test_stop_require_apply.py +0 -99
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +0 -130
- /package/skills/{trtc-topic → trtc}/runtime/lib/platforms.py +0 -0
- /package/skills/{trtc-topic → trtc}/runtime/telemetry_collector.py +0 -0
- /package/skills/{trtc-onboarding/reference → trtc/runtime}/usersig-handling.md +0 -0
- /package/skills/{trtc-topic/scripts → trtc/tools}/finalize_session.py +0 -0
- /package/skills/{trtc-apply/guardrails/apply_lib → trtc-ai-service/capabilities/conversation-core/tests}/__init__.py +0 -0
- /package/skills/{trtc-topic → trtc-conference}/references/execution-units.yaml +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.d.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.js +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/backend-contract.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/integration.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/theme.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/index.html +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/package.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/postcss.config.js +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/App.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/ConsultationManagePanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LanguageSwitch.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LoadingSpinner.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalAlert.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalBusinessPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalButton.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalDataPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalRecordPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/PrescriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/basic-info-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/runtime-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/env.d.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationChat.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationDevices.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationParticipants.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationPermissions.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/utils.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/en-US/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/medicalTranslate.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/state.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/zh-CN/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/main.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/appointments.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/users.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/router/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/launchContext.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/shared/icons.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/index.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/tailwind.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/theme.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/auth.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/format.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/navigation.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/session.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorDashboardView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/LoginView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationFinishedView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientSelectDoctorView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientWaitingView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.node.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/vite.config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md" +0 -0
- /package/skills/{trtc-topic/runtime/lib → trtc-conference/tools}/__init__.py +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Phase 3 Stage 6: human-handoff capability ports/adapters unit tests.
|
|
2
|
+
|
|
3
|
+
Coverage targets:
|
|
4
|
+
- LocalQueueHandoffClient: FIFO queue + auto-connect + cancel / status transitions
|
|
5
|
+
- MockHandoffClient: pre-seeded demo data (_seed three tickets) + list_tickets
|
|
6
|
+
- DefaultRestHandoffClient: base_url security validation (private network denied) + HTTP path calls + passthrough responses
|
|
7
|
+
|
|
8
|
+
Note: This test file cleans up stale ``src.*`` modules from sys.modules before imports
|
|
9
|
+
to avoid conflicts with the same-named ``src`` package in ``test_kb_ports.py``;
|
|
10
|
+
KB tests likewise self-clean.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import unittest
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Isolated import: clean src.* cache + insert human-handoff's own src parent directory
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
_HH = _ROOT / "capabilities" / "human-handoff"
|
|
26
|
+
|
|
27
|
+
for _name in [k for k in list(sys.modules) if k == "src" or k.startswith("src.")]:
|
|
28
|
+
del sys.modules[_name]
|
|
29
|
+
sys.path[:] = [p for p in sys.path if "/capabilities/" not in p]
|
|
30
|
+
sys.path.insert(0, str(_HH))
|
|
31
|
+
|
|
32
|
+
# Import directly from core.models module to bypass the circular dependency in src/core/__init__.py
|
|
33
|
+
# (src/core/__init__.py imports service.py, which in turn depends on ports.handoff_client)
|
|
34
|
+
import importlib # noqa: E402
|
|
35
|
+
|
|
36
|
+
_models = importlib.import_module("src.core.models")
|
|
37
|
+
TicketStatusEnum = _models.TicketStatusEnum
|
|
38
|
+
|
|
39
|
+
from src.adapters.local_queue import LocalQueueHandoffClient # noqa: E402
|
|
40
|
+
from src.adapters.mock import MockHandoffClient # noqa: E402
|
|
41
|
+
# Pre-import default_rest (HH version) and pin the reference at module scope
|
|
42
|
+
# to avoid ``src`` being replaced by KB namespace when test_kb_ports runs later
|
|
43
|
+
from src.adapters import default_rest as _hh_default_rest # noqa: E402
|
|
44
|
+
|
|
45
|
+
DefaultRestHandoffClient = _hh_default_rest.DefaultRestHandoffClient
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LocalQueueAdapterTests(unittest.TestCase):
|
|
49
|
+
"""Cover LocalQueueHandoffClient core behaviors."""
|
|
50
|
+
|
|
51
|
+
def test_create_then_query_pending(self):
|
|
52
|
+
c = LocalQueueHandoffClient(
|
|
53
|
+
capacity=5, agent_pool_size=0, estimated_wait_per_slot=10
|
|
54
|
+
)
|
|
55
|
+
t = c.create_ticket(
|
|
56
|
+
user_id="u1",
|
|
57
|
+
subject="退款",
|
|
58
|
+
description="3 天未到账",
|
|
59
|
+
priority="normal",
|
|
60
|
+
transcript=["用户:申请退款"],
|
|
61
|
+
)
|
|
62
|
+
# agent_pool_size=0 → no auto-connect; enters queue
|
|
63
|
+
self.assertEqual(t.status, TicketStatusEnum.PENDING.value)
|
|
64
|
+
self.assertEqual(t.queue_position, 1)
|
|
65
|
+
self.assertEqual(t.eta_seconds, 10)
|
|
66
|
+
|
|
67
|
+
status = c.query_status(t.ticket_id)
|
|
68
|
+
self.assertIsNotNone(status)
|
|
69
|
+
self.assertEqual(status.status, TicketStatusEnum.PENDING.value)
|
|
70
|
+
|
|
71
|
+
def test_auto_connect_when_agent_available(self):
|
|
72
|
+
c = LocalQueueHandoffClient(capacity=5, agent_pool_size=1)
|
|
73
|
+
t = c.create_ticket(user_id="u_auto", subject="物流")
|
|
74
|
+
self.assertEqual(t.status, TicketStatusEnum.PROCESSING.value)
|
|
75
|
+
self.assertEqual(t.queue_position, 0)
|
|
76
|
+
self.assertIsNotNone(t.agent_id)
|
|
77
|
+
|
|
78
|
+
def test_cancel_releases_slot(self):
|
|
79
|
+
c = LocalQueueHandoffClient(capacity=5, agent_pool_size=1)
|
|
80
|
+
t = c.create_ticket(user_id="u_cancel", subject="发票")
|
|
81
|
+
canceled = c.cancel_ticket(t.ticket_id, reason="用户主动取消")
|
|
82
|
+
self.assertIsNotNone(canceled)
|
|
83
|
+
self.assertEqual(canceled.status, TicketStatusEnum.CANCELED.value)
|
|
84
|
+
# After cancel, status still queryable (closed ticket), but overall_status.connected = 0
|
|
85
|
+
status = c.overall_status()
|
|
86
|
+
self.assertEqual(status.connected, 0)
|
|
87
|
+
|
|
88
|
+
def test_capacity_full_yields_timeout(self):
|
|
89
|
+
c = LocalQueueHandoffClient(capacity=1, agent_pool_size=0)
|
|
90
|
+
c.create_ticket(user_id="u_fill")
|
|
91
|
+
t2 = c.create_ticket(user_id="u_overflow")
|
|
92
|
+
self.assertEqual(t2.status, TicketStatusEnum.TIMEOUT.value)
|
|
93
|
+
self.assertIsNotNone(t2.closed_at)
|
|
94
|
+
|
|
95
|
+
def test_update_status_manual_close(self):
|
|
96
|
+
c = LocalQueueHandoffClient(capacity=5, agent_pool_size=1)
|
|
97
|
+
t = c.create_ticket(user_id="u_close")
|
|
98
|
+
# auto-connected → close
|
|
99
|
+
closed = c.update_status(t.ticket_id, "closed")
|
|
100
|
+
self.assertEqual(closed.status, TicketStatusEnum.CLOSED.value)
|
|
101
|
+
self.assertIsNotNone(closed.closed_at)
|
|
102
|
+
self.assertEqual(c.overall_status().connected, 0)
|
|
103
|
+
|
|
104
|
+
def test_get_or_attach_returns_active_only(self):
|
|
105
|
+
c = LocalQueueHandoffClient(capacity=5, agent_pool_size=0)
|
|
106
|
+
t = c.create_ticket(user_id="u_query")
|
|
107
|
+
same = c.get_or_attach("u_query")
|
|
108
|
+
self.assertEqual(same.ticket_id, t.ticket_id)
|
|
109
|
+
# After cancel, get_or_attach should return None (only returns active tickets)
|
|
110
|
+
c.cancel_ticket(t.ticket_id)
|
|
111
|
+
self.assertIsNone(c.get_or_attach("u_query"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MockAdapterTests(unittest.TestCase):
|
|
115
|
+
"""MockHandoffClient demo data should be pre-populated."""
|
|
116
|
+
|
|
117
|
+
def test_seed_three_tickets(self):
|
|
118
|
+
c = MockHandoffClient(seed_demo_data=True, agent_pool_size=2)
|
|
119
|
+
tickets = c.list_tickets(limit=10)
|
|
120
|
+
ids = {t.ticket_id for t in tickets}
|
|
121
|
+
# All three demo tickets present
|
|
122
|
+
self.assertIn("demo_pending_001", ids)
|
|
123
|
+
self.assertIn("demo_processing_001", ids)
|
|
124
|
+
self.assertIn("demo_closed_001", ids)
|
|
125
|
+
statuses = {t.status for t in tickets}
|
|
126
|
+
self.assertIn(TicketStatusEnum.PENDING.value, statuses)
|
|
127
|
+
self.assertIn(TicketStatusEnum.PROCESSING.value, statuses)
|
|
128
|
+
self.assertIn(TicketStatusEnum.CLOSED.value, statuses)
|
|
129
|
+
|
|
130
|
+
def test_seed_can_be_disabled(self):
|
|
131
|
+
c = MockHandoffClient(seed_demo_data=False, agent_pool_size=2)
|
|
132
|
+
self.assertEqual(c.list_tickets(), [])
|
|
133
|
+
|
|
134
|
+
def test_inherits_create_behavior(self):
|
|
135
|
+
c = MockHandoffClient(seed_demo_data=False, agent_pool_size=2)
|
|
136
|
+
t = c.create_ticket(user_id="u_new", subject="测试")
|
|
137
|
+
# agent_pool_size=2 → immediate connect
|
|
138
|
+
self.assertEqual(t.status, TicketStatusEnum.PROCESSING.value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DefaultRestAdapterSecurityTests(unittest.TestCase):
|
|
142
|
+
"""DefaultRestHandoffClient base_url security validation (no actual HTTP calls)."""
|
|
143
|
+
|
|
144
|
+
def test_https_public_allowed(self):
|
|
145
|
+
# No real HTTP: constructing the object is enough to trigger _validate_base_url
|
|
146
|
+
c = DefaultRestHandoffClient(base_url="https://crm.example.com")
|
|
147
|
+
# Verify the validated url via _base
|
|
148
|
+
self.assertTrue(c._base.startswith("https://"))
|
|
149
|
+
|
|
150
|
+
def test_localhost_http_allowed(self):
|
|
151
|
+
c = DefaultRestHandoffClient(base_url="http://localhost:8080")
|
|
152
|
+
self.assertIn("localhost", c._base)
|
|
153
|
+
|
|
154
|
+
def test_private_network_denied(self):
|
|
155
|
+
for url in (
|
|
156
|
+
"https://10.1.2.3",
|
|
157
|
+
"https://192.168.0.1",
|
|
158
|
+
"https://172.16.0.5",
|
|
159
|
+
"https://9.0.0.1",
|
|
160
|
+
):
|
|
161
|
+
with self.subTest(url=url):
|
|
162
|
+
with self.assertRaises(ValueError):
|
|
163
|
+
DefaultRestHandoffClient(base_url=url)
|
|
164
|
+
|
|
165
|
+
def test_non_https_remote_denied(self):
|
|
166
|
+
with self.assertRaises(ValueError):
|
|
167
|
+
DefaultRestHandoffClient(base_url="http://crm.example.com")
|
|
168
|
+
|
|
169
|
+
def test_create_ticket_calls_post(self):
|
|
170
|
+
"""Simulate a successful create_ticket via requests, verify key payload fields."""
|
|
171
|
+
fake_resp = MagicMock()
|
|
172
|
+
fake_resp.status_code = 200
|
|
173
|
+
fake_resp.json.return_value = {
|
|
174
|
+
"ticket_id": "T-9001",
|
|
175
|
+
"queue_position": 2,
|
|
176
|
+
"eta_seconds": 60,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
c = DefaultRestHandoffClient(base_url="https://crm.example.com", token="tok")
|
|
180
|
+
with patch.object(c._session, "post", return_value=fake_resp) as p:
|
|
181
|
+
t = c.create_ticket(
|
|
182
|
+
user_id="u9", subject="退款", description="3 天未到账"
|
|
183
|
+
)
|
|
184
|
+
self.assertEqual(t.ticket_id, "T-9001")
|
|
185
|
+
self.assertEqual(t.queue_position, 2)
|
|
186
|
+
# Verify call arguments
|
|
187
|
+
self.assertEqual(p.call_count, 1)
|
|
188
|
+
_, kwargs = p.call_args
|
|
189
|
+
self.assertEqual(kwargs["json"]["user_id"], "u9")
|
|
190
|
+
# Authorization header should carry Bearer
|
|
191
|
+
self.assertIn("Bearer ", kwargs["headers"]["Authorization"])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
unittest.main()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Phase 3 Stage 6: knowledge-base capability ports/adapters unit tests.
|
|
2
|
+
|
|
3
|
+
Coverage targets:
|
|
4
|
+
- LocalJsonKbClient: reload / search (TF-IDF) / upsert / delete + stats
|
|
5
|
+
- MockKbClient: 5 pre-seeded demo FAQ entries + keyword search hits
|
|
6
|
+
- DefaultRestKbClient: base_url security validation + search/list_all via HTTP passthrough
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from unittest.mock import MagicMock, patch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Isolated import: clean src.* cache (avoid conflict with same-named src in test_handoff_ports)
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
+
_KB = _ROOT / "capabilities" / "knowledge-base"
|
|
23
|
+
|
|
24
|
+
for _name in [k for k in list(sys.modules) if k == "src" or k.startswith("src.")]:
|
|
25
|
+
del sys.modules[_name]
|
|
26
|
+
sys.path[:] = [p for p in sys.path if "/capabilities/" not in p]
|
|
27
|
+
sys.path.insert(0, str(_KB))
|
|
28
|
+
|
|
29
|
+
import importlib # noqa: E402
|
|
30
|
+
|
|
31
|
+
_models = importlib.import_module("src.core.models")
|
|
32
|
+
FaqEntry = _models.FaqEntry
|
|
33
|
+
|
|
34
|
+
from src.adapters.local_json import LocalJsonKbClient # noqa: E402
|
|
35
|
+
from src.adapters.mock import MockKbClient # noqa: E402
|
|
36
|
+
# 预先导入 default_rest(KB 版本)并固化引用,
|
|
37
|
+
# 避免后续测试运行时 ``src`` 已被替换为 HH 命名空间
|
|
38
|
+
from src.adapters import default_rest as _kb_default_rest # noqa: E402
|
|
39
|
+
|
|
40
|
+
DefaultRestKbClient = _kb_default_rest.DefaultRestKbClient
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LocalJsonAdapterTests(unittest.TestCase):
|
|
44
|
+
|
|
45
|
+
def setUp(self):
|
|
46
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
47
|
+
self.data_path = Path(self._tmp.name) / "faq.json"
|
|
48
|
+
self.data_path.write_text(
|
|
49
|
+
json.dumps(
|
|
50
|
+
[
|
|
51
|
+
{
|
|
52
|
+
"id": "kb_refund",
|
|
53
|
+
"question": "退款政策是什么?",
|
|
54
|
+
"answer": "支持 7 天无理由退款",
|
|
55
|
+
"keywords": ["退款", "退货", "refund"],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "kb_ship",
|
|
59
|
+
"question": "物流多久送达?",
|
|
60
|
+
"answer": "全国 3-5 天送达",
|
|
61
|
+
"keywords": ["物流", "发货"],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
ensure_ascii=False,
|
|
65
|
+
),
|
|
66
|
+
encoding="utf-8",
|
|
67
|
+
)
|
|
68
|
+
self.client = LocalJsonKbClient(
|
|
69
|
+
data_file=self.data_path, min_score=0.0, top_k=5
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def tearDown(self):
|
|
73
|
+
self._tmp.cleanup()
|
|
74
|
+
|
|
75
|
+
def test_reload_loads_entries(self):
|
|
76
|
+
self.assertEqual(len(self.client.list_all()), 2)
|
|
77
|
+
stats = self.client.stats()
|
|
78
|
+
self.assertEqual(stats.backend, "local_json")
|
|
79
|
+
self.assertEqual(stats.entry_count, 2)
|
|
80
|
+
self.assertIsNotNone(stats.loaded_at)
|
|
81
|
+
|
|
82
|
+
def test_search_hits_by_keyword(self):
|
|
83
|
+
hits = self.client.search("退款", top_k=3)
|
|
84
|
+
self.assertTrue(len(hits) >= 1)
|
|
85
|
+
self.assertEqual(hits[0].entry.id, "kb_refund")
|
|
86
|
+
# score should be > 0
|
|
87
|
+
self.assertGreater(hits[0].score, 0.0)
|
|
88
|
+
|
|
89
|
+
def test_search_returns_empty_for_blank_query(self):
|
|
90
|
+
self.assertEqual(self.client.search(""), [])
|
|
91
|
+
self.assertEqual(self.client.search(" "), [])
|
|
92
|
+
|
|
93
|
+
def test_upsert_persists_to_disk(self):
|
|
94
|
+
new_entry = FaqEntry(
|
|
95
|
+
id="kb_invoice",
|
|
96
|
+
question="如何开发票?",
|
|
97
|
+
answer="下单时勾选需要发票",
|
|
98
|
+
keywords=["发票"],
|
|
99
|
+
)
|
|
100
|
+
self.client.upsert(new_entry)
|
|
101
|
+
self.assertEqual(len(self.client.list_all()), 3)
|
|
102
|
+
|
|
103
|
+
# 真正落盘了:重读文件
|
|
104
|
+
on_disk = json.loads(self.data_path.read_text(encoding="utf-8"))
|
|
105
|
+
ids = {e["id"] for e in on_disk}
|
|
106
|
+
self.assertIn("kb_invoice", ids)
|
|
107
|
+
|
|
108
|
+
def test_upsert_updates_existing(self):
|
|
109
|
+
updated = FaqEntry(
|
|
110
|
+
id="kb_refund",
|
|
111
|
+
question="退款政策(已更新)",
|
|
112
|
+
answer="支持 14 天无理由退款",
|
|
113
|
+
keywords=["退款"],
|
|
114
|
+
)
|
|
115
|
+
self.client.upsert(updated)
|
|
116
|
+
items = self.client.list_all()
|
|
117
|
+
self.assertEqual(len(items), 2) # 总数不变
|
|
118
|
+
kept = next(e for e in items if e.id == "kb_refund")
|
|
119
|
+
self.assertIn("已更新", kept.question)
|
|
120
|
+
|
|
121
|
+
def test_delete_removes_entry(self):
|
|
122
|
+
ok = self.client.delete("kb_ship")
|
|
123
|
+
self.assertTrue(ok)
|
|
124
|
+
self.assertEqual(len(self.client.list_all()), 1)
|
|
125
|
+
# Non-existent id returns False
|
|
126
|
+
self.assertFalse(self.client.delete("nonexistent"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MockKbAdapterTests(unittest.TestCase):
|
|
130
|
+
|
|
131
|
+
def test_seed_five_demo_entries(self):
|
|
132
|
+
c = MockKbClient(seed_demo_data=True)
|
|
133
|
+
items = c.list_all()
|
|
134
|
+
self.assertGreaterEqual(len(items), 5)
|
|
135
|
+
ids = {e.id for e in items}
|
|
136
|
+
for required in ("demo_refund", "demo_logistics", "demo_invoice"):
|
|
137
|
+
self.assertIn(required, ids)
|
|
138
|
+
|
|
139
|
+
def test_search_hits_chinese_keyword(self):
|
|
140
|
+
c = MockKbClient(seed_demo_data=True)
|
|
141
|
+
hits = c.search("物流")
|
|
142
|
+
self.assertTrue(any(h.entry.id == "demo_logistics" for h in hits))
|
|
143
|
+
|
|
144
|
+
def test_seed_disable(self):
|
|
145
|
+
c = MockKbClient(seed_demo_data=False)
|
|
146
|
+
self.assertEqual(c.list_all(), [])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class DefaultRestKbAdapterSecurityTests(unittest.TestCase):
|
|
150
|
+
|
|
151
|
+
def test_https_public_allowed(self):
|
|
152
|
+
c = DefaultRestKbClient(base_url="https://kb.example.com")
|
|
153
|
+
self.assertTrue(c._base.startswith("https://"))
|
|
154
|
+
|
|
155
|
+
def test_localhost_http_allowed(self):
|
|
156
|
+
c = DefaultRestKbClient(base_url="http://127.0.0.1:9090")
|
|
157
|
+
self.assertIn("127.0.0.1", c._base)
|
|
158
|
+
|
|
159
|
+
def test_private_network_denied(self):
|
|
160
|
+
for url in ("https://10.1.1.1", "https://192.168.1.5"):
|
|
161
|
+
with self.subTest(url=url):
|
|
162
|
+
with self.assertRaises(ValueError):
|
|
163
|
+
DefaultRestKbClient(base_url=url)
|
|
164
|
+
|
|
165
|
+
def test_search_calls_post(self):
|
|
166
|
+
fake_resp = MagicMock()
|
|
167
|
+
fake_resp.status_code = 200
|
|
168
|
+
fake_resp.json.return_value = {
|
|
169
|
+
"hits": [
|
|
170
|
+
{
|
|
171
|
+
"entry": {
|
|
172
|
+
"id": "1",
|
|
173
|
+
"question": "退款?",
|
|
174
|
+
"answer": "7 天无理由",
|
|
175
|
+
"keywords": ["退款"],
|
|
176
|
+
},
|
|
177
|
+
"score": 0.85,
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
c = DefaultRestKbClient(base_url="https://kb.example.com", token="tok")
|
|
182
|
+
with patch.object(c._session, "post", return_value=fake_resp) as p:
|
|
183
|
+
hits = c.search("退款", top_k=3)
|
|
184
|
+
|
|
185
|
+
self.assertEqual(len(hits), 1)
|
|
186
|
+
self.assertEqual(hits[0].entry.id, "1")
|
|
187
|
+
# Request URL path
|
|
188
|
+
args, kwargs = p.call_args
|
|
189
|
+
self.assertTrue(args[0].endswith("/faq/search"))
|
|
190
|
+
self.assertEqual(kwargs["json"]["query"], "退款")
|
|
191
|
+
self.assertEqual(kwargs["json"]["top_k"], 3)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
unittest.main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Manifest parsing / topological order / circular dependency / version conflict / unknown injection point tests."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
8
|
+
sys.path.insert(0, str(_ROOT))
|
|
9
|
+
|
|
10
|
+
from scripts.lib import manifest_resolver as mr
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _mf(name, deps=(), inj=(), ext=(), typ="capability", version="1.0.0"):
|
|
14
|
+
return mr.Manifest(
|
|
15
|
+
name=name, version=version, type=typ,
|
|
16
|
+
dependencies=[mr.DependencySpec(d if isinstance(d, str) else d[0],
|
|
17
|
+
d[1] if isinstance(d, tuple) else "*") for d in deps],
|
|
18
|
+
injection_points=list(inj),
|
|
19
|
+
extensions=list(ext),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ManifestResolverTests(unittest.TestCase):
|
|
24
|
+
|
|
25
|
+
def setUp(self):
|
|
26
|
+
self.skeleton = _mf(
|
|
27
|
+
"conversation-core", typ="skeleton",
|
|
28
|
+
inj=[
|
|
29
|
+
{"id": "agent.before_start", "target": "src/agent.py", "position": "before:start_agent"},
|
|
30
|
+
{"id": "agent.after_start", "target": "src/agent.py", "position": "after:start_agent"},
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_topological_order_with_skeleton_first(self):
|
|
35
|
+
a = _mf("cap-a", deps=["conversation-core"])
|
|
36
|
+
b = _mf("cap-b", deps=["conversation-core", "cap-a"])
|
|
37
|
+
graph = mr.resolve([a, self.skeleton, b])
|
|
38
|
+
self.assertEqual(graph.skeleton.name, "conversation-core")
|
|
39
|
+
self.assertEqual(graph.order[0], "conversation-core")
|
|
40
|
+
# b must come after a
|
|
41
|
+
self.assertGreater(graph.order.index("cap-b"), graph.order.index("cap-a"))
|
|
42
|
+
|
|
43
|
+
def test_circular_dependency_detected(self):
|
|
44
|
+
a = _mf("cap-a", deps=["cap-b", "conversation-core"])
|
|
45
|
+
b = _mf("cap-b", deps=["cap-a", "conversation-core"])
|
|
46
|
+
with self.assertRaises(mr.CircularDependencyError):
|
|
47
|
+
mr.resolve([self.skeleton, a, b])
|
|
48
|
+
|
|
49
|
+
def test_unknown_injection_point_rejected(self):
|
|
50
|
+
bad = _mf(
|
|
51
|
+
"cap-x",
|
|
52
|
+
deps=["conversation-core"],
|
|
53
|
+
ext=[{"inject_at": "no.such.point"}],
|
|
54
|
+
)
|
|
55
|
+
with self.assertRaises(mr.UnknownInjectionPointError):
|
|
56
|
+
mr.resolve([self.skeleton, bad])
|
|
57
|
+
|
|
58
|
+
def test_known_injection_point_accepted(self):
|
|
59
|
+
ok = _mf(
|
|
60
|
+
"cap-y",
|
|
61
|
+
deps=["conversation-core"],
|
|
62
|
+
ext=[{"inject_at": "agent.before_start"}],
|
|
63
|
+
)
|
|
64
|
+
graph = mr.resolve([self.skeleton, ok])
|
|
65
|
+
self.assertIn("cap-y", graph.order)
|
|
66
|
+
|
|
67
|
+
def test_version_conflict_blocked(self):
|
|
68
|
+
strict = _mf("cap-z", deps=[("conversation-core", ">=2.0.0")])
|
|
69
|
+
with self.assertRaises(mr.VersionConflictError):
|
|
70
|
+
mr.resolve([self.skeleton, strict])
|
|
71
|
+
|
|
72
|
+
def test_semver_caret_compatibility(self):
|
|
73
|
+
self.assertTrue(mr.satisfies("1.4.2", "^1.0.0"))
|
|
74
|
+
self.assertFalse(mr.satisfies("2.0.0", "^1.0.0"))
|
|
75
|
+
self.assertTrue(mr.satisfies("1.4.0", ">=1.0.0,<2.0.0"))
|
|
76
|
+
|
|
77
|
+
def test_skeleton_uniqueness(self):
|
|
78
|
+
s2 = _mf("another-skeleton", typ="skeleton")
|
|
79
|
+
with self.assertRaises(mr.ManifestError):
|
|
80
|
+
mr.resolve([self.skeleton, s2])
|
|
81
|
+
|
|
82
|
+
def test_real_project_capabilities_resolve(self):
|
|
83
|
+
"""The 6 capabilities in the real repo should all resolve correctly."""
|
|
84
|
+
manifests = mr.discover_manifests(_ROOT / "capabilities")
|
|
85
|
+
self.assertGreaterEqual(len(manifests), 6)
|
|
86
|
+
graph = mr.resolve(manifests)
|
|
87
|
+
self.assertEqual(graph.order[0], "conversation-core")
|
|
88
|
+
for n in ("knowledge-base", "tool-calling", "human-handoff",
|
|
89
|
+
"session-summary", "digital-human"):
|
|
90
|
+
self.assertIn(n, graph.order)
|
|
91
|
+
self.assertGreater(graph.order.index(n), graph.order.index("conversation-core"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
unittest.main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Phase 3 Stage 6: scenarios/* recipe static validation + Path B Jinja template rendering tests.
|
|
2
|
+
|
|
3
|
+
Coverage targets:
|
|
4
|
+
- Path A: scenarios/customer-service/recipe.yaml has valid structure and key fields present
|
|
5
|
+
- Path B: output-templates/recipe.yaml.j2 rendered with 4 typical Q1~Q4 answer sets produces valid YAML with correct field linkage
|
|
6
|
+
- ui_overlay referenced subdirectories widget-floating / admin-board all physically exist
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
sys.path.insert(0, str(_ROOT))
|
|
18
|
+
|
|
19
|
+
_SCEN = _ROOT / "scenarios"
|
|
20
|
+
_RECIPE_A = _SCEN / "customer-service" / "recipe.yaml"
|
|
21
|
+
_TEMPLATE = _SCEN / "custom-builder" / "output-templates" / "recipe.yaml.j2"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CustomerServiceRecipeTests(unittest.TestCase):
|
|
25
|
+
"""Path A default recipe static structure validation."""
|
|
26
|
+
|
|
27
|
+
def setUp(self):
|
|
28
|
+
self.assertTrue(_RECIPE_A.exists(), f"recipe.yaml not found: {_RECIPE_A}")
|
|
29
|
+
self.recipe = yaml.safe_load(_RECIPE_A.read_text(encoding="utf-8"))
|
|
30
|
+
|
|
31
|
+
def test_top_level_kind_and_api_version(self):
|
|
32
|
+
self.assertEqual(self.recipe.get("kind"), "Recipe")
|
|
33
|
+
self.assertEqual(self.recipe.get("apiVersion"), "ai-customer-service/v1")
|
|
34
|
+
|
|
35
|
+
def test_install_capabilities_contain_kb_and_handoff(self):
|
|
36
|
+
installs = self.recipe["capabilities"]["install"]
|
|
37
|
+
names = {c["name"] for c in installs}
|
|
38
|
+
self.assertIn("knowledge-base", names)
|
|
39
|
+
self.assertIn("human-handoff", names)
|
|
40
|
+
# adapter field must be explicitly given (out-of-box experience relies on mock / local_queue)
|
|
41
|
+
for c in installs:
|
|
42
|
+
self.assertIn("adapter", c)
|
|
43
|
+
self.assertIn("env", c)
|
|
44
|
+
|
|
45
|
+
def test_excluded_capability_is_digital_human(self):
|
|
46
|
+
excluded = self.recipe["capabilities"].get("excluded") or []
|
|
47
|
+
names = {c["name"] for c in excluded}
|
|
48
|
+
self.assertIn("digital-human", names)
|
|
49
|
+
|
|
50
|
+
def test_ui_overlay_layers_reference_real_dirs(self):
|
|
51
|
+
layers = self.recipe["ui_overlay"]["layers"]
|
|
52
|
+
ui_root = _SCEN / "customer-service" / "ui"
|
|
53
|
+
for layer in layers:
|
|
54
|
+
sub = ui_root / layer["source"]
|
|
55
|
+
self.assertTrue(sub.is_dir(), f"missing UI overlay source dir: {sub}")
|
|
56
|
+
|
|
57
|
+
def test_post_assembly_health_check_url(self):
|
|
58
|
+
hc = self.recipe["post_assembly"]["health_check"]
|
|
59
|
+
self.assertEqual(hc["url"], "http://localhost:3000/api/v1/health")
|
|
60
|
+
|
|
61
|
+
def test_design_locks_dark_no_emoji(self):
|
|
62
|
+
d = self.recipe["design"]
|
|
63
|
+
self.assertEqual(d["theme"], "dark")
|
|
64
|
+
self.assertFalse(d["emoji_in_ui"])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Path B: Jinja2 template rendering (falls back to substring checks if Jinja2 unavailable)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
try:
|
|
71
|
+
import jinja2 # type: ignore
|
|
72
|
+
_HAS_JINJA = True
|
|
73
|
+
except ImportError: # pragma: no cover
|
|
74
|
+
_HAS_JINJA = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CustomBuilderTemplateStructureTests(unittest.TestCase):
|
|
78
|
+
"""The key Jinja branch literals must exist in the template (does not depend on Jinja2 engine)."""
|
|
79
|
+
|
|
80
|
+
def setUp(self):
|
|
81
|
+
self.assertTrue(_TEMPLATE.exists(), f"template not found: {_TEMPLATE}")
|
|
82
|
+
self.text = _TEMPLATE.read_text(encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
def test_io_modality_branches_present(self):
|
|
85
|
+
for modality in ("text_only", "text_with_tts", "omni", "voice_only"):
|
|
86
|
+
self.assertIn(modality, self.text)
|
|
87
|
+
|
|
88
|
+
def test_ui_form_branches_present(self):
|
|
89
|
+
for form in ("floating", "fullscreen", "headless"):
|
|
90
|
+
self.assertIn(form, self.text)
|
|
91
|
+
|
|
92
|
+
def test_capability_branches_present(self):
|
|
93
|
+
for cap in (
|
|
94
|
+
"knowledge-base",
|
|
95
|
+
"human-handoff",
|
|
96
|
+
"tool-calling",
|
|
97
|
+
"session-summary",
|
|
98
|
+
):
|
|
99
|
+
self.assertIn(cap, self.text)
|
|
100
|
+
|
|
101
|
+
def test_excludes_digital_human(self):
|
|
102
|
+
self.assertIn("digital-human", self.text)
|
|
103
|
+
self.assertIn("excluded", self.text)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@unittest.skipUnless(_HAS_JINJA, "jinja2 not installed; skip render assertions")
|
|
107
|
+
class CustomBuilderTemplateRenderTests(unittest.TestCase):
|
|
108
|
+
"""Jinja2 渲染后的 YAML 应可被 yaml.safe_load 解析,且关键字段按 Q 答联动。"""
|
|
109
|
+
|
|
110
|
+
def _render(self, **ctx) -> str:
|
|
111
|
+
env = jinja2.Environment( # type: ignore[name-defined]
|
|
112
|
+
keep_trailing_newline=True,
|
|
113
|
+
trim_blocks=False,
|
|
114
|
+
lstrip_blocks=False,
|
|
115
|
+
)
|
|
116
|
+
tpl = env.from_string(_TEMPLATE.read_text(encoding="utf-8"))
|
|
117
|
+
return tpl.render(**ctx)
|
|
118
|
+
|
|
119
|
+
def test_render_text_with_tts_floating_kb_only(self):
|
|
120
|
+
out = self._render(
|
|
121
|
+
business_desc="我们是一家咖啡品牌的电商客服",
|
|
122
|
+
business_name="ACME",
|
|
123
|
+
io_modality="text_with_tts",
|
|
124
|
+
ui_form="floating",
|
|
125
|
+
extra_capabilities=["knowledge-base"],
|
|
126
|
+
)
|
|
127
|
+
data = yaml.safe_load(out)
|
|
128
|
+
self.assertEqual(data["apiVersion"], "ai-customer-service/v1")
|
|
129
|
+
# Q2 linkage
|
|
130
|
+
self.assertFalse(data["runtime_modality"]["voice_input"])
|
|
131
|
+
self.assertTrue(data["runtime_modality"]["voice_output"])
|
|
132
|
+
# Q3 联动
|
|
133
|
+
self.assertTrue(data["ui"]["overlay_required"])
|
|
134
|
+
# human-handoff not selected → admin-board layer not present
|
|
135
|
+
layer_sources = [l["source"] for l in data["ui"]["ui_overlay"]["layers"]]
|
|
136
|
+
self.assertIn("widget-floating", layer_sources)
|
|
137
|
+
self.assertNotIn("admin-board", layer_sources)
|
|
138
|
+
# Q4: only knowledge-base installed
|
|
139
|
+
installed_names = {c["name"] for c in data["capabilities"]["install"]}
|
|
140
|
+
self.assertEqual(installed_names, {"knowledge-base"})
|
|
141
|
+
|
|
142
|
+
def test_render_omni_fullscreen_with_handoff(self):
|
|
143
|
+
out = self._render(
|
|
144
|
+
business_desc="车企售后客服",
|
|
145
|
+
business_name="XYZ",
|
|
146
|
+
io_modality="omni",
|
|
147
|
+
ui_form="fullscreen",
|
|
148
|
+
extra_capabilities=["knowledge-base", "human-handoff"],
|
|
149
|
+
)
|
|
150
|
+
data = yaml.safe_load(out)
|
|
151
|
+
self.assertTrue(data["runtime_modality"]["voice_input"])
|
|
152
|
+
self.assertTrue(data["runtime_modality"]["text_input"])
|
|
153
|
+
# human-handoff selected → admin-board layer should exist
|
|
154
|
+
layer_sources = [l["source"] for l in data["ui"]["ui_overlay"]["layers"]]
|
|
155
|
+
self.assertIn("admin-board", layer_sources)
|
|
156
|
+
installed_names = {c["name"] for c in data["capabilities"]["install"]}
|
|
157
|
+
self.assertEqual(installed_names, {"knowledge-base", "human-handoff"})
|
|
158
|
+
|
|
159
|
+
def test_render_headless_no_overlay(self):
|
|
160
|
+
out = self._render(
|
|
161
|
+
business_desc="纯 API 后端客服",
|
|
162
|
+
business_name="BackOnly",
|
|
163
|
+
io_modality="text_only",
|
|
164
|
+
ui_form="headless",
|
|
165
|
+
extra_capabilities=[],
|
|
166
|
+
)
|
|
167
|
+
data = yaml.safe_load(out)
|
|
168
|
+
self.assertFalse(data["ui"]["overlay_required"])
|
|
169
|
+
self.assertIsNone(data["ui"]["ui_overlay"])
|
|
170
|
+
# User selected no capabilities → install list is empty
|
|
171
|
+
self.assertEqual(data["capabilities"]["install"], [])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
unittest.main()
|