fix(arch): remove expectedArtifactExtensions from bridge payload
This commit is contained in:
parent
fc965b3ec4
commit
e6437d9be7
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user