add skill activity stream and job runtime foundation (v1.1.0)
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 17s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 17s
Introduce SAS v1 emit/finish/journal API, sibling CLI bridge, RPA video session integration, and tests for the host Job Runner activity timeline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
209
tests/test_activity_emit_finish.py
Normal file
209
tests/test_activity_emit_finish.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Skill Activity Stream: journal, flush, job_id, finish stdout, truncation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from jiangchang_skill_core import activity
|
||||
from jiangchang_skill_core.runtime_env import get_runs_dir
|
||||
|
||||
|
||||
class _ActivityEnvIsolation(unittest.TestCase):
|
||||
_KEYS = (
|
||||
"JIANGCHANG_DATA_ROOT",
|
||||
"JIANGCHANG_USER_ID",
|
||||
"JIANGCHANG_JOB_ID",
|
||||
"JIANGCHANG_RUN_MODE",
|
||||
"JIANGCHANG_ACTIVITY_DEBUG",
|
||||
)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._saved = {k: os.environ.get(k) for k in self._KEYS}
|
||||
self._tmps: list[str] = []
|
||||
for k in self._KEYS:
|
||||
os.environ.pop(k, None)
|
||||
activity._reset_for_tests()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
activity._reset_for_tests()
|
||||
for k, v in self._saved.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
for tmp in self._tmps:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
def _tmp_data_root(self) -> str:
|
||||
tmp = tempfile.mkdtemp()
|
||||
self._tmps.append(tmp)
|
||||
return tmp
|
||||
|
||||
|
||||
class TestActivityJournal(_ActivityEnvIsolation):
|
||||
def test_emit_writes_ndjson_with_meta(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-test-1"
|
||||
activity._reset_for_tests()
|
||||
|
||||
activity.emit("步骤一", type="activity", skill="geo-batch")
|
||||
journal = Path(get_runs_dir()) / "job-test-1.jsonl"
|
||||
self.assertTrue(journal.is_file())
|
||||
lines = journal.read_text(encoding="utf-8").strip().splitlines()
|
||||
self.assertGreaterEqual(len(lines), 2)
|
||||
meta = json.loads(lines[0])
|
||||
self.assertEqual(meta["type"], "meta")
|
||||
self.assertEqual(meta["job_id"], "job-test-1")
|
||||
event = json.loads(lines[1])
|
||||
self.assertEqual(event["type"], "activity")
|
||||
self.assertEqual(event["text"], "步骤一")
|
||||
self.assertEqual(event["schema_version"], 1)
|
||||
self.assertEqual(event["skill"], "geo-batch")
|
||||
self.assertIn("ts", event)
|
||||
|
||||
def test_emit_flushes_immediately(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-flush"
|
||||
activity._reset_for_tests()
|
||||
activity.emit("flush test")
|
||||
journal = Path(get_runs_dir()) / "job-flush.jsonl"
|
||||
self.assertGreater(journal.stat().st_size, 0)
|
||||
|
||||
def test_job_id_from_env(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "env-job-42"
|
||||
activity._reset_for_tests()
|
||||
self.assertEqual(activity.get_job_id(), "env-job-42")
|
||||
|
||||
def test_job_id_generated_and_cached(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
activity._reset_for_tests()
|
||||
jid1 = activity.get_job_id()
|
||||
jid2 = activity.get_job_id()
|
||||
self.assertEqual(jid1, jid2)
|
||||
self.assertEqual(len(jid1), 32)
|
||||
|
||||
def test_debug_skipped_without_env(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-no-debug"
|
||||
activity._reset_for_tests()
|
||||
activity.emit("hidden", type="debug")
|
||||
journal = Path(get_runs_dir()) / "job-no-debug.jsonl"
|
||||
self.assertFalse(journal.exists())
|
||||
|
||||
def test_debug_written_when_enabled(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-debug"
|
||||
os.environ["JIANGCHANG_ACTIVITY_DEBUG"] = "1"
|
||||
activity._reset_for_tests()
|
||||
activity.emit("dbg", type="debug")
|
||||
journal = Path(get_runs_dir()) / "job-debug.jsonl"
|
||||
content = journal.read_text(encoding="utf-8")
|
||||
self.assertIn('"type":"debug"', content.replace(" ", ""))
|
||||
|
||||
def test_emit_never_prints_stdout(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-quiet"
|
||||
activity._reset_for_tests()
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", buf):
|
||||
activity.emit("silent step")
|
||||
self.assertEqual(buf.getvalue(), "")
|
||||
|
||||
def test_get_run_mode_default_cli(self) -> None:
|
||||
self.assertEqual(activity.get_run_mode(), "cli")
|
||||
|
||||
def test_get_run_mode_job(self) -> None:
|
||||
os.environ["JIANGCHANG_RUN_MODE"] = "job"
|
||||
self.assertEqual(activity.get_run_mode(), "job")
|
||||
|
||||
|
||||
class TestActivityFinish(_ActivityEnvIsolation):
|
||||
def test_finish_stdout_single_line_result(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-finish"
|
||||
activity._reset_for_tests()
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", buf):
|
||||
activity.finish(status="success", message="ok", skill="geo", generated=3)
|
||||
|
||||
lines = [ln for ln in buf.getvalue().splitlines() if ln.strip()]
|
||||
self.assertEqual(len(lines), 1)
|
||||
result = json.loads(lines[0])
|
||||
self.assertEqual(result["type"], "result")
|
||||
self.assertEqual(result["schema_version"], 1)
|
||||
self.assertEqual(result["status"], "success")
|
||||
self.assertEqual(result["skill"], "geo")
|
||||
self.assertEqual(result["generated"], 3)
|
||||
|
||||
journal = Path(get_runs_dir()) / "job-finish.jsonl"
|
||||
journal_lines = journal.read_text(encoding="utf-8").strip().splitlines()
|
||||
last = json.loads(journal_lines[-1])
|
||||
self.assertEqual(last["type"], "done")
|
||||
self.assertEqual(last["status"], "success")
|
||||
|
||||
def test_finish_failed_writes_error_event(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-fail"
|
||||
activity._reset_for_tests()
|
||||
|
||||
with patch("sys.stdout", io.StringIO()):
|
||||
activity.finish(status="failed", message="boom")
|
||||
|
||||
journal = Path(get_runs_dir()) / "job-fail.jsonl"
|
||||
last = json.loads(journal.read_text(encoding="utf-8").strip().splitlines()[-1])
|
||||
self.assertEqual(last["type"], "error")
|
||||
|
||||
def test_finish_truncates_large_result(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "job-trunc"
|
||||
activity._reset_for_tests()
|
||||
|
||||
big = {f"field_{i}": "x" * 200 for i in range(50)}
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", buf):
|
||||
activity.finish(status="success", skill="big", **big)
|
||||
|
||||
line = buf.getvalue().strip()
|
||||
self.assertLessEqual(len(line), 4096)
|
||||
result = json.loads(line)
|
||||
self.assertTrue(result.get("truncated"))
|
||||
|
||||
|
||||
class TestJobContext(_ActivityEnvIsolation):
|
||||
def test_job_context_auto_finish_on_exception(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
activity._reset_for_tests()
|
||||
|
||||
with patch("sys.stdout", io.StringIO()):
|
||||
with self.assertRaises(ValueError):
|
||||
with activity.job_context(job_id="ctx-err", skill="test-skill"):
|
||||
raise ValueError("broken")
|
||||
|
||||
journal = Path(get_runs_dir()) / "ctx-err.jsonl"
|
||||
last = json.loads(journal.read_text(encoding="utf-8").strip().splitlines()[-1])
|
||||
self.assertEqual(last["type"], "error")
|
||||
self.assertIn("broken", last["message"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
120
tests/test_activity_rpa_step_decorator.py
Normal file
120
tests/test_activity_rpa_step_decorator.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""rpa_step decorator: sync + async."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from jiangchang_skill_core import activity
|
||||
from jiangchang_skill_core.runtime_env import get_runs_dir
|
||||
|
||||
|
||||
class _DecoratorEnvIsolation(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._saved_root = os.environ.get("JIANGCHANG_DATA_ROOT")
|
||||
self._saved_job = os.environ.get("JIANGCHANG_JOB_ID")
|
||||
self._tmps: list[str] = []
|
||||
os.environ.pop("JIANGCHANG_JOB_ID", None)
|
||||
activity._reset_for_tests()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
activity._reset_for_tests()
|
||||
if self._saved_root is None:
|
||||
os.environ.pop("JIANGCHANG_DATA_ROOT", None)
|
||||
else:
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = self._saved_root
|
||||
if self._saved_job is None:
|
||||
os.environ.pop("JIANGCHANG_JOB_ID", None)
|
||||
else:
|
||||
os.environ["JIANGCHANG_JOB_ID"] = self._saved_job
|
||||
for tmp in self._tmps:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
def _tmp_data_root(self) -> str:
|
||||
tmp = tempfile.mkdtemp()
|
||||
self._tmps.append(tmp)
|
||||
return tmp
|
||||
|
||||
|
||||
class TestRpaStepDecorator(_DecoratorEnvIsolation):
|
||||
def _journal_texts(self, job_id: str) -> list[str]:
|
||||
journal = Path(get_runs_dir()) / f"{job_id}.jsonl"
|
||||
lines = journal.read_text(encoding="utf-8").strip().splitlines()
|
||||
events = [json.loads(ln) for ln in lines if json.loads(ln).get("type") != "meta"]
|
||||
return [e["text"] for e in events]
|
||||
|
||||
def test_sync_success(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "rpa-sync-ok"
|
||||
activity._reset_for_tests()
|
||||
|
||||
@activity.rpa_step("打开浏览器")
|
||||
def open_browser() -> str:
|
||||
return "ok"
|
||||
|
||||
self.assertEqual(open_browser(), "ok")
|
||||
texts = self._journal_texts("rpa-sync-ok")
|
||||
self.assertIn("▶ 打开浏览器", texts)
|
||||
self.assertIn("✓ 打开浏览器", texts)
|
||||
|
||||
def test_sync_failure_emits_warn_and_reraises(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "rpa-sync-fail"
|
||||
activity._reset_for_tests()
|
||||
|
||||
@activity.rpa_step("提交表单")
|
||||
def submit() -> None:
|
||||
raise RuntimeError("timeout")
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
submit()
|
||||
|
||||
journal = Path(get_runs_dir()) / "rpa-sync-fail.jsonl"
|
||||
content = journal.read_text(encoding="utf-8")
|
||||
self.assertIn("timeout", content)
|
||||
self.assertIn('"type":"warn"', content.replace(" ", ""))
|
||||
|
||||
def test_async_success(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "rpa-async-ok"
|
||||
activity._reset_for_tests()
|
||||
|
||||
@activity.rpa_step("异步步骤")
|
||||
async def async_step() -> int:
|
||||
await asyncio.sleep(0)
|
||||
return 42
|
||||
|
||||
result = asyncio.run(async_step())
|
||||
self.assertEqual(result, 42)
|
||||
texts = self._journal_texts("rpa-async-ok")
|
||||
self.assertIn("▶ 异步步骤", texts)
|
||||
self.assertIn("✓ 异步步骤", texts)
|
||||
|
||||
def test_async_failure(self) -> None:
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "rpa-async-fail"
|
||||
activity._reset_for_tests()
|
||||
|
||||
@activity.rpa_step()
|
||||
async def fail_step() -> None:
|
||||
await asyncio.sleep(0)
|
||||
raise ValueError("async err")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
asyncio.run(fail_step())
|
||||
|
||||
journal = Path(get_runs_dir()) / "rpa-async-fail.jsonl"
|
||||
self.assertIn("async err", journal.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
72
tests/test_sibling_bridge.py
Normal file
72
tests/test_sibling_bridge.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""sibling_bridge: mock subprocess sibling CLI calls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from jiangchang_skill_core.sibling_bridge import (
|
||||
call_sibling_json,
|
||||
call_sibling_json_array,
|
||||
get_sibling_main_path,
|
||||
)
|
||||
|
||||
|
||||
class TestSiblingBridge(unittest.TestCase):
|
||||
def test_get_sibling_main_path(self) -> None:
|
||||
with patch.dict(os.environ, {"JIANGCHANG_SKILLS_ROOT": r"D:\skills"}):
|
||||
path = get_sibling_main_path("account-manager")
|
||||
self.assertTrue(path.replace("\\", "/").endswith("account-manager/scripts/main.py"))
|
||||
|
||||
@patch("jiangchang_skill_core.sibling_bridge.subprocess.run")
|
||||
def test_call_sibling_json_success(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = subprocess.CompletedProcess(
|
||||
args=[],
|
||||
returncode=0,
|
||||
stdout='log line\n{"id": 1, "name": "test"}\n',
|
||||
stderr="",
|
||||
)
|
||||
result = call_sibling_json("account-manager", ["list", "all"])
|
||||
self.assertEqual(result, {"id": 1, "name": "test"})
|
||||
cmd = mock_run.call_args[0][0]
|
||||
self.assertIn("account-manager", cmd[1])
|
||||
self.assertIn("list", cmd)
|
||||
self.assertTrue(mock_run.call_args[1]["capture_output"])
|
||||
|
||||
@patch("jiangchang_skill_core.sibling_bridge.subprocess.run")
|
||||
def test_call_sibling_json_nonzero_exit(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = subprocess.CompletedProcess(
|
||||
args=[], returncode=1, stdout="", stderr="ERROR:fail"
|
||||
)
|
||||
self.assertIsNone(call_sibling_json("content-manager", ["get", "x"]))
|
||||
|
||||
@patch("jiangchang_skill_core.sibling_bridge.subprocess.run")
|
||||
def test_call_sibling_json_array(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = subprocess.CompletedProcess(
|
||||
args=[],
|
||||
returncode=0,
|
||||
stdout=json.dumps([{"a": 1}, {"b": 2}]),
|
||||
stderr="",
|
||||
)
|
||||
result = call_sibling_json_array("account-manager", ["list", "all"])
|
||||
self.assertEqual(result, [{"a": 1}, {"b": 2}])
|
||||
|
||||
@patch("jiangchang_skill_core.sibling_bridge.subprocess.run")
|
||||
def test_uses_subprocess_env_with_trace(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = subprocess.CompletedProcess(
|
||||
args=[], returncode=0, stdout='{"ok":true}', stderr=""
|
||||
)
|
||||
with patch(
|
||||
"jiangchang_skill_core.sibling_bridge.subprocess_env_with_trace",
|
||||
return_value={"JIANGCHANG_TRACE_ID": "abc123"},
|
||||
) as mock_env:
|
||||
call_sibling_json("api-key-vault", ["get", "key"])
|
||||
self.assertEqual(mock_run.call_args[1]["env"], {"JIANGCHANG_TRACE_ID": "abc123"})
|
||||
mock_env.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
113
tests/test_video_session_activity_integration.py
Normal file
113
tests/test_video_session_activity_integration.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""RpaVideoSession.add_step integrates with activity.emit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from jiangchang_skill_core import activity, config
|
||||
from jiangchang_skill_core.rpa.video_session import RpaVideoSession
|
||||
from jiangchang_skill_core.runtime_env import get_runs_dir
|
||||
|
||||
|
||||
class TestVideoSessionActivityIntegration(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
config.reset_cache()
|
||||
self._saved_root = os.environ.get("JIANGCHANG_DATA_ROOT")
|
||||
self._saved_job = os.environ.get("JIANGCHANG_JOB_ID")
|
||||
self._saved_video = os.environ.get("OPENCLAW_RECORD_VIDEO")
|
||||
self._tmps: list[str] = []
|
||||
activity._reset_for_tests()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
activity._reset_for_tests()
|
||||
if self._saved_root is None:
|
||||
os.environ.pop("JIANGCHANG_DATA_ROOT", None)
|
||||
else:
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = self._saved_root
|
||||
if self._saved_job is None:
|
||||
os.environ.pop("JIANGCHANG_JOB_ID", None)
|
||||
else:
|
||||
os.environ["JIANGCHANG_JOB_ID"] = self._saved_job
|
||||
if self._saved_video is None:
|
||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||
else:
|
||||
os.environ["OPENCLAW_RECORD_VIDEO"] = self._saved_video
|
||||
config.reset_cache()
|
||||
for tmp in self._tmps:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
def _tmp_data_root(self) -> str:
|
||||
tmp = tempfile.mkdtemp()
|
||||
self._tmps.append(tmp)
|
||||
return tmp
|
||||
|
||||
def test_add_step_emits_activity_when_disabled(self) -> None:
|
||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "video-act-1"
|
||||
activity._reset_for_tests()
|
||||
config.reset_cache()
|
||||
|
||||
session = RpaVideoSession(
|
||||
skill_slug="publish-toutiao",
|
||||
skill_data_dir=tmp,
|
||||
batch_id="batch1",
|
||||
)
|
||||
self.assertFalse(session.enabled)
|
||||
session.add_step("打开浏览器")
|
||||
|
||||
journal = Path(get_runs_dir()) / "video-act-1.jsonl"
|
||||
self.assertTrue(journal.is_file())
|
||||
content = journal.read_text(encoding="utf-8")
|
||||
self.assertIn("打开浏览器", content)
|
||||
self.assertIn("rpa.video", content)
|
||||
self.assertIn("publish-toutiao", content)
|
||||
|
||||
def test_emit_activity_false_skips_journal(self) -> None:
|
||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "video-act-off"
|
||||
activity._reset_for_tests()
|
||||
|
||||
session = RpaVideoSession(
|
||||
skill_slug="publish-toutiao",
|
||||
skill_data_dir=tmp,
|
||||
batch_id="batch2",
|
||||
emit_activity=False,
|
||||
)
|
||||
session.add_step("不应写入")
|
||||
journal = Path(get_runs_dir()) / "video-act-off.jsonl"
|
||||
self.assertFalse(journal.exists())
|
||||
|
||||
def test_add_step_writes_both_journal_and_subtitle_steps(self) -> None:
|
||||
os.environ["OPENCLAW_RECORD_VIDEO"] = "1"
|
||||
tmp = self._tmp_data_root()
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||
os.environ["JIANGCHANG_JOB_ID"] = "video-act-both"
|
||||
activity._reset_for_tests()
|
||||
config.reset_cache()
|
||||
|
||||
session = RpaVideoSession(
|
||||
skill_slug="receive-order",
|
||||
skill_data_dir=tmp,
|
||||
batch_id="batch3",
|
||||
)
|
||||
session.add_step("录制步骤")
|
||||
self.assertEqual(len(session._steps), 1)
|
||||
|
||||
journal = Path(get_runs_dir()) / "video-act-both.jsonl"
|
||||
lines = [json.loads(ln) for ln in journal.read_text(encoding="utf-8").splitlines()]
|
||||
activity_events = [e for e in lines if e.get("type") == "activity"]
|
||||
self.assertEqual(len(activity_events), 1)
|
||||
self.assertEqual(activity_events[0]["text"], "录制步骤")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user