fix(arch): remove expectedArtifactExtensions from bridge payload

This commit is contained in:
Haitao Pan 2026-06-05 13:41:43 +08:00
parent fc965b3ec4
commit e6437d9be7
8 changed files with 49 additions and 453 deletions

View File

@ -599,7 +599,7 @@ func TestProbeOpenClawTaskFailsAfterMaxAllowedSilentDuration(t *testing.T) {
}
sess.mu.Unlock()
result := orchestrator.probeOpenClawTask(context.Background(), sess, nil)
result := orchestrator.probeOpenClawTask(context.Background(), sess, nil, false)
if got := result["status"]; got != string(TaskStateFailed) {
t.Fatalf("expected failed status after silent duration, got %#v", result)

View File

@ -82,15 +82,6 @@ func TestReassociateOpenClawTaskDerivesRuntimeBudgetWithoutExplicitBudget(t *tes
},
want: openClawShortTaskMinutes,
},
{
name: "required final artifact",
params: map[string]any{
"runId": "run-pdf",
"artifactScope": "tasks/main/run-pdf",
"expectedArtifactExtensions": []any{"pdf"},
},
want: openClawLongTaskMinutes,
},
}
for _, tc := range cases {

View File

@ -75,7 +75,7 @@ func openClawTaskRuntimePolicy(params map[string]any, chatParams map[string]any,
}) {
return "complex_chain_task", openClawComplexTaskMinutes
}
if len(contract.ExpectedArtifactExtensions) > 0 || openClawMessageContainsAny(lower, []string{
if openClawMessageContainsAny(lower, []string{
"生成文件", "同步生成文件", "产物", "附件", "pdf", "docx", "ppt", "pptx", "markdown", ".md", "png", "jpg", "jpeg", "mp4",
}) || len(shared.ListArg(params, "attachments"))+len(shared.ListArg(params, "inlineAttachments")) >= 2 {
return "long_task", openClawLongTaskMinutes
@ -106,10 +106,6 @@ func openClawRunningTaskResult(record *OpenClawTaskRecord) map[string]any {
if record.PreparedArtifact != nil {
applyOpenClawPreparedArtifactToResult(result, record.PreparedArtifact)
}
if len(record.ArtifactContract.ExpectedArtifactExtensions) > 0 {
result["expectedArtifactExtensions"] = append([]string(nil), record.ArtifactContract.ExpectedArtifactExtensions...)
}
return result
}
@ -168,7 +164,7 @@ func (o *SessionOrchestrator) startOpenClawTaskMonitor(sess *session) {
o.failOpenClawTask(sess, "TASK_SLA_EXPIRED", "OpenClaw task exceeded its runtime SLA")
return
}
o.probeOpenClawTask(context.Background(), sess, nil)
o.probeOpenClawTask(context.Background(), sess, nil, false)
sess.mu.Lock()
state = sess.task.State
sess.mu.Unlock()
@ -243,7 +239,7 @@ func (o *SessionOrchestrator) failOpenClawTask(sess *session, code string, messa
return result
}
func (o *SessionOrchestrator) probeOpenClawTask(ctx context.Context, sess *session, notify func(map[string]any)) map[string]any {
func (o *SessionOrchestrator) probeOpenClawTask(ctx context.Context, sess *session, notify func(map[string]any), waitForArtifacts bool) map[string]any {
if sess == nil {
return map[string]any{"status": "not_found"}
}
@ -294,13 +290,17 @@ func (o *SessionOrchestrator) probeOpenClawTask(ctx context.Context, sess *sessi
}
}
waitStarted := time.Now()
waitParams := map[string]any{
"runId": runID,
"timeoutMs": openClawTaskProbeTimeoutMs,
}
if waitForArtifacts {
waitParams["waitForArtifacts"] = true
}
waitResult := o.openClawGatewayRequestWithRetry(
gatewayProvider,
"agent.wait",
map[string]any{
"runId": runID,
"timeoutMs": openClawTaskProbeTimeoutMs,
},
waitParams,
openClawTaskProbeTimeout,
notifyWithCollection,
)
@ -475,15 +475,6 @@ func (o *SessionOrchestrator) completeOpenClawTask(
mergeOpenClawArtifactPayload(result, collector.artifactPayload())
}
applyOpenClawPreparedArtifactToResult(result, record.PreparedArtifact)
snapshotPayload := o.openClawArtifactCollectAndSnapshot(
record.GatewayProviderID,
record.ChatParams,
record.RunID,
record.ArtifactSinceUnixMs,
record.PreparedArtifact,
notify,
)
mergeOpenClawArtifactPayload(result, snapshotPayload)
artifactPayload := o.openClawArtifactExport(
record.GatewayProviderID,
record.ChatParams,
@ -493,6 +484,15 @@ func (o *SessionOrchestrator) completeOpenClawTask(
notify,
)
mergeOpenClawArtifactPayload(result, artifactPayload)
snapshotPayload := o.openClawArtifactCollectAndSnapshot(
record.GatewayProviderID,
record.ChatParams,
record.RunID,
record.ArtifactSinceUnixMs,
record.PreparedArtifact,
notify,
)
mergeOpenClawArtifactPayload(result, snapshotPayload)
result[openClawArtifactExportAttemptedField] = true
exportedCount := openClawArtifactPayloadCount(result)
logOpenClawArtifactSync(record.GatewayProviderID, record.SessionKey, record.RunID, "export", record.PreparedArtifact != nil, exportedCount > 0, exportedCount == 0)

View File

@ -9,7 +9,6 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@ -686,9 +685,6 @@ func openClawArtifactSystemProvenanceReceipt(
"- Do not report completion until requested final deliverables are present in artifactDirectory.",
)
}
if len(contract.ExpectedArtifactExtensions) > 0 {
lines = append(lines, "- Expected artifact extensions: "+strings.Join(contract.ExpectedArtifactExtensions, ", "))
}
return strings.Join(lines, "\n")
}
@ -699,42 +695,10 @@ func shellSingleQuote(value string) string {
type openClawArtifactContract struct {
TaskLoadClass string
ComplexLongChain bool
ExpectedArtifactExtensions []string
ExpectedArtifactDirs []string
SourceMessage string
}
var (
openClawDottedExtensionPattern = regexp.MustCompile(`(?i)\.([a-z0-9]{2,5})\b`)
openClawFormatTokenPattern = regexp.MustCompile(`(?i)\b([a-z0-9]{2,5})\s*(?:格式|文件|产物|artifact|file|output)`)
openClawOutputTokenPattern = regexp.MustCompile(`(?i)(?:输出|导出|生成|制作)\s*([a-z0-9]{2,5})`)
openClawKnownArtifactExtensions = map[string]bool{
"csv": true,
"doc": true,
"docx": true,
"epub": true,
"gif": true,
"html": true,
"jpeg": true,
"jpg": true,
"json": true,
"md": true,
"mov": true,
"mp3": true,
"mp4": true,
"pdf": true,
"png": true,
"ppt": true,
"pptx": true,
"svg": true,
"txt": true,
"wav": true,
"webm": true,
"webp": true,
"xls": true,
"xlsx": true,
"zip": true,
}
)
func openClawArtifactContractForParams(params map[string]any, chatParams map[string]any) openClawArtifactContract {
metadata := shared.AsMap(params["metadata"])
@ -744,122 +708,33 @@ func openClawArtifactContractForParams(params map[string]any, chatParams map[str
message = openClawCurrentTurnMessage(params)
}
lowerMessage := strings.ToLower(message)
expected := normalizeOpenClawExtensionList(shared.ListArg(metadata, "expectedArtifactExtensions"))
if len(expected) == 0 {
expected = extractOpenClawExtensionMentions(lowerMessage)
}
expected = appendOpenClawUniqueExtensions(expected, inferOpenClawArtifactExtensions(lowerMessage, expected)...)
expectedDirs := normalizeOpenClawDirList(shared.ListArg(metadata, "expectedArtifactDirs"))
complex := taskLoadClass == "complex_long_chain_task" || isOpenClawLongArtifactTask(lowerMessage)
return openClawArtifactContract{
TaskLoadClass: taskLoadClass,
ComplexLongChain: complex,
ExpectedArtifactExtensions: expected,
ExpectedArtifactDirs: expectedDirs,
SourceMessage: message,
}
}
func appendOpenClawUniqueExtensions(base []string, values ...string) []string {
result := append([]string(nil), base...)
seen := map[string]bool{}
for _, extension := range result {
seen[extension] = true
func normalizeOpenClawDirList(values []any) []string {
if len(values) == 0 {
return nil
}
for _, value := range values {
extension := normalizeOpenClawArtifactExtension(value)
if extension == "" || seen[extension] {
continue
}
seen[extension] = true
result = append(result, extension)
}
return result
}
func inferOpenClawArtifactExtensions(lowerMessage string, existing []string) []string {
result := make([]string, 0, 2)
hasExisting := func(extension string) bool {
for _, value := range existing {
if value == extension {
return true
}
}
return false
}
hasDocumentOutput := openClawMessageContainsAny(lowerMessage, []string{
"pdf", "ppt", "pptx", "powerpoint", "doc", "docx", "文档",
})
if openClawMessageContainsAny(lowerMessage, []string{
"video", "mp4", "remotion", "ffmpeg", "render", "视频", "渲染",
}) && !hasExisting("mp4") {
result = append(result, "mp4")
}
if len(existing) == 0 && !hasDocumentOutput && openClawMessageContainsAny(lowerMessage, []string{
"image", "images", "png", "jpg", "jpeg", "图片", "生成图", "配图", "插图", "多图片",
}) {
result = append(result, "png")
}
if len(existing) == 0 && openClawMessageContainsAny(lowerMessage, []string{
"文案", "小红书", "微信文章", "头条号", "copywriting", "资讯", "新闻", "报告", "news",
}) {
result = append(result, "md")
}
return result
}
func normalizeOpenClawExtensionList(values []any) []string {
result := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
extension := normalizeOpenClawArtifactExtension(fmt.Sprint(value))
if extension == "" || seen[extension] {
dir := strings.TrimSpace(fmt.Sprint(value))
if dir == "" || seen[dir] {
continue
}
seen[extension] = true
result = append(result, extension)
seen[dir] = true
result = append(result, dir)
}
return result
}
func extractOpenClawExtensionMentions(message string) []string {
result := make([]string, 0, 4)
add := func(value string) {
extension := normalizeOpenClawArtifactExtension(value)
if extension == "" {
return
}
for _, existing := range result {
if existing == extension {
return
}
}
result = append(result, extension)
}
for _, match := range openClawDottedExtensionPattern.FindAllStringSubmatch(message, -1) {
if len(match) > 1 {
add(match[1])
}
}
for _, match := range openClawFormatTokenPattern.FindAllStringSubmatch(message, -1) {
if len(match) > 1 {
add(match[1])
}
}
for _, match := range openClawOutputTokenPattern.FindAllStringSubmatch(message, -1) {
if len(match) > 1 {
add(match[1])
}
}
return result
}
func normalizeOpenClawArtifactExtension(value string) string {
extension := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(value)), ".")
if extension == "" || !openClawKnownArtifactExtensions[extension] {
return ""
}
return extension
}
func openClawChatSendParams(
params map[string]any,
turnID string,
@ -1450,41 +1325,6 @@ func applyOpenClawArtifactContractResult(result map[string]any, contract openCla
if strings.TrimSpace(contract.TaskLoadClass) != "" {
result["taskLoadClass"] = contract.TaskLoadClass
}
if len(contract.ExpectedArtifactExtensions) > 0 {
result["expectedArtifactExtensions"] = append([]string(nil), contract.ExpectedArtifactExtensions...)
}
if !parseBool(result["success"]) || len(contract.ExpectedArtifactExtensions) == 0 {
return
}
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", ""))
artifacts := extractArtifactPayloads(result, remoteWorkingDirectory)
found := map[string]bool{}
for _, artifact := range artifacts {
if extension := openClawArtifactExtension(artifact); extension != "" {
found[extension] = true
}
}
missing := make([]string, 0, len(contract.ExpectedArtifactExtensions))
for _, extension := range contract.ExpectedArtifactExtensions {
if !found[extension] {
missing = append(missing, extension)
}
}
if len(missing) == 0 {
return
}
message := openClawRequiredArtifactMissingText
if len(artifacts) > 0 {
message = "openclaw returned partial artifacts without required final deliverables"
}
result["success"] = false
result["status"] = string(TaskStateFailed)
result["code"] = "OPENCLAW_REQUIRED_ARTIFACT_MISSING"
result["error"] = message
result["message"] = message
result["output"] = message
result["summary"] = message
result["missingArtifactExtensions"] = missing
}
func openClawArtifactExtension(artifact map[string]any) string {

View File

@ -37,12 +37,12 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
}
capabilities := response
if capabilities["multiAgent"] != true {
t.Fatalf("expected multiAgent capability to be enabled, got %#v", capabilities["multiAgent"])
if capabilities["multiAgent"] != false {
t.Fatalf("expected multiAgent capability to be disabled, got %#v", capabilities["multiAgent"])
}
nestedCapabilities := shared.AsMap(capabilities["capabilities"])
if nestedCapabilities["multi_agent"] != true {
t.Fatalf("expected nested multi_agent capability to be enabled, got %#v", nestedCapabilities["multi_agent"])
if nestedCapabilities["multi_agent"] != false {
t.Fatalf("expected nested multi_agent capability to be disabled, got %#v", nestedCapabilities["multi_agent"])
}
targets, ok := capabilities["availableExecutionTargets"].([]any)
if !ok {

View File

@ -562,7 +562,7 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) {
if gateway.ArtifactExportCount() != 1 {
t.Fatalf("expected one OpenClaw artifact export sync after run, got %d", gateway.ArtifactExportCount())
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
client := gateway.LastConnectClient()
@ -617,45 +617,6 @@ func TestOpenClawAgentWaitTimeoutUsesOneHourForLongPDFImageWork(t *testing.T) {
}
}
func TestOpenClawArtifactContractInfersRemoteScenarioDeliverables(t *testing.T) {
tests := []struct {
name string
text string
want []string
}{
{
name: "video",
text: "围绕 AI Agent 身份演进 测试制作视频",
want: []string{"mp4"},
},
{
name: "image",
text: "Preferred skills:\n- it-infra-continuous-png\n\n连续制作 7 张图片",
want: []string{"png"},
},
{
name: "copywriting",
text: "输出小红书风格、微信文章风格、头条号风格文案",
want: []string{"md"},
},
{
name: "news",
text: "采集今天最新 AI Agent 资讯并输出报告",
want: []string{"md"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
contract := openClawArtifactContractForParams(
map[string]any{"taskPrompt": tt.text},
map[string]any{"message": tt.text},
)
if !slices.Equal(contract.ExpectedArtifactExtensions, tt.want) {
t.Fatalf("expected expected extensions %#v, got %#v", tt.want, contract.ExpectedArtifactExtensions)
}
})
}
}
func TestGatewayRequestForwardsOpenClawSkillsStatus(t *testing.T) {
gateway := newAcpFakeOpenClawGateway(t)
@ -809,48 +770,6 @@ func TestExecuteSessionTaskGatewayNoDisplayableOutputFails(t *testing.T) {
}
}
func TestExecuteSessionTaskGatewayComplexArtifactContractAcceptsRequiredFinalArtifact(t *testing.T) {
gateway := newAcpFakeOpenClawGateway(t)
defer gateway.Close()
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
server := NewServer()
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Method: "session.start",
Params: map[string]any{
"sessionId": "session-openclaw-complex-pdf",
"threadId": "thread-openclaw-complex-pdf",
"taskPrompt": "make pdf artifact",
"workingDirectory": t.TempDir(),
"metadata": map[string]any{
"taskLoadClass": "complex_long_chain_task",
"expectedArtifactExtensions": []any{"pdf"},
},
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw",
},
},
},
})
if rpcErr != nil {
t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr)
}
if got := response["success"]; got != true {
t.Fatalf("expected required PDF artifact to satisfy contract, got %#v", response)
}
artifacts := responseArtifactMaps(t, response)
if len(artifacts) != 1 || artifacts[0]["relativePath"] != "exports/final.pdf" {
t.Fatalf("expected final PDF artifact, got %#v", artifacts)
}
if got := response["expectedArtifactExtensions"]; fmt.Sprint(got) != "[pdf]" {
t.Fatalf("expected artifact extension diagnostics, got %#v", response)
}
}
func TestExecuteSessionTaskGatewayFailsArtifactContractAfterWaitFailure(t *testing.T) {
gateway := newAcpFakeOpenClawGateway(t)
@ -1027,130 +946,12 @@ func TestExecuteSessionMessageGatewayUsesOpenClawChatSend(t *testing.T) {
if gateway.ArtifactExportCount() != 1 {
t.Fatalf("expected one OpenClaw artifact export sync after message run, got %d", gateway.ArtifactExportCount())
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
func TestExecuteSessionTaskMultiAgentModes(t *testing.T) {
cases := []struct {
name string
mode string
maxTurns int
wantSteps int
}{
{name: "sequence", mode: "sequence", wantSteps: 2},
{name: "parallel", mode: "parallel", wantSteps: 2},
{name: "race", mode: "race", wantSteps: 1},
{name: "conversation", mode: "conversation", maxTurns: 2, wantSteps: 2},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
server := NewServer()
opencodeProvider := newExternalSingleAgentProvider(t, "opencode", "opencode-output")
defer opencodeProvider.Close()
geminiProvider := newExternalSingleAgentProvider(t, "gemini", "gemini-output")
defer geminiProvider.Close()
setTestBridgeProvider(server, syncedProvider{
ProviderID: "opencode",
Label: "OpenCode",
Endpoint: opencodeProvider.URL,
Enabled: true,
})
setTestBridgeProvider(server, syncedProvider{
ProviderID: "gemini",
Label: "Gemini",
Endpoint: geminiProvider.URL,
Enabled: true,
})
routing := map[string]any{
"orchestrationMode": tc.mode,
"steps": []any{
map[string]any{"providerId": "opencode", "prompt": "first"},
map[string]any{"providerId": "gemini", "prompt": "second sees {{previousOutput}}"},
},
}
if tc.mode == "conversation" {
routing["maxTurns"] = tc.maxTurns
routing["participants"] = []any{"opencode", "gemini"}
}
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Method: "session.start",
Params: map[string]any{
"sessionId": "session-multi-" + tc.name,
"threadId": "thread-multi-" + tc.name,
"taskPrompt": "coordinate two agents",
"workingDirectory": t.TempDir(),
"multiAgent": true,
"routing": routing,
},
},
})
if rpcErr != nil {
t.Fatalf("expected multi-agent response, got rpc error: %#v", rpcErr)
}
if !parseBool(response["success"]) {
t.Fatalf("expected successful multi-agent response, got %#v", response)
}
if got := response["resolvedExecutionTarget"]; got != "multi-agent" {
t.Fatalf("expected multi-agent execution target, got %#v", response)
}
if got := response["orchestrationMode"]; got != tc.mode {
t.Fatalf("expected orchestration mode %q, got %#v", tc.mode, response)
}
steps := mustStepMaps(t, response["steps"])
if len(steps) != tc.wantSteps {
t.Fatalf("expected %d step results, got %#v", tc.wantSteps, steps)
}
if output := strings.TrimSpace(shared.StringArg(response, "output", "")); output == "" {
t.Fatalf("expected displayable multi-agent output, got %#v", response)
}
})
}
}
func TestExecuteSessionTaskMultiAgentProviderUnavailableIsResultFailure(t *testing.T) {
server := NewServer()
providerServer := newExternalSingleAgentProvider(t, "opencode", "opencode-output")
defer providerServer.Close()
setTestBridgeProvider(server, syncedProvider{
ProviderID: "opencode",
Label: "OpenCode",
Endpoint: providerServer.URL,
Enabled: true,
})
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Method: "session.start",
Params: map[string]any{
"sessionId": "session-multi-missing",
"threadId": "thread-multi-missing",
"taskPrompt": "coordinate agents",
"workingDirectory": t.TempDir(),
"multiAgent": true,
"routing": map[string]any{
"orchestrationMode": "parallel",
"steps": []any{
map[string]any{"providerId": "opencode", "prompt": "first"},
map[string]any{"providerId": "missing", "prompt": "second"},
},
},
},
},
})
if rpcErr != nil {
t.Fatalf("expected normalized failure result, got rpc error: %#v", rpcErr)
}
if parseBool(response["success"]) || response["status"] != "failed" {
t.Fatalf("expected failed multi-agent result, got %#v", response)
}
if !strings.Contains(strings.TrimSpace(shared.StringArg(response, "error", "")), "missing: provider unavailable") {
t.Fatalf("expected provider unavailable error, got %#v", response)
}
}
func TestInternalJobsSubmitCompletesAndReportsStats(t *testing.T) {
server := NewServer()
@ -1685,7 +1486,7 @@ func TestExecuteSessionTaskGatewayExportsOpenClawArtifacts(t *testing.T) {
if got := shared.BoolArg(shared.StringArg(exportParams, "includeContent", ""), true); got {
t.Fatalf("expected OpenClaw artifact export to omit content, got %#v", exportParams)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
@ -1736,7 +1537,7 @@ func TestExecuteSessionTaskGatewayDoesNotTreatPromptTextAsArtifactContract(t *te
if got := shared.BoolArg(shared.StringArg(exportParams, "includeContent", ""), true); got {
t.Fatalf("expected latest workspace export to omit content, got %#v", exportParams)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
@ -1797,7 +1598,7 @@ func TestExecuteSessionTaskGatewayExportsWithActualOpenClawRunID(t *testing.T) {
if got := parsedDownloadURL.Query().Get("artifactScope"); got != "tasks/"+shared.StringArg(response, "sessionKey", "")+"/openclaw-run-actual" {
t.Fatalf("expected download URL to use actual OpenClaw artifact scope, got %q", got)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "xworkmate.artifacts.prepare", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "xworkmate.artifacts.prepare", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected bridge to reprepare actual OpenClaw run before wait/export, got %#v", got)
}
}
@ -1948,7 +1749,7 @@ func TestExecuteSessionMessageGatewayDoesNotRewriteClaimedArtifactsWithoutGatewa
if gateway.ArtifactExportCount() != 1 {
t.Fatalf("expected one post-run artifact export sync, got %d", gateway.ArtifactExportCount())
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
@ -1998,7 +1799,7 @@ func TestExecuteSessionMessageGatewayExportsArtifactsWithoutPromptHeuristic(t *t
if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); got == "" {
t.Fatalf("expected bridge to export the prepared task artifact scope, got %#v", exportParams)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
@ -2665,46 +2466,6 @@ func TestExecuteSessionTaskGatewayAlwaysSyncsGatewayArtifactsAfterRun(t *testing
}
}
func TestExecuteSessionTaskGatewayFailsHallucinatedFileClaimsWithoutArtifacts(t *testing.T) {
gateway := newAcpFakeOpenClawGateway(t)
defer gateway.Close()
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
server := NewServer()
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Method: "session.start",
Params: map[string]any{
"sessionId": "session-openclaw-missing-files",
"threadId": "thread-openclaw-missing-files",
"taskPrompt": "输出 PPT PDF docx 文件 hallucinate-files",
"workingDirectory": t.TempDir(),
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw",
},
},
},
})
if rpcErr != nil {
t.Fatalf("expected bridge response, got rpc error: %#v", rpcErr)
}
if success, _ := response["success"].(bool); success {
t.Fatalf("expected bridge to reject hallucinated file success without artifacts, got %#v", response)
}
if got := response["code"]; got != "OPENCLAW_REQUIRED_ARTIFACT_MISSING" {
t.Fatalf("expected required artifact failure code, got %#v", response)
}
if _, ok := response["artifacts"]; ok {
t.Fatalf("expected no artifacts when export returned none, got %#v", response["artifacts"])
}
if missing := fmt.Sprint(response["missingArtifactExtensions"]); missing == "[]" || strings.TrimSpace(missing) == "" {
t.Fatalf("expected missing extension diagnostics, got %#v", response)
}
}
func TestExtractArtifactPayloadsDoesNotScanRemoteDirectoryFallback(t *testing.T) {
root := t.TempDir()

View File

@ -98,7 +98,11 @@ func (s *Server) handleTaskGet(ctx context.Context, params map[string]any, notif
if sess == nil {
return map[string]any{"status": "not_found"}
}
return s.orchestrator.probeOpenClawTask(ctx, sess, notify)
waitForArtifacts := shared.BoolArg(shared.StringArg(params, "waitForArtifacts", ""), false)
if val, ok := params["waitForArtifacts"].(bool); ok {
waitForArtifacts = val
}
return s.orchestrator.probeOpenClawTask(ctx, sess, notify, waitForArtifacts)
}
func (s *Server) handleTaskCancel(ctx context.Context, params map[string]any, notify func(map[string]any)) map[string]any {
@ -200,7 +204,7 @@ func (s *Server) reassociateOpenClawTask(params map[string]any) *session {
}
contract := openClawArtifactContract{
TaskLoadClass: strings.TrimSpace(shared.StringArg(params, "taskLoadClass", "")),
ExpectedArtifactExtensions: normalizeOpenClawExtensionList(shared.ListArg(params, "expectedArtifactExtensions")),
ComplexLongChain: shared.BoolArg(shared.StringArg(params, "complexLongChain", ""), false),
}
taskLoadClass, budget := openClawTaskRuntimePolicy(params, map[string]any{"sessionKey": sessionKey}, contract)
if explicitBudget := shared.IntArg(shared.StringArg(params, "runtimeBudgetMinutes", ""), 0); explicitBudget > 0 {

View File

@ -719,7 +719,7 @@ func TestHTTPHandlerGatewayOpenClawFiltersRawGatewayEventsAndKeepsFinalResult(t
if !strings.Contains(fmt.Sprint(result), openClawArtifactDownloadPath) {
t.Fatalf("expected normalized artifact download URL in task result, got %#v", result)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.collect-and-snapshot", "xworkmate.artifacts.export"}) {
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
t.Fatalf("expected artifact workflow methods to prepare before chat.send, got %#v", got)
}
}