feat: add deepseek-harness workbench plugin and console diagnosis API
- console: GET /api/workflows/{id}, /runs/{run_id}, /runs/{run_id}/diagnosis
(journal trace + sanitized bounded log tails, workflow/run pairing enforced)
- workbench/: dsh 宿主插件(7 个工作流工具、中文系统提示词、失败监控器、
本机回环桥接服务)+ 侧边栏面板客户端包(sidebar.footer.action 与
shell.overlay 追加插槽)+ 降级独立面板 + 一键启动脚本
- adapters/browser: 收敛 looks_like_login_url 到共享层,修复
jd_main_image_collector 对 gyxx_flow.accounts 的越层导入
- tests: 新端点覆盖;replay_policy 断言对齐已迁移的 catalog(repeatable)
This commit is contained in:
+147
-1
@@ -546,7 +546,7 @@ def test_overview_tracks_the_current_complete_catalog_without_leaking_paths(
|
||||
assert failed["last_run"]["mode"] == "execute"
|
||||
assert failed["last_run"]["error"] == "critical steps failed: collect"
|
||||
assert all(step["name"] and step["description"] for step in failed["steps"])
|
||||
assert {step["replay_policy"] for step in failed["steps"]} == {"guarded"}
|
||||
assert {step["replay_policy"] for step in failed["steps"]} == {"repeatable"}
|
||||
notes_master = next(
|
||||
item
|
||||
for item in initial["workflows"]
|
||||
@@ -3271,3 +3271,149 @@ def test_scheduler_reload_catalog_runs_before_each_tick(tmp_path: Path) -> None:
|
||||
assert scheduler.tick(now) == []
|
||||
assert scheduler.catalog is disabled_catalog
|
||||
assert load_calls == [due_catalog, disabled_catalog]
|
||||
|
||||
|
||||
def test_workflow_detail_returns_a_single_workflow(
|
||||
console_settings: Settings,
|
||||
) -> None:
|
||||
service = WorkflowConsoleService(
|
||||
console_settings,
|
||||
launcher=FakeConsoleLauncher(),
|
||||
)
|
||||
|
||||
detail = service.workflow_detail("content.metrics.daily")
|
||||
|
||||
assert detail["workflow"]["id"] == "content.metrics.daily"
|
||||
assert detail["workflow"]["module"] == "content_marketing"
|
||||
|
||||
with pytest.raises(ConsoleNotFoundError):
|
||||
service.workflow_detail("content.unknown")
|
||||
|
||||
|
||||
def test_run_detail_returns_journal_steps(
|
||||
console_settings: Settings,
|
||||
) -> None:
|
||||
run_id = _index_parallel_main_image_run(console_settings)
|
||||
service = WorkflowConsoleService(
|
||||
console_settings,
|
||||
launcher=FakeConsoleLauncher(),
|
||||
)
|
||||
|
||||
detail = service.run_detail("product.main_image.weekly", run_id)
|
||||
|
||||
assert detail["workflow_id"] == "product.main_image.weekly"
|
||||
assert detail["run"]["run_id"] == run_id
|
||||
assert detail["run"]["status"] == "failed"
|
||||
assert [step["id"] for step in detail["run"]["steps"]] == [
|
||||
"jd.attempt-1",
|
||||
"tmall.attempt-1",
|
||||
]
|
||||
|
||||
with pytest.raises(ConsoleNotFoundError):
|
||||
service.run_detail("content.metrics.daily", run_id)
|
||||
with pytest.raises(ConsoleNotFoundError):
|
||||
service.run_detail("product.main_image.weekly", "missing-run")
|
||||
with pytest.raises(ConsoleNotFoundError):
|
||||
service.run_detail("product.main_image.weekly", "../escape")
|
||||
|
||||
|
||||
def test_run_diagnosis_bundles_trace_and_sanitized_log_tails(
|
||||
console_settings: Settings,
|
||||
) -> None:
|
||||
run_id = _index_parallel_main_image_run(console_settings)
|
||||
layout = DataLayout(console_settings.data_root)
|
||||
log_dir = layout.log_dir("product.main_image.weekly", "2026-08-02")
|
||||
secret = "hunter" + "2"
|
||||
(log_dir / "jd.attempt-1.log").write_text(
|
||||
"采集开始\npass" + "word=" + secret + "\n浏览器崩溃\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(log_dir / "empty.log").write_text("", encoding="utf-8")
|
||||
service = WorkflowConsoleService(
|
||||
console_settings,
|
||||
launcher=FakeConsoleLauncher(),
|
||||
)
|
||||
|
||||
diagnosis = service.run_diagnosis("product.main_image.weekly", run_id)
|
||||
|
||||
assert diagnosis["workflow_id"] == "product.main_image.weekly"
|
||||
assert diagnosis["run"]["run_id"] == run_id
|
||||
journal = diagnosis["journal"]
|
||||
assert journal["path"].endswith("/run.json")
|
||||
assert journal["trace"]["paths"]["log"].startswith("logs/")
|
||||
assert len(diagnosis["logs"]) == 1
|
||||
tail = diagnosis["logs"][0]["tail"]
|
||||
assert diagnosis["logs"][0]["path"].endswith("jd.attempt-1.log")
|
||||
assert "浏览器崩溃" in tail
|
||||
assert secret not in tail
|
||||
assert "[REDACTED]" in tail
|
||||
|
||||
|
||||
def test_run_diagnosis_handles_a_missing_journal(
|
||||
console_settings: Settings,
|
||||
) -> None:
|
||||
context = RunContext.create(
|
||||
"product.main_image.weekly",
|
||||
"2026-08-02",
|
||||
now=datetime(2026, 8, 2, 3, 4, 5, tzinfo=timezone.utc),
|
||||
random_suffix="broken1",
|
||||
)
|
||||
journal = RunJournal.create(DataLayout(console_settings.data_root), context)
|
||||
journal.finalize("failed", error="launch failed")
|
||||
RunIndex(console_settings.data_root).index_journal(journal)
|
||||
journal.path.unlink()
|
||||
service = WorkflowConsoleService(
|
||||
console_settings,
|
||||
launcher=FakeConsoleLauncher(),
|
||||
)
|
||||
|
||||
diagnosis = service.run_diagnosis("product.main_image.weekly", context.run_id)
|
||||
|
||||
assert diagnosis["journal"] == {"path": None, "mode": None, "trace": None}
|
||||
assert diagnosis["logs"] == []
|
||||
assert diagnosis["run"]["status"] == "failed"
|
||||
|
||||
|
||||
def test_http_workflow_detail_and_diagnosis_routes(
|
||||
console_settings: Settings,
|
||||
) -> None:
|
||||
run_id = _index_parallel_main_image_run(console_settings)
|
||||
with _running_server(console_settings, FakeConsoleLauncher()) as server:
|
||||
status, headers, content = _http_request(
|
||||
server, "GET", "/api/workflows/product.main_image.weekly"
|
||||
)
|
||||
assert status == HTTPStatus.OK
|
||||
_assert_security_headers(headers)
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
assert payload["workflow"]["id"] == "product.main_image.weekly"
|
||||
|
||||
status, _, _ = _http_request(server, "GET", "/api/workflows/unknown")
|
||||
assert status == HTTPStatus.NOT_FOUND
|
||||
|
||||
status, headers, content = _http_request(
|
||||
server,
|
||||
"GET",
|
||||
f"/api/workflows/product.main_image.weekly/runs/{run_id}",
|
||||
)
|
||||
assert status == HTTPStatus.OK
|
||||
_assert_security_headers(headers)
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
assert payload["run"]["run_id"] == run_id
|
||||
|
||||
status, _, content = _http_request(
|
||||
server,
|
||||
"GET",
|
||||
f"/api/workflows/product.main_image.weekly/runs/{run_id}/diagnosis",
|
||||
)
|
||||
assert status == HTTPStatus.OK
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
assert payload["workflow_id"] == "product.main_image.weekly"
|
||||
assert payload["journal"]["trace"] is not None
|
||||
assert payload["logs"] == []
|
||||
|
||||
status, _, _ = _http_request(
|
||||
server,
|
||||
"GET",
|
||||
f"/api/workflows/content.metrics.daily/runs/{run_id}",
|
||||
)
|
||||
assert status == HTTPStatus.NOT_FOUND
|
||||
|
||||
Reference in New Issue
Block a user