diff --git a/.github/scripts/builder-confirmed-tool-units.py b/.github/scripts/builder-confirmed-tool-units.py index 995bd956df7..5ccf75bbe2a 100644 --- a/.github/scripts/builder-confirmed-tool-units.py +++ b/.github/scripts/builder-confirmed-tool-units.py @@ -1553,7 +1553,7 @@ for module, functions in { "tests.unit_tests.services.dify_builder.test_confirmed_full_run": { "test_closed_preparation_failure_is_not_reconciled_as_a_dispatched_run": 1, "test_error_after_observed_dispatch_remains_unknown": 1, - "test_failed_native_plugin_resource_uses_actual_historical_graph": 1, + "test_failed_native_plugin_resource_uses_actual_historical_graph": 3, "test_observed_preparation_failure_is_failed_and_never_redispatched": 1, "test_preparation_failure_identifies_only_the_observed_resource_nodes": 1, }, @@ -2189,7 +2189,7 @@ a4_expected["tests.unit_tests.core.dify_builder.test_handlers_build"] = { "test_mock_inputs_route_a_missing_endpoint_back_to_the_testdata_gate": 1, "test_no_gap_notice_when_the_agent_reports_no_gap": 1, "test_no_rename_callback_wired_costs_no_model_call": 1, - "test_plan_approval_approve_builds_graph_and_reveals_nodes": 1, + "test_plan_approval_approve_builds_graph_and_notifies_once": 1, "test_plan_approval_deletes_pre_existing_start_on_from_scratch_build": 1, "test_plan_approval_does_not_call_an_unapplicable_graph_a_workflow_that_cannot_start": 1, "test_plan_approval_empty_build_surfaces_error_and_keeps_canvas": 1, @@ -2581,6 +2581,33 @@ a4_expected["tests.unit_tests.services.dify_builder.native_testing.test_agent_ri "test_risk_rubric_separates_observed_destruction_from_uncertain_side_effects": 1, } ) +# Exact additions from native cancellation, fix risk preparation and complete-goal handling. +for module, functions in { + "tests.unit_tests.services.dify_builder.native_testing.test_confirmed_agent_completion": { + "test_cancel_control_repause_uses_real_workflow_stop_owner": 69, + "test_cancel_before_queued_resume_claim_stops_original_pause": 10, + "test_running_cancel_observes_native_cancelled_child_without_rewriting_failed_parent": 32, + "test_revocation_before_child_terminal_consumption_observes_original_cancelled_child": 12, + }, + "tests.unit_tests.services.dify_builder.test_code_risk_preparation": { + "test_fix_preparation_assesses_without_business_goal_and_keeps_native_confirmation": 6, + "test_fix_risk_failure_keeps_code_simulated": 8, + "test_fix_risk_snapshot_cannot_authorize_changed_execution": 6, + "test_empty_nonfix_context_cannot_acquire_code_assessment": 7, + "test_malformed_persisted_fix_context_remains_unavailable": 6, + }, + "tests.unit_tests.services.dify_builder.test_test_actions": { + "test_native_metadata_read_does_not_construct_private_command_owner": 1, + "test_native_command_keeps_owner_authorization_after_metadata_check": 3, + "test_native_command_projects_private_disclosure_only_after_execution": 2, + "test_native_metadata_read_selects_only_current_owned_input": 1, + }, + "tests.unit_tests.core.dify_builder.test_handlers_build": { + "test_complete_goal_automatically_reaches_resources_without_a_requirements_submission": 2, + "test_complete_goal_without_a_usable_plan_fails_explicitly_without_an_empty_form": 1, + }, +}.items(): + a4_expected.setdefault(module, a3_expected.get(module, {}).copy()).update(functions) for module, expected in {**a3_expected, **a4_expected}.items(): actual = {} for classname, name in identities: diff --git a/.github/workflows/builder-safe-execution-tests.yml b/.github/workflows/builder-safe-execution-tests.yml index 8724608c796..bf1a2949f04 100644 --- a/.github/workflows/builder-safe-execution-tests.yml +++ b/.github/workflows/builder-safe-execution-tests.yml @@ -948,6 +948,11 @@ jobs: 'test_status_real_http_timeout_refuses': 1, 'test_status_unsupported_and_transport_failure_refuse_without_acquire': 1}}.items(): EXPECTED.setdefault(module, {}).update(functions) + # Native transport diagnostics retain exact route and logging-refusal coverage. + EXPECTED['tests.local.dify_agent.adapters.native_testing.test_transport'].update({ + 'test_native_transport_timing_contains_only_route_identity_and_safe_outcome': 1, + 'test_native_transport_logging_failure_preserves_redirect_refusal': 1, + }) REPORT = "builder-native-agent.xml" code = pytest.main(['tests/local/dify_agent/protocol/test_shell_job_receipt.py', 'tests/local/dify_agent/server/test_binding_job_status.py', 'tests/local/dify_agent/layers/shell/test_layer.py', 'tests/local/dify_agent/client/test_run_recovery.py', 'tests/local/dify_agent/protocol/test_run_recovery.py', 'tests/local/dify_agent/runtime/test_confirmed_continuation_owner.py', 'tests/local/dify_agent/runtime/test_native_lost_ack.py', 'tests/local/dify_agent/runtime/test_run_recovery.py', 'tests/local/dify_agent/server/test_identity_profile.py', 'tests/local/dify_agent/server/test_run_recovery.py', 'tests/local/dify_agent/storage/test_run_recovery.py', 'tests/local/dify_agent/agent_stub/server/test_runtime_admission.py', 'tests/integration/dify_agent/storage/test_active_stub_identity.py', 'tests/local/dify_agent/adapters/native_testing/test_producers.py', 'tests/local/dify_agent/adapters/native_testing/test_transport.py', 'tests/local/dify_agent/layers/dify_plugin/test_layers.py', 'tests/local/dify_agent/layers/knowledge/test_layer.py', 'tests/local/dify_agent/runtime/test_run_transport.py', 'tests/local/dify_agent/server/test_app.py', 'tests/local/dify_agent/server/test_runtime_transport.py', 'tests/local/dify_agent/protocol/test_initial_preparation.py', 'tests/local/dify_agent/server/test_initial_identity.py', 'tests/local/dify_agent/protocol/test_native_testing.py', 'tests/local/dify_agent/runtime/test_native_continuation.py', 'tests/local/dify_agent/storage/test_native_continuation_store.py', 'tests/local/dify_agent/runtime/test_native_effects.py', 'tests/local/dify_agent/protocol/test_native_checkpoint_readback.py', 'tests/local/dify_agent/server/test_native_checkpoint_readback.py', '-vv', '-rA', '--tb=long', '--junitxml=builder-native-agent.xml']) problems = [] @@ -1661,6 +1666,11 @@ jobs: 'tests.unit_tests.services.workflow_runtime.test_workflow_pipeline.TestGenerateTaskPipelineCases': {'test_workflow_app_log_saved_only_on_initial_start': 1, 'test_workflow_app_log_skipped_on_resumption_start': 1}}.items(): new_expected.setdefault(module, {}).update(functions) + new_expected['tests.unit_tests.services.dify_builder.native_testing.test_agent_associations'].update({ + 'test_v2_association_validation_reads_selected_config_once': 2, + 'test_v2_single_config_read_keeps_sql_recheck_after_io': 2, + 'test_protected_read_timing_failure_does_not_change_guard_result': 1, + }) files = list(dict.fromkeys(files)) new_actual = {module: Counter() for module in new_expected} expected = {path.removeprefix("api/").removesuffix(".py").replace("/", ".") for path in files} diff --git a/api/tests/unit_tests/controllers/openapi/test_app_run.py b/api/tests/unit_tests/controllers/openapi/test_app_run.py index 7d3dcf069bb..5702afbe9de 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_run.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_run.py @@ -230,7 +230,7 @@ def _generate_stub(monkeypatch: pytest.MonkeyPatch, chunks: list[str]) -> Mock: return generate_mock -def _ctx(mode: AppMode, session: Session | Mock | None = None) -> _SealableContext: +def _ctx(mode: AppMode, session: Session | None = None) -> _SealableContext: app_model = _make_app() app_model.mode = mode return _SealableContext( @@ -241,10 +241,14 @@ def _ctx(mode: AppMode, session: Session | Mock | None = None) -> _SealableConte ) -def test_run_reads_everything_off_the_context_before_streaming(app: Flask, monkeypatch: pytest.MonkeyPatch): +def test_run_reads_everything_off_the_context_before_streaming( + app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +): generate_mock = _generate_stub(monkeypatch, ["event: a\n\n", "event: b\n\n"]) - session = Mock() - ctx = _ctx(AppMode.ADVANCED_CHAT, session) + commits: list[str] = [] + event.listen(sqlite_session, "after_commit", lambda _session: commits.append("commit")) + transaction = sqlite_session.begin() + ctx = _ctx(AppMode.ADVANCED_CHAT, sqlite_session) api = AdvancedChatRunApi() body = AdvancedChatRunPayload(inputs={}, query="hi") with app.test_request_context(f"/openapi/v1/apps/{_TEST_APP_ID}/advanced-chat:run", method="POST"): @@ -254,7 +258,8 @@ def test_run_reads_everything_off_the_context_before_streaming(app: Flask, monke assert body == "event: a\n\nevent: b\n\n" assert generate_mock.call_args.kwargs["streaming"] is True - session.commit.assert_not_called() + assert commits == [] + assert sqlite_session.get_transaction() is transaction def test_per_mode_route_refuses_an_app_of_another_mode(app: Flask, monkeypatch: pytest.MonkeyPatch): diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py index d3526c85ee1..199a87179e9 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py @@ -2,6 +2,7 @@ from datetime import UTC, datetime from unittest.mock import Mock import pytest +from sqlalchemy import event from sqlalchemy.orm import Session from core.workflow.nodes.agent_v2.validators import ( @@ -137,16 +138,17 @@ def _tool_graph(tool_data: dict) -> dict: } -def test_historical_agent_version_two_is_not_validated_as_dify_agent() -> None: +def test_historical_agent_version_two_is_not_validated_as_dify_agent(unbound_session: Session) -> None: graph = { "nodes": [{"id": "legacy-agent", "data": {"type": "agent", "version": "2"}}], "edges": [], } - session = Mock() + executions: list[str] = [] + event.listen(unbound_session, "do_orm_execute", lambda _state: executions.append("execute")) - WorkflowAgentNodeValidator.validate_published_workflow(session=session, workflow=_workflow(graph)) + WorkflowAgentNodeValidator.validate_published_workflow(session=unbound_session, workflow=_workflow(graph)) - session.scalar.assert_not_called() + assert executions == [] def _persist_validation_scope( diff --git a/api/tests/unit_tests/services/dify_builder/test_advance_task.py b/api/tests/unit_tests/services/dify_builder/test_advance_task.py index e9e740606ee..986f606453f 100644 --- a/api/tests/unit_tests/services/dify_builder/test_advance_task.py +++ b/api/tests/unit_tests/services/dify_builder/test_advance_task.py @@ -1204,8 +1204,14 @@ def _seed_build_session(repo: SqlDifyBuilderRepository) -> Session: def test_start_build_emits_complete_reply_with_form_and_reaches_goal_analysis( - repo: SqlDifyBuilderRepository, wired: Wired + repo: SqlDifyBuilderRepository, wired: Wired, monkeypatch: pytest.MonkeyPatch ) -> None: + class RequirementsAgent(StubAgent): + @override + def analyze_goal(self, goal_text: str, *, reply_language: str | None = None) -> dict[str, object]: + return {"fields": [{"key": "audience", "label": "Audience", "type": "text"}], "values": {}} + + monkeypatch.setattr(mod, "build_dify_builder_agent", lambda **_: RequirementsAgent()) events, released = wired s = _seed_build_session(repo)