139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""P1:lesson 证据强制、升格 merge、回放与生产 flow 同一入口、web 写面闭集。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import inspect
|
||
import json
|
||
import os
|
||
import pathlib
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
|
||
|
||
ROOT = next(
|
||
parent
|
||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
|
||
)
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from unittest import mock
|
||
|
||
from muse import replay as replay_mod # noqa: E402
|
||
from muse.flow import dispatch as dispatch_mod # noqa: E402
|
||
from muse.flow.adopt import adopt_candidate # noqa: E402
|
||
from muse.store import ( # noqa: E402
|
||
add_review,
|
||
approve_and_merge_lesson,
|
||
connect,
|
||
register_lesson,
|
||
)
|
||
from web import app as webapp # noqa: E402
|
||
|
||
|
||
class CompoundingTest(unittest.TestCase):
|
||
def setUp(self) -> None:
|
||
self.tmp = pathlib.Path(tempfile.mkdtemp())
|
||
self.db = self.tmp / "muse.db"
|
||
with connect(self.db) as conn:
|
||
conn.execute(
|
||
"""INSERT INTO runs(id, created_at, kind, input_json, output_text, meta_json, skill_set_hash)
|
||
VALUES ('run-p1', datetime('now'), 'agent.writer', '{"k":1}', '旧稿', '{}', 'hash')"""
|
||
)
|
||
conn.commit()
|
||
self.review_id = add_review(
|
||
run_id="run-p1",
|
||
target="candidate",
|
||
action="revise",
|
||
reviewer="qingse",
|
||
reason="改钩子",
|
||
before_text="旧稿",
|
||
after_text="新稿",
|
||
path=self.db,
|
||
)
|
||
|
||
def test_empty_evidence_ids_are_rejected_by_python_and_db(self) -> None:
|
||
with self.assertRaises(ValueError):
|
||
register_lesson(
|
||
lesson_id="bad",
|
||
source_run_ids=[],
|
||
source_review_ids=[str(self.review_id)],
|
||
kind="craft",
|
||
title="空证据",
|
||
content="x",
|
||
target_ref="prose-craft",
|
||
lessons_dir=self.tmp / "lessons",
|
||
path=self.db,
|
||
)
|
||
with connect(self.db) as conn:
|
||
with self.assertRaises(sqlite3.IntegrityError):
|
||
conn.execute(
|
||
"""INSERT INTO lessons(id, source_run_ids, source_review_ids, kind, title, content_path, status)
|
||
VALUES ('bad2', '[]', '[]', 'craft', 'x', 'p.md', 'proposed')"""
|
||
)
|
||
|
||
def test_lesson_merges_into_skill_references_with_source_ids(self) -> None:
|
||
register_lesson(
|
||
lesson_id="hook-1",
|
||
source_run_ids=["run-p1"],
|
||
source_review_ids=[str(self.review_id)],
|
||
kind="craft",
|
||
title="钩子再紧",
|
||
content="章末只留一个未解动作。\n",
|
||
target_ref="prose-craft",
|
||
lessons_dir=self.tmp / "pending",
|
||
path=self.db,
|
||
)
|
||
dest = approve_and_merge_lesson(
|
||
"hook-1",
|
||
decided_by="qingse",
|
||
rationale="人审通过",
|
||
references_dir=self.tmp / "prose-craft" / "references",
|
||
path=self.db,
|
||
)
|
||
self.assertTrue(dest.is_file())
|
||
self.assertIn("未解动作", dest.read_text(encoding="utf-8"))
|
||
with connect(self.db) as conn:
|
||
row = conn.execute("SELECT * FROM lessons WHERE id='hook-1'").fetchone()
|
||
self.assertEqual(row["status"], "promoted")
|
||
self.assertEqual(json.loads(row["source_run_ids"]), ["run-p1"])
|
||
self.assertEqual(json.loads(row["source_review_ids"]), [str(self.review_id)])
|
||
|
||
def test_replay_imports_the_same_production_flow(self) -> None:
|
||
self.assertIs(replay_mod.production_run_dispatch, dispatch_mod.run_dispatch)
|
||
source = inspect.getsource(replay_mod.replay_run)
|
||
self.assertIn("production_run_dispatch", source)
|
||
|
||
def test_web_write_surface_is_reviews_revisions_adopt_only(self) -> None:
|
||
self.assertEqual(webapp.WRITE_ACTIONS, frozenset({"reviews", "revisions", "adopt"}))
|
||
source = pathlib.Path(webapp.__file__).read_text(encoding="utf-8")
|
||
self.assertNotIn("INSERT INTO cards", source)
|
||
self.assertNotIn("INSERT INTO runs", source)
|
||
os.environ["MUSE_DB"] = str(self.db)
|
||
self.addCleanup(os.environ.pop, "MUSE_DB", None)
|
||
self.assertIn("adopt_candidate", inspect.getsource(webapp.adopt))
|
||
with mock.patch.object(webapp, "adopt_candidate", wraps=adopt_candidate) as spy:
|
||
review_id = webapp.adopt("run-p1", "qingse", "收下")
|
||
spy.assert_called_once_with("run-p1", "qingse", "收下")
|
||
self.assertIsInstance(review_id, int)
|
||
revise_id = webapp.revise("run-p1", "qingse", "改", "旧", "新")
|
||
with connect(self.db) as conn:
|
||
actions = [
|
||
row[0]
|
||
for row in conn.execute(
|
||
"SELECT action FROM reviews WHERE run_id='run-p1' ORDER BY id"
|
||
)
|
||
]
|
||
revision_n = conn.execute("SELECT COUNT(*) FROM revisions").fetchone()[0]
|
||
self.assertIn("adopt", actions)
|
||
self.assertIn("revise", actions)
|
||
self.assertGreaterEqual(revision_n, 2)
|
||
self.assertNotEqual(review_id, revise_id)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|