mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 14:51:39 +08:00
fix(builder): preserve confirmed requirements in knowledge clarification
This commit is contained in:
1 parent
c620d136b0
commit
2f2c3f6d6c
8 files changed
+363
-2
No files matched your search
@@ -194,6 +194,7 @@ FILES += [
|
||||
"api/tests/unit_tests/services/dify_builder/test_knowledge_inventory.py",
|
||||
"api/tests/unit_tests/services/dify_builder/test_knowledge_discovery.py",
|
||||
"api/tests/unit_tests/services/dify_builder/test_knowledge_source_cue.py",
|
||||
"api/tests/unit_tests/services/dify_builder/test_requirement_provenance.py",
|
||||
]
|
||||
SELECTED_GUARDS = [
|
||||
"api/tests/unit_tests/core/dify_builder/test_handlers_edit.py::test_sufficient_goal_reaches_plan_gate_or_actionable_retry_with_monotonic_progress",
|
||||
@@ -2703,6 +2704,23 @@ a4_expected.update(
|
||||
"test_nonbare_knowledge_item_keeps_handler_clarification_with_false_model_hint": 6,
|
||||
"test_shared_source_predicate_keeps_handler_clarification_with_false_model_hint": 5
|
||||
},
|
||||
"tests.unit_tests.services.dify_builder.test_requirement_provenance": {
|
||||
"test_actual_fresh_analysis_ignores_only_unconfirmed_derived_source_summary": 1,
|
||||
"test_explicitly_submitted_source_values_and_defaults_remain_guarded": 4,
|
||||
"test_only_present_offered_payload_keys_are_confirmed_and_current_values_are_used": 1,
|
||||
"test_raw_goal_without_complete_explicit_exclusion_keeps_full_requirements_guard": 4,
|
||||
"test_unknown_malformed_or_missing_key_provenance_never_drops_the_source_guard": 6,
|
||||
"test_missing_confirmed_key_fails_closed_even_without_another_source_value": 1,
|
||||
"test_fresh_provenance_never_waives_other_server_source_guards": 4,
|
||||
"test_confirmation_provenance_roundtrips_without_aliasing": 3,
|
||||
"test_malformed_serialized_provenance_decodes_as_legacy_unknown": 6,
|
||||
"test_missing_serialized_provenance_stays_unknown_after_a_later_submission": 1,
|
||||
"test_reanalysis_replaces_stale_confirmation_provenance": 1,
|
||||
"test_new_task_reset_cannot_reuse_old_confirmation_provenance": 4,
|
||||
"test_adjustment_history_retains_confirmed_source_after_request_is_cleared": 1,
|
||||
"test_clear_discards_confirmation_provenance_at_the_durable_owner": 1,
|
||||
"test_fix_activation_clears_provenance_with_replaced_requirements": 1,
|
||||
},
|
||||
"tests.unit_tests.services.dify_builder.test_knowledge_source_cue": {
|
||||
"test_explicitly_excluded_source_does_not_create_a_knowledge_goal": 11,
|
||||
"test_affirmative_mixed_or_uncertain_source_still_requires_clarification": 29,
|
||||
|
||||
@@ -397,6 +397,9 @@ class DifyBuilderContext:
|
||||
attachment_material: list[dict[str, Any]] = field(default_factory=list)
|
||||
reply_language: str = "" # detected user-input language (BCP-47); drives reply/localization
|
||||
requirements: dict[str, Any] = field(default_factory=dict)
|
||||
# Server-owned submitted form keys. None means legacy/unknown provenance;
|
||||
# [] means fresh analysis with no user-confirmed requirement values.
|
||||
confirmed_requirement_keys: list[str] | None = None
|
||||
plan_items: list[str] = field(default_factory=list)
|
||||
plan_title: str = ""
|
||||
plan_input_summary: str = ""
|
||||
|
||||
@@ -100,9 +100,10 @@ from services.dify_builder.initial_draft import build_intents_for_draft
|
||||
from services.dify_builder.plan import (
|
||||
ResourceDiscovery,
|
||||
WorkflowPlan,
|
||||
has_knowledge_source_cue,
|
||||
has_confirmed_knowledge_source_cue,
|
||||
make_plan_card,
|
||||
store_plan,
|
||||
validated_confirmed_requirement_keys,
|
||||
)
|
||||
from services.dify_builder.repair_gate import NO_REPAIR_REASON
|
||||
|
||||
@@ -174,6 +175,7 @@ def handle_capability_check(env: Env, turn: Turn, s: Session, fc: DifyBuilderCon
|
||||
analysis = env.agent.analyze_goal(fc.goal_text, reply_language=fc.reply_language or None)
|
||||
fc.form_fields = list(analysis.get("fields") or [])
|
||||
fc.requirements = dict(analysis.get("values") or {})
|
||||
fc.confirmed_requirement_keys = []
|
||||
_refine_app_name(env, fc)
|
||||
|
||||
progress.activate("build-prepare-requirements")
|
||||
@@ -259,6 +261,20 @@ def handle_goal_analysis(env: Env, turn: Turn, s: Session, fc: DifyBuilderContex
|
||||
keys = [f["key"] for f in fc.form_fields if isinstance(f, dict) and f.get("key")]
|
||||
previous_requirements = fc.requirements
|
||||
fc.requirements = merge_known_keys(fc.requirements, turn.action.payload, keys)
|
||||
confirmed_keys = validated_confirmed_requirement_keys(fc.confirmed_requirement_keys)
|
||||
if confirmed_keys is not None:
|
||||
fc.confirmed_requirement_keys = list(
|
||||
dict.fromkeys(
|
||||
[
|
||||
*confirmed_keys,
|
||||
*(
|
||||
key
|
||||
for key in keys
|
||||
if isinstance(key, str) and key in turn.action.payload and key in fc.requirements
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
if fc.requirements != previous_requirements:
|
||||
clear_task_acceptance(fc)
|
||||
|
||||
@@ -622,7 +638,7 @@ def handle_resource_recommendation(env: Env, turn: Turn, s: Session, fc: DifyBui
|
||||
or fc.resource_selection.get("blocked_capability") == "knowledge_retrieval_phase_two"
|
||||
or fc.resource_selection.get("knowledge_source_unavailable")
|
||||
or fc.resource_selection.get("knowledge_requires_clarification")
|
||||
or has_knowledge_source_cue(fc.goal_text, fc.requirements)
|
||||
or has_confirmed_knowledge_source_cue(fc.goal_text, fc.requirements, fc.confirmed_requirement_keys)
|
||||
)
|
||||
):
|
||||
progress.fail_step("build-bind-resources")
|
||||
@@ -1593,6 +1609,13 @@ def _collect_adjustment(fc: DifyBuilderContext) -> StepResult:
|
||||
previous_request = fc.requirements.get(_ADJUSTMENT_REQUEST)
|
||||
if isinstance(previous_request, str) and previous_request.strip():
|
||||
fc.requirements[_ADJUSTMENT_HISTORY] = [*fc.requirements.get(_ADJUSTMENT_HISTORY, []), previous_request]
|
||||
confirmed_keys = validated_confirmed_requirement_keys(fc.confirmed_requirement_keys)
|
||||
if confirmed_keys is not None and _ADJUSTMENT_REQUEST in confirmed_keys:
|
||||
# The accepted request moved into history; do not lose its source cue
|
||||
# when the next form clears the current request for another submission.
|
||||
fc.confirmed_requirement_keys = [key for key in confirmed_keys if key != _ADJUSTMENT_REQUEST]
|
||||
if isinstance(previous_request, str) and previous_request.strip() and _ADJUSTMENT_HISTORY not in confirmed_keys:
|
||||
fc.confirmed_requirement_keys.append(_ADJUSTMENT_HISTORY)
|
||||
fc.form_fields = [field for field in fc.form_fields if field.get("key") != _ADJUSTMENT_REQUEST]
|
||||
fc.form_fields.append(
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ def _reset_for_new_task(session: Session, context: DifyBuilderContext, mode: Ent
|
||||
context.paused = False
|
||||
context.recovery_class = ""
|
||||
context.attachment_material = []
|
||||
context.confirmed_requirement_keys = None
|
||||
context.retained_test_input_ref = context.test_input_ref or context.retained_test_input_ref
|
||||
context.confirmed_consent_id = ""
|
||||
context.confirmed_command_id = ""
|
||||
|
||||
@@ -359,6 +359,7 @@ class SqlFixIntentRepository:
|
||||
context.last_repair_error = context.last_repair_graph_revision = context.last_edit_rejection = ""
|
||||
context.goal_text = ""
|
||||
context.requirements = context.edit_rules = context.resource_selection = {}
|
||||
context.confirmed_requirement_keys = None
|
||||
context.plan_items = context.built_node_ids = context.edit_target_node_ids = []
|
||||
context.attachment_material = []
|
||||
context.last_snapshot_hash = revision
|
||||
|
||||
@@ -153,6 +153,34 @@ def has_knowledge_source_cue(goal: str, requirements: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def validated_confirmed_requirement_keys(value: object) -> list[str] | None:
|
||||
"""Copy server provenance; malformed or absent data retains legacy scanning."""
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or any(not isinstance(key, str) or not key for key in value)
|
||||
or len(set(value)) != len(value)
|
||||
):
|
||||
return None
|
||||
return list(value)
|
||||
|
||||
|
||||
def has_confirmed_knowledge_source_cue(
|
||||
goal: str, requirements: dict[str, object], confirmed_keys: list[str] | None
|
||||
) -> bool:
|
||||
"""Discount derived summaries only for an already recognized raw exclusion.
|
||||
|
||||
Keep all requirements for unknown provenance or goals without an explicit
|
||||
excluded source. Confirmed keys refer to current values, including defaults;
|
||||
a missing current key is unproven and must keep the source gate closed.
|
||||
"""
|
||||
keys = validated_confirmed_requirement_keys(confirmed_keys)
|
||||
if keys is None or not _KNOWLEDGE_CUE.search(goal) or has_knowledge_source_cue(goal, {}):
|
||||
return has_knowledge_source_cue(goal, requirements)
|
||||
if any(key not in requirements for key in keys):
|
||||
return True
|
||||
return has_knowledge_source_cue(goal, {key: requirements[key] for key in keys})
|
||||
|
||||
|
||||
class EditPlanNotActionableError(RuntimeError):
|
||||
"""An Edit goal has no approvable change; never a stored/public plan."""
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from core.dify_builder.models import (
|
||||
Risk,
|
||||
TestInput,
|
||||
)
|
||||
from services.dify_builder.plan import validated_confirmed_requirement_keys
|
||||
|
||||
|
||||
def context_to_dict(fc: DifyBuilderContext) -> dict[str, Any]:
|
||||
@@ -36,6 +37,7 @@ def context_to_dict(fc: DifyBuilderContext) -> dict[str, Any]:
|
||||
from services.dify_builder.task_lifecycle import validated_task_summaries
|
||||
|
||||
result = asdict(fc)
|
||||
result["confirmed_requirement_keys"] = validated_confirmed_requirement_keys(fc.confirmed_requirement_keys)
|
||||
result["code_risk_assessments"] = validated_assessments(fc.code_risk_assessments)
|
||||
result["task_summaries"] = [asdict(summary) for summary in validated_task_summaries(fc.task_summaries)]
|
||||
return result
|
||||
@@ -84,6 +86,7 @@ def context_from_dict(d: dict[str, Any]) -> DifyBuilderContext:
|
||||
attachment_material=list(d.get("attachment_material") or []),
|
||||
active_entry_mode=d.get("active_entry_mode", ""),
|
||||
requirements=dict(d.get("requirements") or {}),
|
||||
confirmed_requirement_keys=validated_confirmed_requirement_keys(d.get("confirmed_requirement_keys")),
|
||||
plan_items=list(d.get("plan_items") or []),
|
||||
plan_title=d.get("plan_title", ""),
|
||||
plan_input_summary=d.get("plan_input_summary", ""),
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Server confirmation provenance bounds only explicit raw source exclusions."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import override
|
||||
|
||||
import pytest
|
||||
|
||||
from core.dify_builder.models import Action, Actor, ConversationItem, DifyBuilderContext, EntryMode, Session, Turn
|
||||
from core.dify_builder.state import PcState
|
||||
from services.dify_builder.application.handlers_build import (
|
||||
_collect_adjustment,
|
||||
handle_capability_check,
|
||||
handle_goal_analysis,
|
||||
)
|
||||
from services.dify_builder.application.runner import Env
|
||||
from services.dify_builder.conversation_tasks import _reset_for_new_task
|
||||
from services.dify_builder.plan import WorkflowPlan
|
||||
from services.dify_builder.repository import SqlDifyBuilderRepository
|
||||
from services.dify_builder.serde import context_from_dict, context_to_dict
|
||||
from services.dify_builder.service import DifyBuilderService
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeDifyPort, InMemoryRepository
|
||||
from tests.unit_tests.services.dify_builder.test_bound_resource_plan_reconciliation import ReconciliationAgent
|
||||
from tests.unit_tests.services.dify_builder.test_knowledge_discovery import MODEL, PLAN, confirm
|
||||
from tests.unit_tests.services.dify_builder.test_knowledge_source_cue import CAPTURED_MEETING_GOAL
|
||||
|
||||
pytest_plugins = ["tests.unit_tests.services.dify_builder.test_service"]
|
||||
|
||||
CAPTURED_REQUIREMENTS = {
|
||||
"mode": "draft_only_plan_review",
|
||||
"llm_rules": [
|
||||
"区分已决定与未决定",
|
||||
"缺失负责人、日期、金额标注待确认,不编造",
|
||||
"风险与阻塞只列原文明示内容,未提及则写「原文未提及」",
|
||||
],
|
||||
"end_outputs": [{"type": "text", "source": "LLM.text", "variable": "minutes"}],
|
||||
"model_policy": "使用当前已配置可用的模型,不新增资源、工具、Code、Agent、HTTP、知识库、文件或凭据",
|
||||
"start_inputs": [{"type": "text", "label": "会议原文", "required": True, "variable": "meeting_notes"}],
|
||||
"workflow_name": "Dev ENG1235 Meeting 439e",
|
||||
"allowed_topology": "Start → LLM → End,3 个节点、2 条连线",
|
||||
"execution_policy": "先生成计划供审阅,不执行测试或发布",
|
||||
"llm_output_language": "中文",
|
||||
"llm_sections_in_order": ["结论", "待办", "待确认事项", "风险与阻塞"],
|
||||
}
|
||||
|
||||
|
||||
class ProvenanceAgent(ReconciliationAgent):
|
||||
@override
|
||||
def analyze_goal(self, goal_text: str, *, reply_language: str | None = None) -> dict[str, object]:
|
||||
return {"fields": [], "values": dict(CAPTURED_REQUIREMENTS)}
|
||||
|
||||
@override
|
||||
def propose_plan_v1(
|
||||
self, requirements: dict[str, object], *, goal_text: str, reply_language: str | None = None
|
||||
) -> WorkflowPlan:
|
||||
return PLAN
|
||||
|
||||
|
||||
def setup_context() -> tuple[Env, Session, DifyBuilderContext]:
|
||||
context = DifyBuilderContext(goal_text=CAPTURED_MEETING_GOAL, requirements=dict(CAPTURED_REQUIREMENTS))
|
||||
session = Session(app_id="app", entry_mode=EntryMode.BUILD, current_state=PcState.BUILD_CAPABILITY_CHECK)
|
||||
env = Env(agent=ProvenanceAgent(PLAN), repo=InMemoryRepository(), dify=FakeDifyPort(), now=lambda: datetime.min)
|
||||
return env, session, context
|
||||
|
||||
|
||||
def submit(env: Env, session: Session, context: DifyBuilderContext, payload: dict[str, object]) -> None:
|
||||
handle_goal_analysis(
|
||||
env,
|
||||
Turn(
|
||||
actor=Actor(account_id="actor", tenant_id="tenant"),
|
||||
action=Action(kind="submit_requirements", payload=payload),
|
||||
),
|
||||
session,
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
def _confirm(
|
||||
context: DifyBuilderContext,
|
||||
) -> tuple[Session, DifyBuilderContext, ReconciliationAgent, list[ConversationItem]]:
|
||||
agent = ReconciliationAgent(PLAN)
|
||||
session, restored, _, items = confirm(agent, offered=[], ids=[MODEL], context=context)
|
||||
return session, restored, agent, items
|
||||
|
||||
|
||||
def test_actual_fresh_analysis_ignores_only_unconfirmed_derived_source_summary() -> None:
|
||||
env, session, context = setup_context()
|
||||
result = handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
assert result.next == PcState.BUILD_INITIAL_PLAN
|
||||
agent = ReconciliationAgent(PLAN)
|
||||
result_session, restored, _, _ = confirm(agent, offered=[], ids=[MODEL], context=context)
|
||||
assert result_session.current_state == PcState.BUILD_PLAN_APPROVAL
|
||||
assert restored.goal_text == CAPTURED_MEETING_GOAL
|
||||
assert agent.requests[0]["requirements"] == CAPTURED_REQUIREMENTS
|
||||
assert not restored.resource_selection.get("knowledge_scope_decision")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["使用知识库回答", "如果需要则访问知识库", "不新增知识库", "默认使用知识库"])
|
||||
def test_explicitly_submitted_source_values_and_defaults_remain_guarded(value: str) -> None:
|
||||
env, session, context = setup_context()
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
context.form_fields = [{"key": "answer", "type": "text"}]
|
||||
context.requirements["answer"] = value
|
||||
submit(env, session, context, {"answer": value})
|
||||
result, restored, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
assert restored.confirmed_requirement_keys == ["answer"]
|
||||
|
||||
|
||||
def test_only_present_offered_payload_keys_are_confirmed_and_current_values_are_used() -> None:
|
||||
env, session, context = setup_context()
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
context.form_fields = [{"key": "answer", "type": "text"}, {"key": "not_submitted", "type": "text"}]
|
||||
context.requirements.update(answer="使用知识库", not_submitted="使用知识库")
|
||||
submit(
|
||||
env,
|
||||
session,
|
||||
context,
|
||||
{
|
||||
"answer": "无需知识库",
|
||||
"model_policy": "使用知识库",
|
||||
"forged": "使用知识库",
|
||||
"confirmed_requirement_keys": ["model_policy"],
|
||||
},
|
||||
)
|
||||
assert context.confirmed_requirement_keys == ["answer"]
|
||||
assert "forged" not in context.requirements
|
||||
assert context.requirements["model_policy"] == CAPTURED_REQUIREMENTS["model_policy"]
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_PLAN_APPROVAL
|
||||
assert len(agent.requests) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"goal", ["根据公司文档回答", "使用知识库", "如果需要使用知识库", "不使用知识库,但是使用知识检索"]
|
||||
)
|
||||
def test_raw_goal_without_complete_explicit_exclusion_keeps_full_requirements_guard(goal: str) -> None:
|
||||
env, session, context = setup_context()
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
context.goal_text = goal
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provenance", [None, "answer", {}, [1], [""], ["missing"]])
|
||||
def test_unknown_malformed_or_missing_key_provenance_never_drops_the_source_guard(provenance: object) -> None:
|
||||
_, _, context = setup_context()
|
||||
context.__dict__["confirmed_requirement_keys"] = provenance
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
|
||||
|
||||
def test_missing_confirmed_key_fails_closed_even_without_another_source_value() -> None:
|
||||
_, _, context = setup_context()
|
||||
context.requirements = {"style": "brief"}
|
||||
context.confirmed_requirement_keys = ["missing"]
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"selection",
|
||||
[
|
||||
{"offered_knowledge_ids": ["knowledge:offered"]},
|
||||
{"knowledge_source_unavailable": True},
|
||||
{"knowledge_requires_clarification": True},
|
||||
{"blocked_capability": "knowledge_retrieval_phase_two"},
|
||||
],
|
||||
)
|
||||
def test_fresh_provenance_never_waives_other_server_source_guards(selection: dict[str, object]) -> None:
|
||||
env, session, context = setup_context()
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
context.resource_selection = selection
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("keys", [None, [], ["answer"]])
|
||||
def test_confirmation_provenance_roundtrips_without_aliasing(keys: list[str] | None) -> None:
|
||||
_, _, context = setup_context()
|
||||
context.confirmed_requirement_keys = keys
|
||||
restored = context_from_dict(json.loads(json.dumps(context_to_dict(context))))
|
||||
assert getattr(restored, "confirmed_requirement_keys", "missing") == keys
|
||||
if keys is not None:
|
||||
assert restored.confirmed_requirement_keys is not None
|
||||
restored.confirmed_requirement_keys.append("other")
|
||||
assert context.confirmed_requirement_keys == keys
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["answer", {}, [1], ["answer", None], [""], ["answer", "answer"]])
|
||||
def test_malformed_serialized_provenance_decodes_as_legacy_unknown(raw: object) -> None:
|
||||
restored = context_from_dict({"confirmed_requirement_keys": raw})
|
||||
assert getattr(restored, "confirmed_requirement_keys", "missing") is None
|
||||
|
||||
|
||||
def test_missing_serialized_provenance_stays_unknown_after_a_later_submission() -> None:
|
||||
env, session, _ = setup_context()
|
||||
context = context_from_dict(
|
||||
{
|
||||
"goal_text": CAPTURED_MEETING_GOAL,
|
||||
"requirements": CAPTURED_REQUIREMENTS,
|
||||
"form_fields": [{"key": "style", "type": "text"}],
|
||||
}
|
||||
)
|
||||
submit(env, session, context, {"style": "brief"})
|
||||
assert getattr(context, "confirmed_requirement_keys", "missing") is None
|
||||
result, _, _, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
|
||||
|
||||
def test_reanalysis_replaces_stale_confirmation_provenance() -> None:
|
||||
env, session, context = setup_context()
|
||||
context.confirmed_requirement_keys = ["old"]
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
assert getattr(context, "confirmed_requirement_keys", "missing") == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", list(EntryMode))
|
||||
def test_new_task_reset_cannot_reuse_old_confirmation_provenance(mode: EntryMode) -> None:
|
||||
_, session, context = setup_context()
|
||||
context.confirmed_requirement_keys = ["model_policy"]
|
||||
_reset_for_new_task(session, context, mode)
|
||||
assert context.confirmed_requirement_keys is None
|
||||
|
||||
|
||||
def test_adjustment_history_retains_confirmed_source_after_request_is_cleared() -> None:
|
||||
env, session, context = setup_context()
|
||||
handle_capability_check(env, Turn(actor=Actor(account_id="actor", tenant_id="tenant")), session, context)
|
||||
_collect_adjustment(context)
|
||||
submit(env, session, context, {"__builder_adjustment_request": "使用知识库"})
|
||||
_collect_adjustment(context)
|
||||
assert context.requirements["__builder_adjustment_request"] == ""
|
||||
assert getattr(context, "confirmed_requirement_keys", None) == ["__builder_adjustment_history"]
|
||||
result, _, agent, _ = _confirm(context)
|
||||
assert result.current_state == PcState.BUILD_GOAL_ANALYSIS
|
||||
assert agent.requests == []
|
||||
|
||||
|
||||
def test_clear_discards_confirmation_provenance_at_the_durable_owner(
|
||||
service: DifyBuilderService, repo: SqlDifyBuilderRepository
|
||||
) -> None:
|
||||
from tests.unit_tests.services.dify_builder.test_conversation_lifecycle import lifecycle, seed
|
||||
from tests.unit_tests.services.dify_builder.test_service import _actor
|
||||
|
||||
session = seed(repo)
|
||||
current, context = repo.get_session(session.id)
|
||||
context.confirmed_requirement_keys = ["old"]
|
||||
repo.compare_and_advance(session.id, current.version, current.current_state, context, [])
|
||||
current, _ = repo.get_session(session.id)
|
||||
lifecycle().clear(
|
||||
service, session.id, _actor(), base_version=current.version, client_action_id="clear", confirmed=True
|
||||
)
|
||||
_, cleared = repo.get_session(session.id)
|
||||
assert getattr(cleared, "confirmed_requirement_keys", "missing") is None
|
||||
|
||||
|
||||
def test_fix_activation_clears_provenance_with_replaced_requirements(repo: SqlDifyBuilderRepository) -> None:
|
||||
from services.dify_builder.fix_intent_repository import SqlFixIntentRepository
|
||||
from tests.unit_tests.services.dify_builder.test_fix_intents import ACTOR, native_run, queue
|
||||
from tests.unit_tests.services.dify_builder.test_service import ACCOUNT_ID, APP_ID, TENANT_ID
|
||||
|
||||
session = Session(
|
||||
app_id=APP_ID,
|
||||
tenant_id=TENANT_ID,
|
||||
owner_account_id=ACCOUNT_ID,
|
||||
entry_mode=EntryMode.BUILD,
|
||||
current_state=PcState.BUILD_COMPLETE,
|
||||
)
|
||||
repo.create_session(
|
||||
session,
|
||||
DifyBuilderContext(requirements={"old": "使用知识库"}, confirmed_requirement_keys=["old"]),
|
||||
[],
|
||||
)
|
||||
intent = queue(repo, session, native_run(repo))
|
||||
activated = SqlFixIntentRepository(repo).activate(ACTOR, session.id, intent.id, dsl={}, revision="current-draft")
|
||||
_, context = repo.get_session(session.id)
|
||||
assert activated.status == "activated"
|
||||
assert context.requirements == {}
|
||||
assert context.confirmed_requirement_keys is None
|
||||
Reference in new issue
Block a user