diff --git a/internal/acp/orchestrator_normalize_result_test.go b/internal/acp/orchestrator_normalize_result_test.go index 4536dd5..fc56f97 100644 --- a/internal/acp/orchestrator_normalize_result_test.go +++ b/internal/acp/orchestrator_normalize_result_test.go @@ -132,6 +132,49 @@ func TestNormalizeOpenClawTaskGetUnknownArtifactEvidenceKeepsActiveRecordRunning } } +func TestExpectedArtifactDirectoriesDoNotBlockTerminalTaskState(t *testing.T) { + params := map[string]any{"expectedArtifactDirs": []any{"reports/", "artifacts/"}} + payload := map[string]any{ + "success": true, + "status": string(TaskStateCompleted), + "artifactScope": "tasks/session/run", + "artifactDirectory": "/remote/openclaw/workspace/tasks/session/run", + "expectedArtifactDirs": []any{ + "reports/", + "artifacts/", + }, + } + + if openClawTaskGetRequiresArtifactExport(params, payload) { + t.Fatal("expectedArtifactDirs must remain non-blocking scan hints") + } + got := normalizeOpenClawTaskGetResult(params, payload, "openclaw", nil) + if status := shared.StringArg(got, "status", ""); status != string(TaskStateCompleted) { + t.Fatalf("expected terminal status to remain completed, got %#v", got) + } + if parseBool(got["pending"]) { + t.Fatalf("expected terminal payload not to become pending, got %#v", got) + } +} + +func TestRequiredArtifactExtensionsStillBlockUntilVerified(t *testing.T) { + params := map[string]any{"requiredArtifactExtensions": []any{"md"}} + payload := map[string]any{ + "success": true, + "status": string(TaskStateCompleted), + "artifactScope": "tasks/session/run", + "artifactDirectory": "/remote/openclaw/workspace/tasks/session/run", + } + + if !openClawTaskGetRequiresArtifactExport(params, payload) { + t.Fatal("requiredArtifactExtensions must remain a blocking delivery contract") + } + got := normalizeOpenClawTaskGetResult(params, payload, "openclaw", nil) + if status := shared.StringArg(got, "status", ""); status != string(TaskStateRunning) { + t.Fatalf("expected missing required artifact to remain syncing, got %#v", got) + } +} + func TestNormalizeOpenClawTaskGetUnknownArtifactEvidenceFailsAfterDeadlineWithoutRequiredArtifacts(t *testing.T) { payload := map[string]any{ "success": false, diff --git a/internal/acp/rpc_handler.go b/internal/acp/rpc_handler.go index bebec26..d8603f8 100644 --- a/internal/acp/rpc_handler.go +++ b/internal/acp/rpc_handler.go @@ -471,9 +471,11 @@ func openClawTaskGetRequiresArtifactExport(params map[string]any, payload map[st if parseBool(params["requiresExportBeforeFinalResponse"]) || parseBool(payload["requiresExportBeforeFinalResponse"]) { return true } - return len(shared.ListArg(params, "expectedArtifactDirs")) > 0 || - len(shared.ListArg(payload, "expectedArtifactDirs")) > 0 || - len(shared.ListArg(params, "requiredArtifactExtensions")) > 0 || + // expectedArtifactDirs are discovery hints for the plugin's workspace-root + // scan. They do not prove that the caller requires a file before the run can + // reach a terminal state. Treating them as a blocking contract turns a + // failed/no-output agent run into an endless "syncing-artifacts" loop. + return len(shared.ListArg(params, "requiredArtifactExtensions")) > 0 || len(shared.ListArg(payload, "requiredArtifactExtensions")) > 0 }