Files
jiangchang-platform-kit/tests/test_activity_stream.py
chendelian 4ae8262aa6
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 24s
feat(activity): add SAS stream_delta API for LLM progressive text (1.2.14)
2026-10-25 16:44:30 +08:00

145 lines
5.2 KiB
Python

# -*- coding: utf-8 -*-
"""SAS stream_start / stream_delta / stream_end / stream_channel."""
from __future__ import annotations
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 _ActivityEnvIsolation(unittest.TestCase):
_KEYS = (
"JIANGCHANG_DATA_ROOT",
"JIANGCHANG_USER_ID",
"JIANGCHANG_JOB_ID",
"JIANGCHANG_RUN_MODE",
"JIANGCHANG_ACTIVITY_DEBUG",
"JIANGCHANG_STREAM_COALESCE_MS",
"JIANGCHANG_STREAM_COALESCE_CHARS",
"JIANGCHANG_CONTROL_DISABLED",
)
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)
os.environ["JIANGCHANG_CONTROL_DISABLED"] = "1"
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
def _events(self, job_id: str) -> list[dict]:
journal = Path(get_runs_dir()) / f"{job_id}.jsonl"
lines = journal.read_text(encoding="utf-8").strip().splitlines()
return [json.loads(line) for line in lines]
class TestActivityStream(_ActivityEnvIsolation):
def test_stream_writes_start_delta_end_without_step_gate(self) -> None:
tmp = self._tmp_data_root()
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
os.environ["JIANGCHANG_JOB_ID"] = "job-stream-1"
os.environ["JIANGCHANG_STREAM_COALESCE_MS"] = "0"
activity._reset_for_tests()
activity.stream_start("answer", skill="demo")
activity.stream_delta("你", stream_id="answer", skill="demo")
activity.stream_delta("好", stream_id="answer", skill="demo")
activity.stream_end("answer", skill="demo")
events = self._events("job-stream-1")
stream_events = [e for e in events if e.get("type") == "stream"]
phases = [e.get("phase") for e in stream_events]
self.assertEqual(phases[0], "start")
self.assertEqual(phases[-1], "end")
deltas = [e.get("delta") for e in stream_events if e.get("phase") == "delta"]
self.assertEqual("".join(deltas), "你好")
self.assertEqual(activity._step_index, 0)
def test_stream_channel_context_manager(self) -> None:
tmp = self._tmp_data_root()
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
os.environ["JIANGCHANG_JOB_ID"] = "job-stream-ch"
os.environ["JIANGCHANG_STREAM_COALESCE_MS"] = "0"
activity._reset_for_tests()
with activity.stream_channel("answer", skill="demo") as ch:
ch.delta("A")
ch.delta("B", flush=True)
events = self._events("job-stream-ch")
stream_events = [e for e in events if e.get("type") == "stream"]
self.assertEqual(stream_events[0]["phase"], "start")
self.assertEqual(stream_events[-1]["phase"], "end")
deltas = [e.get("delta") for e in stream_events if e.get("phase") == "delta"]
self.assertEqual("".join(d for d in deltas if d), "AB")
def test_stream_delta_auto_starts(self) -> None:
tmp = self._tmp_data_root()
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
os.environ["JIANGCHANG_JOB_ID"] = "job-stream-auto"
os.environ["JIANGCHANG_STREAM_COALESCE_MS"] = "0"
activity._reset_for_tests()
activity.stream_delta("x", stream_id="answer")
activity.stream_end("answer")
events = self._events("job-stream-auto")
stream_events = [e for e in events if e.get("type") == "stream"]
self.assertEqual(stream_events[0]["phase"], "start")
self.assertTrue(any(e.get("phase") == "delta" and e.get("delta") == "x" for e in stream_events))
def test_empty_delta_ignored(self) -> None:
tmp = self._tmp_data_root()
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
os.environ["JIANGCHANG_JOB_ID"] = "job-stream-empty"
os.environ["JIANGCHANG_STREAM_COALESCE_MS"] = "0"
activity._reset_for_tests()
activity.stream_start("answer")
activity.stream_delta("", stream_id="answer")
activity.stream_end("answer")
events = self._events("job-stream-empty")
stream_events = [e for e in events if e.get("type") == "stream"]
self.assertFalse(any(e.get("phase") == "delta" for e in stream_events))
def test_public_exports(self) -> None:
from jiangchang_skill_core import (
StreamChannel,
stream_channel,
stream_delta,
stream_end,
stream_start,
)
self.assertTrue(callable(stream_start))
self.assertTrue(callable(stream_delta))
self.assertTrue(callable(stream_end))
self.assertTrue(callable(stream_channel))
self.assertTrue(issubclass(StreamChannel, object))
if __name__ == "__main__":
unittest.main()