基于 disburse-payroll-icbc 实战经验沉淀,让所有未来新 skill 复制模板后直接 享受本系列全部改进: - record_screencast.py.sample: · 字幕脚本改用 "step: N/X" 模式 + ASCII 关键词锚(避免编码问题) · 展示三元组 (keyword, text, duration) 用法(依赖 platform-kit subtitle.py) · 加 TEST_FILE_NAME / MUSIC_SUBDIR 切换常量 · 文档说明绕开 SDK ask() / send_file 的最佳实践 - test_desktop_smoke.py.sample: · docstring 加"何时用此模板 / 何时改用 _with_attachment"指引 · 标注已知 SDK 限制(ask() user_idx<0 / send_file NotImplementedError) - test_desktop_smoke_with_attachment.py.sample(新增): · 完整附件场景模板,import jiangchang_desktop_sdk.e2e_helpers 6 个 API · 7 步 step: N/7 模式,跟 record_screencast.py.sample 字幕脚本对齐 · 占位符 SKILL_SLUG / PROMPT / EXPECTED_KEYWORDS_IN_REPLY - README.md: 追加 3 节 · 模板选择指南(纯问答 vs 附件场景) · 已知 SDK 坑(send_file / ask / 字幕关键词 / bring_to_front) · 录屏新能力(activate_window / monitor_index / 字幕三元组) - conftest.py: 同步到支持新版 platform-kit src/ 布局的 SDK 路径推断 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.6 KiB
Plaintext
138 lines
4.6 KiB
Plaintext
# -*- coding: utf-8 -*-
|
||
"""
|
||
可选桌面 E2E 示例(默认不执行:文件名后缀为 .sample)—— **附件上传 + 长流程场景**。
|
||
|
||
启用方式:
|
||
1. 复制为 test_desktop_smoke_with_attachment.py
|
||
2. 修改 SKILL_SLUG / PROMPT / 断言为你技能的真实业务
|
||
3. 准备样例附件,或动态生成(参考 _prepare_sample_attachment)
|
||
4. 运行:pytest tests/desktop/test_desktop_smoke_with_attachment.py -v -s
|
||
|
||
依赖:
|
||
- pytest / jiangchang_desktop_sdk / 匠厂桌面宿主已就绪
|
||
- 通过 conftest.py 自动 bootstrap,jiangchang_desktop_sdk.e2e_helpers 模块在 sys.path
|
||
|
||
【为什么用此模板而不是 test_desktop_smoke.py.sample】
|
||
- client.send_file() 在 v2.0.17+ 抛 NotImplementedError,必须用拖拽方式
|
||
- client.ask() 在 user_idx<0 时会锁死(已知 SDK 缺陷),本模板绕开 ask(),
|
||
在测试侧自己用 jiangchang_desktop_sdk.e2e_helpers 做等待
|
||
|
||
【字幕配合】
|
||
- 关键节点都打了 _logger.info("step: N/7 ..."),对应 record_screencast.py 的
|
||
SUBTITLE_SCRIPT 用 step: 关键词触发字幕
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from jiangchang_desktop_sdk import JiangchangDesktopClient
|
||
from jiangchang_desktop_sdk.e2e_helpers import (
|
||
drop_file_into_composer,
|
||
send_prompt_via_composer,
|
||
wait_for_agent_complete,
|
||
wait_for_attachment_ready,
|
||
wait_for_composer_enabled,
|
||
wait_for_user_message,
|
||
)
|
||
|
||
# ============================================================
|
||
# ↓↓↓ 复制后修改这些 ↓↓↓
|
||
# ============================================================
|
||
|
||
SKILL_SLUG = "your-skill-slug"
|
||
|
||
PROMPT = (
|
||
"请使用 /your-skill-slug 处理我刚才上传的文件。"
|
||
"完成后请用清单方式汇总:[改成你技能要求的字段]。"
|
||
)
|
||
|
||
# 业务断言:根据你的技能输出修改
|
||
EXPECTED_KEYWORDS_IN_REPLY = ["关键词1", "关键词2"]
|
||
|
||
# ============================================================
|
||
|
||
_logger = logging.getLogger("desktop-e2e-with-attachment-sample")
|
||
if not _logger.handlers:
|
||
_logger.setLevel(logging.INFO)
|
||
_h = logging.StreamHandler()
|
||
_h.setFormatter(logging.Formatter("[E2E] %(message)s"))
|
||
_logger.addHandler(_h)
|
||
|
||
_ARTIFACT_DIR = os.path.join(os.path.dirname(__file__), "artifacts")
|
||
|
||
|
||
def _prepare_sample_attachment(tmp_path: Path) -> Path:
|
||
"""在 tmp_path 下生成一个样例附件(按 skill 真实需要修改)。
|
||
|
||
示例:生成一个 5 行的 xlsx。你的 skill 可能需要 CSV / PDF / 图片等其他格式。
|
||
"""
|
||
from openpyxl import Workbook # noqa: WPS433
|
||
xlsx = tmp_path / "sample.xlsx"
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.append(["列1", "列2", "列3"])
|
||
for i in range(1, 6):
|
||
ws.append([f"值{i}A", f"值{i}B", i * 100])
|
||
wb.save(str(xlsx))
|
||
return xlsx
|
||
|
||
|
||
@pytest.fixture
|
||
def client(request):
|
||
c = JiangchangDesktopClient()
|
||
_logger.info("ensure_app_running")
|
||
c.ensure_app_running(wait_timeout_s=45)
|
||
c.bring_to_front()
|
||
c.wait_gateway_ready(timeout_ms=30000)
|
||
yield c
|
||
try:
|
||
rep = getattr(request.node, "rep_call", None)
|
||
if rep is not None and rep.failed:
|
||
os.makedirs(_ARTIFACT_DIR, exist_ok=True)
|
||
paths = c.snapshot(_ARTIFACT_DIR, tag=request.node.name)
|
||
_logger.error("failure snapshot: %s", paths)
|
||
finally:
|
||
c.disconnect()
|
||
|
||
|
||
def test_desktop_with_attachment(client: JiangchangDesktopClient, tmp_path: Path) -> None:
|
||
_logger.info("step: 1/7 准备样例附件")
|
||
attachment_path = _prepare_sample_attachment(tmp_path)
|
||
|
||
client.new_task()
|
||
page = client.get_page()
|
||
client.bring_to_front()
|
||
|
||
_logger.info("step: 2/7 拖拽附件到聊天框")
|
||
drop_file_into_composer(page, attachment_path)
|
||
|
||
_logger.info("step: 3/7 等附件 stage-buffer 完成")
|
||
wait_for_attachment_ready(page, attachment_path.name)
|
||
|
||
wait_for_composer_enabled(page)
|
||
client.bring_to_front()
|
||
_logger.info("step: 4/7 键入并发送指令")
|
||
send_prompt_via_composer(page, PROMPT)
|
||
|
||
_logger.info("step: 5/7 Main Agent 接收,等待路由到技能")
|
||
user_idx = wait_for_user_message(page, PROMPT, timeout_s=60.0)
|
||
|
||
_logger.info("step: 6/7 技能在后端执行(可能 2-3 分钟)")
|
||
client.bring_to_front()
|
||
reply = wait_for_agent_complete(
|
||
page, user_idx,
|
||
overall_timeout_s=1200.0,
|
||
stable_seconds=5.0,
|
||
)
|
||
|
||
_logger.info("step: 7/7 校验回复内容")
|
||
_logger.info("reply length=%d", len(reply))
|
||
|
||
assert reply, "assistant 回复为空"
|
||
for kw in EXPECTED_KEYWORDS_IN_REPLY:
|
||
assert kw in reply, f"期望关键词 {kw!r} 未出现在回复中;片段:{reply[:400]!r}"
|