178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from server.agent_core import harness
|
|
from server.aps_domain.workflow import (
|
|
execute_confirmed,
|
|
handle_intent,
|
|
stage_schedule_publish,
|
|
)
|
|
from server.contracts import IntentResult
|
|
from server.state.seed import seed_world
|
|
from tests.golden.test_flex_publish_mes_gate import _MemStore, _scheduled_world
|
|
|
|
|
|
def _run(store: _MemStore, intent: IntentResult, session_id: str = "workflow-contract"):
|
|
return asyncio.run(handle_intent(store, session_id, intent, actor="test"))
|
|
|
|
|
|
def test_knowledge_catalog_requires_explicit_mode(tmp_path: Path):
|
|
store = _MemStore(seed_world(), tmp_path / "knowledge-mode-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="knowledge.query",
|
|
params={"query": "知识清单"},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
assert "mode=catalog" in reply.text
|
|
assert "不再从 query 文本推断" in reply.text
|
|
assert not reply.blocks
|
|
|
|
|
|
@pytest.mark.parametrize("enabled", [None, "false", 0, 1])
|
|
def test_skill_enable_requires_boolean_enabled_not_query(tmp_path: Path, enabled):
|
|
store = _MemStore(seed_world(), tmp_path / f"skill-enable-{enabled}-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="skill.enable",
|
|
params={"skillId": "algo.stub", "enabled": enabled, "query": "停用 skill algo.stub"},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
assert "enabled" in reply.text
|
|
assert "不再从 query 文本推断" in reply.text
|
|
assert not any(block.type == "confirm-card" for block in reply.blocks)
|
|
|
|
|
|
def test_skill_enable_accepts_explicit_false(tmp_path: Path):
|
|
store = _MemStore(seed_world(), tmp_path / "skill-enable-false-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="skill.enable",
|
|
params={"skillId": "algo.stub", "enabled": False},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
card = next(block for block in reply.blocks if block.type == "confirm-card")
|
|
assert "停用算法 Skill algo.stub" in reply.text
|
|
assert card.props["confirmId"]
|
|
|
|
|
|
@pytest.mark.parametrize("std_min", [None, "not-a-number", True, 0, float("inf")])
|
|
def test_flex_time_update_requires_structured_positive_minutes(tmp_path: Path, std_min):
|
|
store = _MemStore(seed_world(), tmp_path / "time-update-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="flex.time.update",
|
|
params={"query": "把 OP-1 工时改成 30 分钟", "operationCode": "OP-1", "stdMin": std_min},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
assert "stdMin" in reply.text or "operationCode" in reply.text
|
|
assert not any(block.type == "confirm-card" for block in reply.blocks)
|
|
|
|
|
|
def test_flex_time_update_accepts_explicit_structured_values(tmp_path: Path):
|
|
store = _MemStore(seed_world(), tmp_path / "time-update-valid-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="flex.time.update",
|
|
params={"operationCode": "OP-1", "stdMin": 30},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
assert any(block.type == "confirm-card" for block in reply.blocks)
|
|
|
|
|
|
def test_sop_compile_requires_explicit_selection(tmp_path: Path):
|
|
store = _MemStore(seed_world(), tmp_path / "sop-compile-checkpoints.json")
|
|
|
|
reply = _run(
|
|
store,
|
|
IntentResult(
|
|
intent="sop.compile",
|
|
params={"query": " "},
|
|
confidence=1.0,
|
|
source="LLM",
|
|
),
|
|
)
|
|
|
|
assert "请先选择要查看的作业标准" in reply.text
|
|
assert not any(block.type == "confirm-card" for block in reply.blocks)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"unsafe_version",
|
|
[
|
|
{"trialOnly": True, "productionReady": True},
|
|
{"trialOnly": False, "productionReady": False},
|
|
],
|
|
)
|
|
def test_trial_or_nonproduction_version_cannot_stage_publish(
|
|
tmp_path: Path, unsafe_version: dict,
|
|
):
|
|
world, version_id = _scheduled_world()
|
|
version = next(row for row in world["flexScheduleVersions"] if row["id"] == version_id)
|
|
version.update(unsafe_version)
|
|
store = _MemStore(world, tmp_path / "unsafe-publish-checkpoints.json")
|
|
|
|
reply = stage_schedule_publish(
|
|
store,
|
|
session_id="unsafe-publish",
|
|
actor="planner",
|
|
track="flex",
|
|
version_id=version_id,
|
|
)
|
|
|
|
assert "不可发布" in reply.text
|
|
assert "trialOnly=true" in reply.text or "productionReady=false" in reply.text
|
|
assert not any(block.type == "confirm-card" for block in reply.blocks)
|
|
assert version["status"] == "DRAFT"
|
|
|
|
|
|
def test_execute_confirmed_rechecks_trial_only_before_publish(tmp_path: Path):
|
|
world, version_id = _scheduled_world()
|
|
version = next(row for row in world["flexScheduleVersions"] if row["id"] == version_id)
|
|
version.update({"trialOnly": True, "productionReady": True})
|
|
store = _MemStore(world, tmp_path / "unsafe-publish-execute-checkpoints.json")
|
|
block = harness.stage_confirmation(
|
|
"unsafe-publish-execute",
|
|
"schedule.publish",
|
|
{"track": "flex", "versionId": version_id, "evidenceRefs": [f"schedule-version:{version_id}"]},
|
|
title="unsafe publish",
|
|
summary_lines=["test"],
|
|
)
|
|
confirm_id = str(block.props["confirmId"])
|
|
|
|
text = execute_confirmed(store, confirm_id, True, actor="planner")
|
|
|
|
assert "发布被拒绝" in text
|
|
assert version["status"] == "DRAFT"
|
|
assert world.get("mesLinks") in (None, [])
|
|
assert not any(row.get("action") == "schedule.publish" for row in world.get("auditEvents") or [])
|