| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666 |
- from __future__ import annotations
- import hashlib
- import json
- import tempfile
- import unittest
- from pathlib import Path
- from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
- from langchain_core.tools import tool
- from production_build_agents.capabilities import CAPABILITIES
- from production_build_agents.contracts.brief_paths import (
- build_allowed_source_paths,
- resolve_direct_json_path,
- )
- from production_build_agents.preprocess.brief_io import load_production_brief
- from production_build_agents.contracts.models import (
- CandidateArtifact,
- ExecutorCandidate,
- ArtifactBindingClaim,
- )
- from production_build_agents.agents.executor.agent import (
- ExecutorOutputError,
- run_executor_agent,
- )
- from production_build_agents.agents.invocation import (
- CORRECTION_PRESERVATION_INSTRUCTION,
- )
- from production_build_agents.agents.executor.context import (
- build_executor_system_prompt,
- build_executor_user_payload,
- create_executor_run_id,
- load_task_contract,
- )
- from production_build_agents.agents.executor.provenance import (
- ExecutorProvenanceError,
- validate_candidate_provenance,
- )
- from production_build_agents.agents.tool_trace import build_tool_call_records
- from production_build_agents.agents.executor.skills.registry import (
- ExecutorSkillError,
- load_executor_skill,
- )
- from production_build_agents.run.records import save_executor_delivery
- from production_build_agents.run.langgraph_checkpointer import (
- checkpoint_messages,
- create_sqlite_checkpointer,
- )
- from production_build_agents.tools.registry import ToolRegistry, create_default_tool_registry
- from tests.support.executor_fixtures import (
- build_executor_candidate,
- build_executor_model,
- )
- from tests.support.fake_models import ToolAwareFakeChatModel
- from tests.support.planner_fixtures import (
- build_planned_task,
- build_task_package,
- )
- BRIEF_PATH = (
- Path(__file__).parents[1] / "fixtures" / "minimal_brief.json"
- )
- IMAGE_URL = "https://example.test/generated-reference.png"
- SOURCE_URL = "https://example.test/research-source"
- VIDEO_URL = "https://example.test/generated-video.mp4"
- FRAME_URL = "https://example.test/generated-video-frame.jpg"
- _FAKE_MEDIA_TEMP = tempfile.TemporaryDirectory()
- _FAKE_MEDIA_DIR = Path(_FAKE_MEDIA_TEMP.name)
- def _fake_media_path(source: str, suffix: str) -> Path:
- path = _FAKE_MEDIA_DIR / (
- hashlib.sha256(source.encode("utf-8")).hexdigest() + suffix
- )
- path.write_bytes(source.encode("utf-8"))
- return path
- def _fake_tool_registry() -> ToolRegistry:
- @tool("search_tool")
- def search_tool(query: str, limit: int = 5) -> dict:
- """发现测试工具。"""
- return {
- "success": True,
- "results": [{"tool_id": "fake-tool", "summary": query}],
- "_duration_ms": 3,
- }
- @tool("inspect_tool")
- def inspect_tool(tool_ids: list[str]) -> dict:
- """返回测试工具参数。"""
- return {
- "success": True,
- "tools": {tool_id: {"input_schema": {}} for tool_id in tool_ids},
- "_duration_ms": 4,
- }
- @tool("run_tool")
- def run_tool(tool_id: str, params: dict) -> dict:
- """执行测试工具。"""
- url = params.get("result_url") or {
- "image": IMAGE_URL,
- "video": VIDEO_URL,
- }.get(params.get("kind"), SOURCE_URL)
- return {
- "success": True,
- "tool_id": tool_id,
- "url": url,
- "_duration_ms": 5,
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试图片。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": str(
- _fake_media_path(source, ".png")
- ),
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- "_duration_ms": 6,
- }
- @tool("detect_faces")
- def detect_faces(source: str) -> dict:
- """返回测试图片的人脸三态判断。"""
- return {
- "success": True,
- "decision": "NO_FACE",
- "source": source,
- "local_path": source,
- "input_sha256": "a" * 64,
- }
- @tool("crop_image")
- def crop_image(image: str, box: list[float]) -> dict:
- """裁剪测试图片。"""
- return {"success": True, "image_urls": [image]}
- @tool("grid_collage")
- def grid_collage(images: list[str]) -> dict:
- """拼接测试图片。"""
- return {"success": True, "image_urls": images[:1]}
- @tool("overlay_text")
- def overlay_text(image: str, texts: list[dict]) -> dict:
- """叠加测试文字。"""
- return {"success": True, "image_urls": [image]}
- @tool("extract_frames")
- def extract_frames(video: str, num_frames: int = 5) -> dict:
- """抽取测试视频帧。"""
- return {
- "success": True,
- "source": video,
- "local_path": str(_fake_media_path(video, ".mp4")),
- "frames": [
- {
- "source": FRAME_URL,
- "timestamp_sec": 0,
- }
- ],
- "_duration_ms": 7,
- }
- @tool("video_trim")
- def video_trim(video: str, start_sec: float = 0) -> dict:
- """裁剪测试视频。"""
- return {"success": True, "video_urls": [video]}
- @tool("video_concat")
- def video_concat(videos: list[str]) -> dict:
- """拼接测试视频。"""
- return {"success": True, "video_urls": videos[:1]}
- @tool("video_mux_audio")
- def video_mux_audio(video: str, audio: str) -> dict:
- """合入测试音频。"""
- return {"success": True, "video_urls": [video]}
- tools = [
- search_tool,
- inspect_tool,
- run_tool,
- detect_faces,
- view_images,
- crop_image,
- grid_collage,
- overlay_text,
- extract_frames,
- video_trim,
- video_concat,
- video_mux_audio,
- ]
- return ToolRegistry({item.name: item for item in tools})
- def _tool_call(name: str, args: dict, call_id: str) -> AIMessage:
- return AIMessage(
- content="",
- tool_calls=[
- {
- "name": name,
- "args": args,
- "id": call_id,
- "type": "tool_call",
- }
- ],
- )
- class ExecutorSkillTest(unittest.TestCase):
- def test_generic_skills_have_distinct_tool_boundaries(self) -> None:
- analysis = load_executor_skill("structured-analysis")
- research = load_executor_skill("external-research")
- image = load_executor_skill("image-production")
- video = load_executor_skill("video-production")
- reference = load_executor_skill("reference-inspection")
- self.assertEqual(analysis.allowed_tools, ())
- self.assertIn("ProductionBrief", analysis.instructions)
- self.assertIn("派生阅读视图", analysis.instructions)
- self.assertEqual(
- research.allowed_tools,
- ("search_tool", "inspect_tool", "run_tool"),
- )
- self.assertIn("view_images", image.allowed_tools)
- self.assertIn("crop_image", image.allowed_tools)
- self.assertEqual(
- image.allowed_remote_tool_ids,
- (
- "seedream_generate_image",
- "gpt-image",
- "nano_banana_image",
- ),
- )
- self.assertNotIn("detect_faces", image.allowed_tools)
- self.assertNotIn("detect_faces", reference.allowed_tools)
- self.assertIn("detect_faces", video.allowed_tools)
- self.assertIn("extract_frames", video.allowed_tools)
- self.assertEqual(
- video.allowed_remote_tool_ids,
- ("seedream_generate_image", "seedance_generate_video"),
- )
- self.assertTrue(
- research.verification_policy.require_inspect_before_run
- )
- self.assertEqual(
- image.verification_policy.final_artifact_check_tool,
- "view_images",
- )
- self.assertEqual(
- video.verification_policy.final_artifact_check_tool,
- "extract_frames",
- )
- self.assertEqual(
- reference.allowed_tools,
- ("probe_media", "view_images", "extract_frames"),
- )
- self.assertEqual(
- set(CAPABILITIES[reference.skill_id].artifact_types),
- {"image", "video", "audio"},
- )
- def test_unknown_skill_is_rejected(self) -> None:
- with self.assertRaisesRegex(ExecutorSkillError, "未注册"):
- load_executor_skill("unknown")
- class ExecutorContextTest(unittest.TestCase):
- def test_model_receives_only_requested_brief_excerpt(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- task = build_task_package(
- BRIEF_PATH,
- planned_task=build_planned_task(
- source_paths=["$.核心制作点[0]"]
- ),
- plan_path=Path(temp_dir) / "plan.json",
- )
- brief, plan, planned_task = load_task_contract(task)
- expectation = plan.stage_requirements[
- 0
- ].artifact_expectations[0]
- sibling = expectation.model_copy(
- update={
- "expectation_id": "Requirement1-Expectation2",
- "usage_scope": "不属于当前 Task 的兄弟期待",
- }
- )
- requirement = plan.stage_requirements[0].model_copy(
- update={
- "artifact_expectations": [expectation, sibling]
- }
- )
- plan = plan.model_copy(
- update={"stage_requirements": [requirement]}
- )
- Path(task.plan_uri).write_text(
- plan.model_dump_json(indent=2),
- encoding="utf-8",
- )
- brief, plan, planned_task = load_task_contract(task)
- run_id = create_executor_run_id(task)
- payload = build_executor_user_payload(
- executor_run_id=run_id,
- task=task,
- brief=brief,
- plan=plan,
- planned_task=planned_task,
- )
- self.assertEqual(
- set(payload["production_brief_excerpt"]),
- {"$.核心制作点[0]"},
- )
- self.assertNotIn("production_brief", payload)
- self.assertEqual(
- payload["allowed_source_paths"],
- planned_task.source_paths,
- )
- self.assertEqual(
- payload["resolved_task"],
- planned_task.model_dump(mode="json"),
- )
- self.assertNotIn("plan", payload)
- self.assertNotIn("stage_requirements", payload)
- self.assertNotIn("requirements", payload)
- self.assertEqual(
- [
- item["expectation"]["expectation_id"]
- for item in payload["selected_expectations"]
- ],
- ["Requirement1-Expectation1"],
- )
- self.assertNotIn(
- "Requirement1-Expectation2",
- json.dumps(payload, ensure_ascii=False),
- )
- self.assertEqual(
- set(payload["task_package"]),
- {
- "schema_version",
- "run_id",
- "plan_id",
- "plan_version",
- "task_id",
- "production_brief_uri",
- "plan_uri",
- "dependency_deliveries",
- },
- )
- prompt = build_executor_system_prompt(
- load_executor_skill(planned_task.skill_id)
- )
- self.assertIn("structured-analysis", prompt)
- self.assertNotIn("{{SKILL_ID}}", prompt)
- def test_shared_json_path_reader_keeps_stable_boundaries(self) -> None:
- _, brief = load_production_brief(BRIEF_PATH)
- payload = brief.model_dump(mode="json", by_alias=True)
- self.assertIn("$.制作表.形式结果[0]", build_allowed_source_paths(brief))
- self.assertEqual(
- resolve_direct_json_path(payload, "$.帖子类型"),
- payload["帖子类型"],
- )
- def test_dependency_manifest_loads_only_summary_and_artifact_refs(self) -> None:
- first_task = build_task_package(BRIEF_PATH)
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- first_task,
- run_dir=root,
- model=build_executor_model(build_executor_candidate()),
- tool_registry=_fake_tool_registry(),
- )
- manifest = save_executor_delivery(root, delivery)
- second_task = build_task_package(
- BRIEF_PATH,
- planned_task=build_planned_task(
- task_id="Task2",
- depends_on=["Task1"],
- ),
- dependency_artifacts={"Task1": str(manifest)},
- )
- brief, plan, planned_task = load_task_contract(second_task)
- payload = build_executor_user_payload(
- executor_run_id=create_executor_run_id(second_task),
- task=second_task,
- brief=brief,
- plan=plan,
- planned_task=planned_task,
- )
- dependency = payload["dependency_artifacts"][0]
- self.assertEqual(dependency["task_id"], "Task1")
- self.assertIsInstance(dependency["summary"], str)
- self.assertEqual(dependency["summary"], delivery.summary)
- self.assertEqual(
- dependency["artifacts"][0]["uri"],
- delivery.artifacts[0].uri,
- )
- self.assertNotIn("tool_calls", dependency)
- class GeneralExecutorTest(unittest.TestCase):
- def test_structured_candidate_must_reference_every_task_source_path(
- self,
- ) -> None:
- planned = build_planned_task(
- source_paths=[
- "$.核心制作点[0]",
- "$.制作表.形式结果[0]",
- ]
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- source_paths=["$.核心制作点[0]"]
- )
- with self.assertRaisesRegex(
- ExecutorProvenanceError,
- "未引用当前 Task 的全部来源路径",
- ):
- validate_candidate_provenance(
- candidate,
- task,
- planned,
- [],
- skill=load_executor_skill("structured-analysis"),
- input_refs=set(),
- )
- def test_structured_candidate_counts_nested_payload_source_paths(
- self,
- ) -> None:
- planned = build_planned_task(
- source_paths=[
- "$.核心制作点[0]",
- "$.制作表.形式结果[0]",
- ]
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- source_paths=["$.核心制作点[0]"]
- ).model_copy(
- update={
- "payload": {
- "sections": [
- {
- "content": "保留形式结果中的精确规则",
- "source_paths": ["$.制作表.形式结果[0]"],
- }
- ]
- }
- }
- )
- validate_candidate_provenance(
- candidate,
- task,
- planned,
- [],
- skill=load_executor_skill("structured-analysis"),
- input_refs=set(),
- )
- def test_same_rule_in_different_scopes_keeps_both_source_paths(
- self,
- ) -> None:
- planned = build_planned_task(
- source_paths=[
- "$.制作表.段落结构[0]",
- "$.制作表.段落结构[1]",
- ]
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate().model_copy(
- update={
- "findings": [],
- "payload": {
- "sections": [
- {
- "content": "主体居中",
- "scope": "00:00:00-00:00:05",
- "source_paths": ["$.制作表.段落结构[0]"],
- },
- {
- "content": "主体居中",
- "scope": "00:00:05-00:00:10",
- "source_paths": ["$.制作表.段落结构[1]"],
- },
- ]
- },
- }
- )
- validate_candidate_provenance(
- candidate,
- task,
- planned,
- [],
- skill=load_executor_skill("structured-analysis"),
- input_refs=set(),
- )
- def test_reference_source_must_match_probe_local_mapping(self) -> None:
- source_a = "https://example.test/a.png"
- source_b = "https://example.test/b.png"
- local_a = str(BRIEF_PATH.resolve())
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- _, _, resolved_task = load_task_contract(task)
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=local_a,
- source_uri=source_b,
- description="错误绑定的参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=local_a,
- )
- ],
- summary="错误地把 A 的缓存声明为 B",
- )
- messages = [
- AIMessage(
- content="",
- tool_calls=[
- {
- "name": "probe_media",
- "args": {"source": source_a},
- "id": "probe-a",
- "type": "tool_call",
- },
- {
- "name": "view_images",
- "args": {"image_sources": [local_a]},
- "id": "view-a",
- "type": "tool_call",
- },
- ],
- ),
- ToolMessage(
- content=json.dumps(
- {
- "success": True,
- "source": source_a,
- "local_path": local_a,
- "media_type": "image",
- }
- ),
- tool_call_id="probe-a",
- name="probe_media",
- ),
- ToolMessage(
- content=json.dumps(
- {
- "success": True,
- "images": [
- {
- "source": local_a,
- "local_path": local_a,
- }
- ],
- }
- ),
- tool_call_id="view-a",
- name="view_images",
- ),
- ]
- with self.assertRaisesRegex(
- ExecutorProvenanceError,
- "probe_media",
- ):
- validate_candidate_provenance(
- candidate,
- task,
- resolved_task,
- build_tool_call_records(messages),
- skill=load_executor_skill("reference-inspection"),
- input_refs={source_b},
- )
- def test_reference_inspection_adopts_real_viewed_image(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- image_path = root / "existing-reference.png"
- from PIL import Image
- Image.new("RGB", (24, 24), "green").save(image_path)
- brief_payload = json.loads(BRIEF_PATH.read_text(encoding="utf-8"))
- brief_payload["核心制作点"][0]["测试已有媒体"] = str(image_path)
- brief_path = root / "production_brief.json"
- brief_path.write_text(
- json.dumps(brief_payload, ensure_ascii=False),
- encoding="utf-8",
- )
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=2,
- )
- task = build_task_package(
- brief_path,
- planned_task=planned,
- )
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=str(image_path.resolve()),
- source_uri=str(image_path.resolve()),
- description="已检查并缓存的人物参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=str(image_path.resolve()),
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="已有参考图可用于后续生产",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": str(image_path.resolve())},
- "probe-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [str(image_path.resolve())]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- delivery = run_executor_agent(
- task,
- run_dir=root / "run",
- model=model,
- tool_registry=create_default_tool_registry(
- output_dir=root / "tool-output",
- ),
- )
- self.assertEqual(delivery.skill_id, "reference-inspection")
- self.assertEqual(delivery.artifacts[0].artifact_type, "image")
- self.assertEqual(
- delivery.artifacts[0].source_uri,
- str(image_path.resolve()),
- )
- def test_reference_review_accepts_separate_probe_and_view_caches(
- self,
- ) -> None:
- source_url = "https://example.test/existing-reference.png"
- # 使用一个真实存在、且与 source_url 不同的路径模拟 probe 缓存;
- # 文件内容不由该测试读取。
- probe_path = str(BRIEF_PATH.resolve())
- @tool("probe_media")
- def probe_media(source: str) -> dict:
- """探测测试媒体。"""
- return {
- "success": True,
- "source": source,
- "local_path": probe_path,
- "media_type": "image",
- "mime_type": "image/png",
- "width": 1080,
- "height": 1920,
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试图片,并模拟另一套真实缓存路径。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": (
- "/cache/view/existing-reference.png"
- ),
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- }
- @tool("extract_frames")
- def extract_frames(video: str) -> dict:
- """占位视频抽帧工具。"""
- return {"success": True, "video": video, "frames": []}
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="image",
- uri=probe_path,
- source_uri=source_url,
- description="已检查的人物参考图",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=probe_path,
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="参考图已经完成探测和查看",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": source_url},
- "probe-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [source_url]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=ToolRegistry(
- {
- item.name: item
- for item in (
- probe_media,
- view_images,
- extract_frames,
- )
- }
- ),
- )
- self.assertEqual(delivery.artifacts[0].uri, probe_path)
- self.assertEqual(delivery.artifacts[0].source_uri, source_url)
- def test_reference_video_review_links_source_to_probe_cache(
- self,
- ) -> None:
- source_url = "https://example.test/existing-reference.mp4"
- probe_path = str(BRIEF_PATH.resolve())
- frame_path = str(BRIEF_PATH.resolve())
- frame_url = "https://example.test/extracted-frame.png"
- @tool("probe_media")
- def probe_media(source: str) -> dict:
- """探测测试视频。"""
- return {
- "success": True,
- "source": source,
- "local_path": probe_path,
- "media_type": "video",
- "mime_type": "video/mp4",
- "duration_sec": 5,
- "width": 1080,
- "height": 1920,
- }
- @tool("extract_frames")
- def extract_frames(video: str) -> dict:
- """从原始来源抽取测试视频帧。"""
- return {
- "success": True,
- "video": video,
- "frames": [
- {
- "local_path": frame_path,
- "url": frame_url,
- }
- ],
- }
- @tool("view_images")
- def view_images(image_sources: list[str]) -> dict:
- """查看测试视频帧。"""
- return {
- "success": True,
- "images": [
- {
- "source": source,
- "local_path": source,
- "data_url": (
- "data:image/png;base64,"
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
- ),
- }
- for source in image_sources
- ],
- }
- planned = build_planned_task(
- skill_id="reference-inspection",
- deliverable_type="reference_collection",
- max_attempts=1,
- )
- task = build_task_package(
- BRIEF_PATH,
- planned_task=planned,
- expectation_artifact_type="video",
- )
- candidate = ExecutorCandidate(
- run_id=task.run_id,
- plan_id=task.plan_id,
- task_id=task.task_id,
- plan_version=task.plan_version,
- deliverable_type="reference_collection",
- artifacts=[
- CandidateArtifact(
- artifact_type="video",
- uri=probe_path,
- source_uri=source_url,
- description="已检查的动作参考视频",
- )
- ],
- artifact_binding_claims=[
- ArtifactBindingClaim(
- expectation_id="Requirement1-Expectation1",
- artifact_uri=probe_path,
- evidence_tool_call_ids=["probe-reference"],
- )
- ],
- summary="参考视频已经完成探测、抽帧和查看",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "probe_media",
- {"source": source_url},
- "probe-reference",
- ),
- _tool_call(
- "extract_frames",
- {"video": source_url},
- "extract-reference",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [frame_url]},
- "view-reference",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=ToolRegistry(
- {
- item.name: item
- for item in (
- probe_media,
- view_images,
- extract_frames,
- )
- }
- ),
- )
- self.assertEqual(delivery.artifacts[0].uri, probe_path)
- self.assertEqual(delivery.artifacts[0].source_uri, source_url)
- def test_analysis_task_writes_local_structured_artifact(self) -> None:
- task = build_task_package(BRIEF_PATH)
- candidate = build_executor_candidate().model_copy(
- update={
- "run_id": "model-run",
- "plan_id": "ModelPlan",
- "task_id": "Task99",
- "plan_version": 99,
- "artifacts": [
- CandidateArtifact(
- artifact_type="structured_data",
- uri="delivery_artifact.json",
- description="由 Host 落盘的结构化结果",
- )
- ]
- }
- )
- self.assertEqual(candidate.schema_version, "0.3")
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=build_executor_model(candidate),
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(delivery.skill_id, "structured-analysis")
- self.assertEqual(delivery.attempt_count, 1)
- self.assertEqual(
- delivery.artifacts[0].artifact_id,
- "Task1-v1-artifact-1",
- )
- self.assertIsInstance(delivery.summary, str)
- self.assertTrue(Path(delivery.manifest_uri).is_file())
- artifact_path = Path(delivery.artifacts[0].uri)
- self.assertTrue(artifact_path.is_file())
- self.assertNotEqual(
- artifact_path.resolve(),
- Path(delivery.manifest_uri).resolve(),
- )
- self.assertEqual(delivery.tool_calls, [])
- saved = ExecutorCandidate.model_validate_json(
- Path(delivery.manifest_uri).read_text(encoding="utf-8")
- )
- self.assertEqual(saved.artifacts, [])
- self.assertEqual(saved.run_id, task.run_id)
- self.assertEqual(saved.plan_id, task.plan_id)
- self.assertEqual(saved.task_id, task.task_id)
- self.assertEqual(saved.plan_version, task.plan_version)
- self.assertEqual(saved.payload["global_constraints"], ["统一主视觉"])
- materialized = json.loads(artifact_path.read_text(encoding="utf-8"))
- self.assertEqual(
- materialized["payload"]["global_constraints"],
- ["统一主视觉"],
- )
- def test_analysis_task_rejects_non_host_artifact_uri(self) -> None:
- planned = build_planned_task(max_attempts=1)
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate().model_copy(
- update={
- "artifacts": [
- CandidateArtifact(
- artifact_type="structured_data",
- uri="another-result.json",
- description="未经 Host 声明的结构化结果",
- )
- ]
- }
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具或依赖产物",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=build_executor_model(candidate),
- tool_registry=_fake_tool_registry(),
- )
- def test_same_task_execution_reuses_stable_agent_run(self) -> None:
- task = build_task_package(BRIEF_PATH)
- candidate = build_executor_candidate()
- model = build_executor_model(candidate, candidate)
- with tempfile.TemporaryDirectory() as temp_dir:
- first = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- second = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(first.executor_run_id, second.executor_run_id)
- def test_invalid_output_retries_inside_same_task(self) -> None:
- task = build_task_package(BRIEF_PATH)
- valid = build_executor_candidate()
- invalid = valid.model_dump(mode="json")
- invalid["payload"] = {}
- model = ToolAwareFakeChatModel(
- responses=[
- AIMessage(content=json.dumps(invalid, ensure_ascii=False)),
- AIMessage(content=valid.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- run_dir = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=run_dir,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- with create_sqlite_checkpointer(
- run_dir,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(
- checkpointer,
- {
- "configurable": {
- "thread_id": delivery.executor_run_id
- }
- },
- )
- self.assertEqual(delivery.attempt_count, 2)
- self.assertIn(task.task_id, delivery.executor_run_id)
- self.assertTrue(
- any(
- isinstance(message, HumanMessage)
- and isinstance(message.content, str)
- and CORRECTION_PRESERVATION_INSTRUCTION
- in message.content
- for message in messages
- )
- )
- def test_host_derives_exact_evidence_call_ids_without_correction(
- self,
- ) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- evidence_tool_call_ids=["model-invented-id"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "资料"}, "call-search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["fake-tool"]},
- "call-inspect",
- ),
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "call-run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- run_dir = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=run_dir,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- with create_sqlite_checkpointer(
- run_dir,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(
- checkpointer,
- {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- },
- )
- saved_candidate = ExecutorCandidate.model_validate_json(
- Path(delivery.manifest_uri).read_bytes()
- )
- corrections = [
- message.content
- for message in messages
- if isinstance(message, HumanMessage)
- and isinstance(message.content, str)
- and message.content.startswith("上一版未通过")
- ]
- self.assertEqual(delivery.attempt_count, 1)
- self.assertEqual(corrections, [])
- self.assertEqual(
- delivery.artifact_expectation_bindings[
- 0
- ].evidence_tool_call_ids,
- ["call-run"],
- )
- self.assertEqual(
- saved_candidate.artifact_binding_claims[
- 0
- ].evidence_tool_call_ids,
- ["call-run"],
- )
- def test_research_task_must_use_real_tool_output(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- evidence_tool_call_ids=["call-run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "资料"}, "call-search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["fake-tool"]},
- "call-inspect",
- ),
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "call-run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(
- [item.tool_name for item in delivery.tool_calls],
- ["search_tool", "inspect_tool", "run_tool"],
- )
- self.assertTrue(
- [item.duration_ms for item in delivery.tool_calls] == [3, 4, 5]
- )
- self.assertEqual(
- delivery.findings[0].source_urls,
- [SOURCE_URL],
- )
- self.assertIn(
- SOURCE_URL,
- delivery.tool_calls[-1].output_excerpt or "",
- )
- def test_image_task_calls_generation_and_views_result(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call("search_tool", {"query": "图片生成"}, "search"),
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=root,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- config = {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- }
- with create_sqlite_checkpointer(
- root,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(checkpointer, config)
- self.assertEqual(
- Path(delivery.artifacts[0].uri),
- _fake_media_path(IMAGE_URL, ".png").resolve(),
- )
- self.assertEqual(delivery.artifacts[0].source_uri, IMAGE_URL)
- self.assertFalse(
- delivery.artifacts[0].uri.startswith(("http://", "https://"))
- )
- self.assertEqual(delivery.tool_calls[-1].tool_name, "view_images")
- self.assertTrue(
- any(
- isinstance(message.content, list)
- and any(
- isinstance(block, dict)
- and block.get("type") == "image_url"
- for block in message.content
- )
- for message in messages
- )
- )
- def test_research_run_requires_prior_matching_inspect(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri=SOURCE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "run_tool 前必须成功 inspect",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_image_final_artifact_must_be_viewed(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [SOURCE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "最终 Artifact 必须经过 view_images",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_view_images_does_not_prove_image_origin(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具或依赖产物",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_video_task_calls_generation_and_extracts_final_frames(self) -> None:
- planned = build_planned_task(
- skill_id="video-production",
- deliverable_type="video",
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="video",
- artifact_uri=VIDEO_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedance_generate_video"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedance_generate_video",
- "params": {"kind": "video"},
- },
- "run",
- ),
- _tool_call(
- "extract_frames",
- {"video": VIDEO_URL, "num_frames": 3},
- "extract",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [FRAME_URL]},
- "view-frames",
- ),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- delivery = run_executor_agent(
- task,
- run_dir=root,
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- config = {
- "configurable": {
- "thread_id": create_executor_run_id(task)
- }
- }
- with create_sqlite_checkpointer(
- root,
- filename="agent_checkpoints.sqlite",
- ) as checkpointer:
- messages = checkpoint_messages(checkpointer, config)
- self.assertEqual(
- Path(delivery.artifacts[0].uri),
- _fake_media_path(VIDEO_URL, ".mp4").resolve(),
- )
- self.assertEqual(delivery.artifacts[0].source_uri, VIDEO_URL)
- self.assertEqual(delivery.tool_calls[-1].tool_name, "view_images")
- self.assertTrue(
- any(
- isinstance(message.content, list)
- and any(
- isinstance(block, dict)
- and block.get("type") == "image_url"
- for block in message.content
- )
- for message in messages
- )
- )
- def test_media_review_and_format_repairs_share_attempt_budget(self) -> None:
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {"kind": "image"},
- },
- "run",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view",
- ),
- AIMessage(content="{}"),
- AIMessage(content=candidate.model_dump_json()),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- delivery = run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- self.assertEqual(delivery.attempt_count, 2)
- def test_media_artifact_changes_cannot_bypass_attempt_budget(self) -> None:
- image_b = "https://example.test/generated-reference-b.png"
- image_c = "https://example.test/generated-reference-c.png"
- planned = build_planned_task(
- skill_id="image-production",
- deliverable_type="image",
- max_attempts=2,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate_a = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=IMAGE_URL,
- evidence_tool_call_ids=["run-a"],
- )
- candidate_b = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=image_b,
- evidence_tool_call_ids=["run-b"],
- )
- candidate_c = build_executor_candidate(
- deliverable_type="image",
- artifact_uri=image_c,
- evidence_tool_call_ids=["run-c"],
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "inspect_tool",
- {"tool_ids": ["seedream_generate_image"]},
- "inspect",
- ),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": IMAGE_URL,
- },
- },
- "run-a",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [IMAGE_URL]},
- "view-a",
- ),
- AIMessage(content=candidate_a.model_dump_json()),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": image_b,
- },
- },
- "run-b",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [image_b]},
- "view-b",
- ),
- AIMessage(content=candidate_b.model_dump_json()),
- _tool_call(
- "run_tool",
- {
- "tool_id": "seedream_generate_image",
- "params": {
- "kind": "image",
- "result_url": image_c,
- },
- },
- "run-c",
- ),
- _tool_call(
- "view_images",
- {"image_sources": [image_c]},
- "view-c",
- ),
- AIMessage(content=candidate_c.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "耗尽修正次数",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_fabricated_remote_uri_is_rejected(self) -> None:
- planned = build_planned_task(
- skill_id="external-research",
- deliverable_type="research_result",
- max_attempts=1,
- )
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate(
- deliverable_type="research_result",
- artifact_uri="https://fabricated.test/source",
- )
- model = ToolAwareFakeChatModel(
- responses=[
- _tool_call(
- "run_tool",
- {"tool_id": "fake-tool", "params": {"kind": "research"}},
- "run",
- ),
- AIMessage(content=candidate.model_dump_json()),
- ]
- )
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=model,
- tool_registry=_fake_tool_registry(),
- )
- def test_analysis_payload_cannot_hide_a_fabricated_url(self) -> None:
- planned = build_planned_task(max_attempts=1)
- task = build_task_package(BRIEF_PATH, planned_task=planned)
- candidate = build_executor_candidate()
- candidate.payload["fake_reference"] = "https://fabricated.test/image.png"
- with tempfile.TemporaryDirectory() as temp_dir:
- with self.assertRaisesRegex(
- ExecutorOutputError,
- "未出现在工具",
- ):
- run_executor_agent(
- task,
- run_dir=Path(temp_dir),
- model=build_executor_model(candidate),
- tool_registry=_fake_tool_registry(),
- )
- if __name__ == "__main__":
- unittest.main()
|