Merge pull request #5 from x-evor/fix/validate-openclaw-smoke-test
Fix/validate openclaw smoke test
This commit is contained in:
commit
77cd9551fe
@ -94,7 +94,8 @@ func openClawRunningTaskResult(record *OpenClawTaskRecord) map[string]any {
|
||||
"runId": record.RunID,
|
||||
"sessionId": record.SessionID,
|
||||
"threadId": record.ThreadID,
|
||||
"sessionKey": record.SessionKey,
|
||||
"appThreadKey": record.ThreadID,
|
||||
"openclawSessionKey": record.SessionKey,
|
||||
"mode": router.ExecutionTargetGatewayChat,
|
||||
"resolvedGatewayProviderId": record.GatewayProviderID,
|
||||
"taskLoadClass": record.TaskLoadClass,
|
||||
@ -463,7 +464,8 @@ func (o *SessionOrchestrator) completeOpenClawTask(
|
||||
"runId": record.RunID,
|
||||
"sessionId": record.SessionID,
|
||||
"threadId": record.ThreadID,
|
||||
"sessionKey": record.SessionKey,
|
||||
"appThreadKey": record.ThreadID,
|
||||
"openclawSessionKey": record.SessionKey,
|
||||
"mode": router.ExecutionTargetGatewayChat,
|
||||
"resolvedExecutionTarget": router.ExecutionTargetGatewayChat,
|
||||
"resolvedProviderId": record.GatewayProviderID,
|
||||
@ -630,7 +632,8 @@ func openClawTaskMapLocked(sess *session) map[string]any {
|
||||
"threadId": task.ThreadID,
|
||||
"turnId": task.TurnID,
|
||||
"runId": task.RunID,
|
||||
"sessionKey": task.SessionKey,
|
||||
"appThreadKey": task.ThreadID,
|
||||
"openclawSessionKey": task.SessionKey,
|
||||
"provider": task.Provider,
|
||||
"target": task.Target,
|
||||
"gatewayProviderId": task.GatewayProviderID,
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ThreadSessionMapper struct {
|
||||
mu sync.Mutex
|
||||
sessions map[string]string
|
||||
}
|
||||
|
||||
func NewThreadSessionMapper() *ThreadSessionMapper {
|
||||
return &ThreadSessionMapper{sessions: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (m *ThreadSessionMapper) OpenClawSessionID(threadID string, sessionID string) string {
|
||||
key := strings.TrimSpace(threadID)
|
||||
if key == "" {
|
||||
key = strings.TrimSpace(sessionID)
|
||||
}
|
||||
if key == "" {
|
||||
key = "main"
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if existing := strings.TrimSpace(m.sessions[key]); existing != "" {
|
||||
return existing
|
||||
}
|
||||
sum := sha256.Sum256([]byte(key))
|
||||
session := "xwm-" + hex.EncodeToString(sum[:])[:24]
|
||||
m.sessions[key] = session
|
||||
return session
|
||||
}
|
||||
@ -324,7 +324,7 @@ func (o *SessionOrchestrator) startOpenClawGatewayTask(
|
||||
}
|
||||
}
|
||||
sessionKey := o.openClawSessionKey(params, turnID)
|
||||
params = withOpenClawWritableWorkspace(params, sessionKey)
|
||||
params = withOpenClawWritableWorkspace(params, openClawAppThreadKey(params))
|
||||
chatParams, rpcErr := openClawChatSendParamsWithSessionKey(params, turnID, sessionKey)
|
||||
if rpcErr != nil {
|
||||
return nil, rpcErr
|
||||
@ -333,8 +333,10 @@ func (o *SessionOrchestrator) startOpenClawGatewayTask(
|
||||
artifactSinceUnixMs := time.Now().Add(-1 * time.Second).UnixMilli()
|
||||
preparedArtifact, prepareErr := o.openClawArtifactPrepare(
|
||||
gatewayProvider,
|
||||
params,
|
||||
sessionKey,
|
||||
turnID,
|
||||
artifactContract,
|
||||
notifyWithCollection,
|
||||
)
|
||||
if prepareErr != nil {
|
||||
@ -362,12 +364,17 @@ func (o *SessionOrchestrator) startOpenClawGatewayTask(
|
||||
return nil, gatewayRPCError(sendResult.Error, "openclaw chat.send failed")
|
||||
}
|
||||
sendPayload := shared.AsMap(sendResult.Payload)
|
||||
if rpcErr := validateOpenClawAcceptedSessionKey(sendPayload, sessionKey); rpcErr != nil {
|
||||
return nil, rpcErr
|
||||
}
|
||||
runID := strings.TrimSpace(shared.StringArg(sendPayload, "runId", turnID))
|
||||
if runID != turnID {
|
||||
preparedArtifact, prepareErr = o.openClawArtifactPrepare(
|
||||
gatewayProvider,
|
||||
params,
|
||||
sessionKey,
|
||||
runID,
|
||||
artifactContract,
|
||||
notifyWithCollection,
|
||||
)
|
||||
if prepareErr != nil {
|
||||
@ -566,22 +573,22 @@ func openClawPreparedArtifactScopeFromPayload(payload map[string]any) *openClawP
|
||||
|
||||
func (o *SessionOrchestrator) openClawArtifactPrepare(
|
||||
gatewayProvider string,
|
||||
params map[string]any,
|
||||
sessionKey string,
|
||||
runID string,
|
||||
artifactContract openClawArtifactContract,
|
||||
notify func(map[string]any),
|
||||
) (*openClawPreparedArtifactScope, *shared.RPCError) {
|
||||
sessionKey = strings.TrimSpace(sessionKey)
|
||||
runID = strings.TrimSpace(runID)
|
||||
if sessionKey == "" || runID == "" {
|
||||
return nil, &shared.RPCError{Code: -32602, Message: "openclaw artifact prepare requires sessionKey and runId"}
|
||||
return nil, &shared.RPCError{Code: -32602, Message: "openclaw artifact prepare requires openclawSessionKey and runId"}
|
||||
}
|
||||
prepareParams := openClawSessionPrepareParams(params, sessionKey, runID, artifactContract)
|
||||
prepareResult := o.openClawGatewayRequestWithRetry(
|
||||
gatewayProvider,
|
||||
"xworkmate.artifacts.prepare",
|
||||
map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": runID,
|
||||
},
|
||||
"xworkmate.session.prepare",
|
||||
prepareParams,
|
||||
30*time.Second,
|
||||
notify,
|
||||
)
|
||||
@ -595,6 +602,50 @@ func (o *SessionOrchestrator) openClawArtifactPrepare(
|
||||
return prepared, nil
|
||||
}
|
||||
|
||||
func openClawSessionPrepareParams(params map[string]any, openClawSessionKey string, runID string, artifactContract openClawArtifactContract) map[string]any {
|
||||
appThreadKey := openClawAppThreadKey(params)
|
||||
result := map[string]any{
|
||||
"schemaVersion": 1,
|
||||
"appThreadKey": appThreadKey,
|
||||
"openclawSessionKey": strings.TrimSpace(openClawSessionKey),
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"requestId": strings.TrimSpace(runID),
|
||||
"externalTaskId": strings.TrimSpace(runID),
|
||||
}
|
||||
if len(artifactContract.ExpectedArtifactDirs) > 0 {
|
||||
result["expectedArtifactDirs"] = append([]string(nil), artifactContract.ExpectedArtifactDirs...)
|
||||
}
|
||||
if sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", "")); sessionID != "" {
|
||||
result["sessionId"] = sessionID
|
||||
}
|
||||
if threadID := strings.TrimSpace(shared.StringArg(params, "threadId", "")); threadID != "" {
|
||||
result["threadId"] = threadID
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func openClawAppThreadKey(params map[string]any) string {
|
||||
if value := strings.TrimSpace(shared.StringArg(params, "appThreadKey", "")); value != "" {
|
||||
return value
|
||||
}
|
||||
metadata := shared.AsMap(params["metadata"])
|
||||
for _, key := range []string{"appThreadKey"} {
|
||||
if value := strings.TrimSpace(shared.StringArg(metadata, key, "")); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
contract := shared.AsMap(metadata["xworkmateTaskArtifactContract"])
|
||||
if value := strings.TrimSpace(shared.StringArg(contract, "appThreadKey", "")); value != "" {
|
||||
return value
|
||||
}
|
||||
for _, key := range []string{"threadId", "sessionId"} {
|
||||
if value := strings.TrimSpace(shared.StringArg(params, key, "")); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return "main"
|
||||
}
|
||||
|
||||
func applyOpenClawPreparedArtifactToResult(result map[string]any, prepared *openClawPreparedArtifactScope) {
|
||||
if result == nil || prepared == nil {
|
||||
return
|
||||
@ -775,14 +826,14 @@ func openClawChatSendParamsWithSessionKey(
|
||||
return chatParams, nil
|
||||
}
|
||||
|
||||
func withOpenClawWritableWorkspace(params map[string]any, sessionKey string) map[string]any {
|
||||
func withOpenClawWritableWorkspace(params map[string]any, appThreadKey string) map[string]any {
|
||||
workingDirectory := strings.TrimSpace(shared.StringArg(params, "workingDirectory", ""))
|
||||
remoteHint := strings.TrimSpace(shared.StringArg(params, "remoteWorkingDirectoryHint", ""))
|
||||
ownerScoped := firstOwnerScopedWorkspace(workingDirectory, remoteHint)
|
||||
if ownerScoped == "" {
|
||||
return params
|
||||
}
|
||||
writable := openClawWritableWorkspaceForOwnerPath(ownerScoped, sessionKey)
|
||||
writable := openClawWritableWorkspaceForOwnerPath(ownerScoped, appThreadKey)
|
||||
if writable == "" || writable == ownerScoped {
|
||||
return params
|
||||
}
|
||||
@ -1161,14 +1212,46 @@ func compactOpenClawTexts(texts []string) []string {
|
||||
}
|
||||
|
||||
func (o *SessionOrchestrator) openClawSessionKey(params map[string]any, turnID string) string {
|
||||
threadID := strings.TrimSpace(shared.StringArg(params, "threadId", ""))
|
||||
sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", ""))
|
||||
if o != nil && o.server != nil && o.server.openClawSessions != nil {
|
||||
return o.server.openClawSessions.OpenClawSessionID(threadID, sessionID)
|
||||
if explicit := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", "")); explicit != "" {
|
||||
return explicit
|
||||
}
|
||||
if appThreadKey := openClawAppThreadKey(params); appThreadKey != "" {
|
||||
return openClawAgentMainSessionKey(appThreadKey)
|
||||
}
|
||||
return fallbackOpenClawSessionKey(params, turnID)
|
||||
}
|
||||
|
||||
func openClawAgentMainSessionKey(appThreadKey string) string {
|
||||
appThreadKey = strings.TrimSpace(appThreadKey)
|
||||
if appThreadKey == "" {
|
||||
return "main"
|
||||
}
|
||||
return appThreadKey
|
||||
}
|
||||
|
||||
func validateOpenClawAcceptedSessionKey(payload map[string]any, expectedSessionKey string) *shared.RPCError {
|
||||
actual := strings.TrimSpace(shared.StringArg(payload, "sessionKey", ""))
|
||||
expected := strings.TrimSpace(expectedSessionKey)
|
||||
if actual == "" || expected == "" || actual == expected {
|
||||
return nil
|
||||
}
|
||||
return &shared.RPCError{
|
||||
Code: -32002,
|
||||
Message: fmt.Sprintf(
|
||||
"OPENCLAW_SESSION_MISMATCH: expected %s but OpenClaw accepted %s",
|
||||
expected,
|
||||
actual,
|
||||
),
|
||||
Data: map[string]any{
|
||||
"code": "OPENCLAW_SESSION_MISMATCH",
|
||||
"expectedSessionKey": expected,
|
||||
"acceptedSessionKey": actual,
|
||||
"expectedOpenClawKey": expected,
|
||||
"actualOpenClawKey": actual,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackOpenClawSessionKey(params map[string]any, turnID string) string {
|
||||
for _, key := range []string{"threadId", "sessionId"} {
|
||||
if value := strings.TrimSpace(shared.StringArg(params, key, "")); value != "" {
|
||||
@ -1195,12 +1278,12 @@ func (o *SessionOrchestrator) openClawArtifactExport(
|
||||
return nil
|
||||
}
|
||||
exportParams := map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
"maxInlineBytes": 0,
|
||||
"includeContent": false,
|
||||
"openclawSessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
"maxInlineBytes": 0,
|
||||
"includeContent": false,
|
||||
}
|
||||
if preparedArtifact != nil && strings.TrimSpace(preparedArtifact.ArtifactScope) != "" {
|
||||
exportParams["artifactScope"] = strings.TrimSpace(preparedArtifact.ArtifactScope)
|
||||
@ -1226,10 +1309,10 @@ func (o *SessionOrchestrator) openClawArtifactCollectAndSnapshot(
|
||||
return nil
|
||||
}
|
||||
snapshotParams := map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
"openclawSessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
}
|
||||
if strings.TrimSpace(preparedArtifact.ArtifactScope) != "" {
|
||||
snapshotParams["artifactScope"] = strings.TrimSpace(preparedArtifact.ArtifactScope)
|
||||
|
||||
@ -66,7 +66,8 @@ func (s *Server) executeSessionTask(t task) (map[string]any, *shared.RPCError) {
|
||||
"threadId": shared.StringArg(response, "threadId", ""),
|
||||
"turnId": shared.StringArg(response, "turnId", ""),
|
||||
"runId": shared.StringArg(response, "runId", ""),
|
||||
"sessionKey": shared.StringArg(response, "sessionKey", ""),
|
||||
"appThreadKey": shared.StringArg(response, "appThreadKey", ""),
|
||||
"openclawSessionKey": shared.StringArg(response, "openclawSessionKey", ""),
|
||||
"artifactScope": shared.StringArg(response, "artifactScope", ""),
|
||||
"artifactDirectory": shared.StringArg(response, "artifactDirectory", ""),
|
||||
"gatewayProviderId": shared.StringArg(response, "resolvedGatewayProviderId", ""),
|
||||
@ -523,7 +524,26 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) {
|
||||
if gateway.ArtifactPrepareCount() != 1 {
|
||||
t.Fatalf("expected one OpenClaw artifact prepare request before chat.send, got %d", gateway.ArtifactPrepareCount())
|
||||
}
|
||||
prepareParams := gateway.LastArtifactPrepareParams()
|
||||
if got := shared.StringArg(prepareParams, "appThreadKey", ""); got != "thread-openclaw" {
|
||||
t.Fatalf("expected prepare appThreadKey to match app thread, got %#v", prepareParams)
|
||||
}
|
||||
if got := shared.StringArg(prepareParams, "openclawSessionKey", ""); got != "thread-openclaw" {
|
||||
t.Fatalf("expected readable OpenClaw session key, got %#v", prepareParams)
|
||||
}
|
||||
if _, ok := prepareParams["sessionKey"]; ok {
|
||||
t.Fatalf("expected prepare params to omit legacy sessionKey, got %#v", prepareParams)
|
||||
}
|
||||
if got := shared.ListArg(prepareParams, "expectedArtifactDirs"); !sameAnyStringSlice(got, []string{"assets/images/", "reports/"}) {
|
||||
t.Fatalf("expected prepare expectedArtifactDirs from app contract, got %#v", prepareParams)
|
||||
}
|
||||
chatParams := gateway.LastChatSendParams()
|
||||
if got, want := shared.StringArg(prepareParams, "requestId", ""), shared.StringArg(chatParams, "idempotencyKey", ""); got == "" || got != want {
|
||||
t.Fatalf("expected prepare requestId to match chat idempotencyKey %q, got %#v", want, prepareParams)
|
||||
}
|
||||
if got, want := shared.StringArg(prepareParams, "externalTaskId", ""), shared.StringArg(chatParams, "idempotencyKey", ""); got == "" || got != want {
|
||||
t.Fatalf("expected prepare externalTaskId to match chat idempotencyKey %q, got %#v", want, prepareParams)
|
||||
}
|
||||
for _, key := range []string{
|
||||
"artifactDirectory",
|
||||
"artifactScope",
|
||||
@ -540,7 +560,7 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) {
|
||||
}
|
||||
receipt := strings.TrimSpace(shared.StringArg(chatParams, "systemProvenanceReceipt", ""))
|
||||
openClawSessionKey := shared.StringArg(chatParams, "sessionKey", "")
|
||||
if openClawSessionKey == "" || openClawSessionKey == "thread-openclaw" {
|
||||
if openClawSessionKey == "" {
|
||||
t.Fatalf("expected mapped OpenClaw sessionKey, got %#v", chatParams)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
@ -568,14 +588,20 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) {
|
||||
t.Fatalf("expected one OpenClaw artifact export sync after run, got %d", gateway.ArtifactExportCount())
|
||||
}
|
||||
exportParams := gateway.LastArtifactExportParams()
|
||||
if _, ok := exportParams["sessionKey"]; ok {
|
||||
t.Fatalf("expected artifact export params to omit legacy sessionKey, got %#v", exportParams)
|
||||
}
|
||||
if got := shared.ListArg(exportParams, "expectedArtifactDirs"); !sameAnyStringSlice(got, []string{"assets/images/", "reports/"}) {
|
||||
t.Fatalf("expected artifact export to receive expectedArtifactDirs from contract, got %#v", exportParams)
|
||||
}
|
||||
snapshotParams := gateway.LastArtifactSnapshotParams()
|
||||
if _, ok := snapshotParams["sessionKey"]; ok {
|
||||
t.Fatalf("expected artifact snapshot params to omit legacy sessionKey, got %#v", snapshotParams)
|
||||
}
|
||||
if got := shared.ListArg(snapshotParams, "expectedArtifactDirs"); !sameAnyStringSlice(got, []string{"assets/images/", "reports/"}) {
|
||||
t.Fatalf("expected artifact snapshot to receive expectedArtifactDirs from contract, got %#v", snapshotParams)
|
||||
}
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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()
|
||||
@ -782,6 +808,50 @@ func TestExecuteSessionTaskGatewayNoDisplayableOutputFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionTaskGatewayFailsClosedWhenOpenClawAcceptsDifferentSession(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
gateway.alternateSessionKey = "dashboard:c061bfeb-ad08-45f5-971d-d9018f745d7a"
|
||||
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": "draft:1780669943199412-3",
|
||||
"threadId": "draft:1780669943199412-3",
|
||||
"taskPrompt": "say pong",
|
||||
"workingDirectory": t.TempDir(),
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "gateway",
|
||||
"preferredGatewayProviderId": "openclaw",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if rpcErr == nil {
|
||||
t.Fatalf("expected OpenClaw session mismatch rpc error, got response %#v", response)
|
||||
}
|
||||
if !strings.Contains(rpcErr.Message, "OPENCLAW_SESSION_MISMATCH") {
|
||||
t.Fatalf("expected structured session mismatch error, got %#v", rpcErr)
|
||||
}
|
||||
if gateway.AgentWaitCount() != 0 {
|
||||
t.Fatalf("session mismatch must fail before agent.wait, got %d waits", gateway.AgentWaitCount())
|
||||
}
|
||||
if gateway.ArtifactExportCount() != 0 {
|
||||
t.Fatalf("session mismatch must fail before artifact export, got %d exports", gateway.ArtifactExportCount())
|
||||
}
|
||||
chatParams := gateway.LastChatSendParams()
|
||||
if got := shared.StringArg(chatParams, "sessionKey", ""); got != "draft:1780669943199412-3" {
|
||||
t.Fatalf("expected Bridge to request the app-mapped OpenClaw session, got %#v", chatParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionTaskGatewayFailsArtifactContractAfterWaitFailure(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
@ -957,7 +1027,7 @@ 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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -1225,7 +1295,7 @@ func TestExecuteSessionTaskGatewaySurfacesOpenClawChatSendError(t *testing.T) {
|
||||
} else if rpcErr.Code != -32002 || !strings.Contains(rpcErr.Message, "openclaw chat failed") {
|
||||
t.Fatalf("expected surfaced chat.send failure, got %#v", rpcErr)
|
||||
}
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.prepare", "chat.send"}) {
|
||||
t.Fatalf("expected connect, artifact prepare, then chat.send, got %#v", got)
|
||||
}
|
||||
}
|
||||
@ -1344,7 +1414,7 @@ func TestExecuteSessionTaskGatewaySurfacesOpenClawAgentWaitError(t *testing.T) {
|
||||
if got := shared.StringArg(response, "message", ""); !strings.Contains(got, "openclaw wait failed") {
|
||||
t.Fatalf("expected surfaced agent.wait failure, got %#v", response)
|
||||
}
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.prepare", "chat.send", "agent.wait"}) {
|
||||
t.Fatalf("expected connect, artifact prepare, chat.send, then agent.wait, got %#v", got)
|
||||
}
|
||||
snapshot := server.handleTaskGet(context.Background(), map[string]any{
|
||||
@ -1474,13 +1544,13 @@ func TestExecuteSessionTaskGatewayExportsOpenClawArtifacts(t *testing.T) {
|
||||
if got := parsedDownloadURL.Path; got != openClawArtifactDownloadPath {
|
||||
t.Fatalf("expected bridge artifact download path, got %q from %q", got, downloadURL)
|
||||
}
|
||||
if got := parsedDownloadURL.Query().Get("sessionKey"); got != shared.StringArg(response, "sessionKey", "") {
|
||||
if got := parsedDownloadURL.Query().Get("sessionKey"); got != shared.StringArg(response, "openclawSessionKey", "") {
|
||||
t.Fatalf("expected mapped sessionKey in downloadUrl, got %q", got)
|
||||
}
|
||||
if got := parsedDownloadURL.Query().Get("relativePath"); got != "reports/final.md" {
|
||||
t.Fatalf("expected artifact relativePath in downloadUrl, got %q", got)
|
||||
}
|
||||
if artifactScope := parsedDownloadURL.Query().Get("artifactScope"); artifactScope != "tasks/"+shared.StringArg(response, "sessionKey", "")+"/"+response["runId"].(string) {
|
||||
if artifactScope := parsedDownloadURL.Query().Get("artifactScope"); artifactScope != "tasks/"+shared.StringArg(response, "openclawSessionKey", "")+"/"+response["runId"].(string) {
|
||||
t.Fatalf("expected prepared artifact scope in downloadUrl, got %q", artifactScope)
|
||||
}
|
||||
if parsedDownloadURL.Query().Get("sig") == "" {
|
||||
@ -1493,7 +1563,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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -1544,7 +1614,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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -1587,7 +1657,7 @@ func TestExecuteSessionTaskGatewayExportsWithActualOpenClawRunID(t *testing.T) {
|
||||
if got := strings.TrimSpace(shared.StringArg(exportParams, "runId", "")); got != "openclaw-run-actual" {
|
||||
t.Fatalf("expected artifact export to use actual OpenClaw runId, got %#v", exportParams)
|
||||
}
|
||||
if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); got != "tasks/"+shared.StringArg(response, "sessionKey", "")+"/openclaw-run-actual" {
|
||||
if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); got != "tasks/"+shared.StringArg(response, "openclawSessionKey", "")+"/openclaw-run-actual" {
|
||||
t.Fatalf("expected artifact export to use actual OpenClaw run scope, got %#v", exportParams)
|
||||
}
|
||||
artifacts, ok := response["artifacts"].([]map[string]any)
|
||||
@ -1602,10 +1672,10 @@ func TestExecuteSessionTaskGatewayExportsWithActualOpenClawRunID(t *testing.T) {
|
||||
if got := parsedDownloadURL.Query().Get("runId"); got != "openclaw-run-actual" {
|
||||
t.Fatalf("expected download URL to use actual OpenClaw runId, got %q from %q", got, downloadURL)
|
||||
}
|
||||
if got := parsedDownloadURL.Query().Get("artifactScope"); got != "tasks/"+shared.StringArg(response, "sessionKey", "")+"/openclaw-run-actual" {
|
||||
if got := parsedDownloadURL.Query().Get("artifactScope"); got != "tasks/"+shared.StringArg(response, "openclawSessionKey", "")+"/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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.prepare", "chat.send", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -1756,7 +1826,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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -1806,7 +1876,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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
@ -2586,6 +2656,7 @@ type acpFakeOpenClawGateway struct {
|
||||
artifactMode string
|
||||
artifactWorkspaceRoot string
|
||||
alternateRunID string
|
||||
alternateSessionKey string
|
||||
}
|
||||
|
||||
func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
@ -2704,6 +2775,10 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
if strings.TrimSpace(fake.alternateRunID) != "" {
|
||||
runID = strings.TrimSpace(fake.alternateRunID)
|
||||
}
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", ""))
|
||||
if strings.TrimSpace(fake.alternateSessionKey) != "" {
|
||||
sessionKey = strings.TrimSpace(fake.alternateSessionKey)
|
||||
}
|
||||
message := strings.TrimSpace(shared.StringArg(params, "message", ""))
|
||||
fake.recordRunMessage(runID, message)
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
@ -2711,16 +2786,17 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
"id": id,
|
||||
"ok": true,
|
||||
"payload": map[string]any{
|
||||
"runId": runID,
|
||||
"status": "started",
|
||||
"runId": runID,
|
||||
"sessionKey": sessionKey,
|
||||
"status": "started",
|
||||
},
|
||||
})
|
||||
case "xworkmate.artifacts.prepare":
|
||||
case "xworkmate.session.prepare":
|
||||
fake.artifactPrepareCount.Add(1)
|
||||
params := shared.AsMap(frame["params"])
|
||||
fake.lastArtifactPrepareParams.Store(params)
|
||||
runID := strings.TrimSpace(shared.StringArg(params, "runId", "fake-run"))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", "main"))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", "main"))
|
||||
artifactScope := "tasks/" + sessionKey + "/" + runID
|
||||
workspaceRoot := "/remote/openclaw/workspace"
|
||||
if strings.TrimSpace(fake.artifactWorkspaceRoot) != "" {
|
||||
@ -2733,6 +2809,10 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
"payload": map[string]any{
|
||||
"runId": runID,
|
||||
"sessionKey": sessionKey,
|
||||
"openclawSessionKey": sessionKey,
|
||||
"appThreadKey": strings.TrimSpace(shared.StringArg(params, "appThreadKey", "")),
|
||||
"mapping": map[string]any{"schemaVersion": 1, "appThreadKey": strings.TrimSpace(shared.StringArg(params, "appThreadKey", "")), "openclawSessionKey": sessionKey, "expectedArtifactDirs": shared.ListArg(params, "expectedArtifactDirs")},
|
||||
"expectedArtifactDirs": shared.ListArg(params, "expectedArtifactDirs"),
|
||||
"remoteWorkingDirectory": workspaceRoot,
|
||||
"remoteWorkspaceRefKind": "remotePath",
|
||||
"artifactScope": artifactScope,
|
||||
@ -2883,7 +2963,7 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
params := shared.AsMap(frame["params"])
|
||||
fake.lastArtifactSnapshotParams.Store(params)
|
||||
runID := strings.TrimSpace(shared.StringArg(params, "runId", "fake-run"))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", ""))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", ""))
|
||||
artifactScope := strings.TrimSpace(shared.StringArg(params, "artifactScope", ""))
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"type": "res",
|
||||
@ -2917,7 +2997,7 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
params := shared.AsMap(frame["params"])
|
||||
fake.lastArtifactExportParams.Store(params)
|
||||
runID := strings.TrimSpace(shared.StringArg(params, "runId", "fake-run"))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", ""))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", ""))
|
||||
artifactScope := strings.TrimSpace(shared.StringArg(params, "artifactScope", ""))
|
||||
payload := map[string]any{
|
||||
"runId": runID,
|
||||
@ -3062,7 +3142,7 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
"ok": true,
|
||||
"payload": map[string]any{
|
||||
"runId": strings.TrimSpace(shared.StringArg(params, "runId", "")),
|
||||
"sessionKey": strings.TrimSpace(shared.StringArg(params, "sessionKey", "")),
|
||||
"sessionKey": strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", "")),
|
||||
"remoteWorkingDirectory": "/remote/openclaw/workspace",
|
||||
"remoteWorkspaceRefKind": "remotePath",
|
||||
"artifactScope": artifactScope,
|
||||
|
||||
@ -181,9 +181,6 @@ func (s *Server) reassociateOpenClawTask(params map[string]any) *session {
|
||||
if sessionID == "" {
|
||||
sessionID = threadID
|
||||
}
|
||||
if sessionID == "" {
|
||||
sessionID = strings.TrimSpace(shared.StringArg(params, "sessionKey", ""))
|
||||
}
|
||||
if sessionID == "" {
|
||||
sessionID = "openclaw:" + runID
|
||||
}
|
||||
@ -191,7 +188,10 @@ func (s *Server) reassociateOpenClawTask(params map[string]any) *session {
|
||||
threadID = sessionID
|
||||
}
|
||||
turnID := strings.TrimSpace(shared.StringArg(params, "turnId", runID))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", threadID))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", ""))
|
||||
if sessionKey == "" {
|
||||
sessionKey = openClawAgentMainSessionKey(strings.TrimSpace(shared.StringArg(params, "appThreadKey", threadID)))
|
||||
}
|
||||
gatewayProvider := strings.TrimSpace(shared.StringArg(params, "gatewayProviderId", "openclaw"))
|
||||
now := time.Now()
|
||||
prepared := &openClawPreparedArtifactScope{
|
||||
|
||||
@ -51,8 +51,7 @@ func NewServer() *Server {
|
||||
shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""),
|
||||
shared.EnvOrDefault("BRIDGE_REVIEW_AUTH_TOKEN", ""),
|
||||
),
|
||||
openClawGate: newOpenClawGatewayAdmissionGate(config),
|
||||
openClawSessions: NewThreadSessionMapper(),
|
||||
openClawGate: newOpenClawGatewayAdmissionGate(config),
|
||||
taskRouter: newDistributedTaskRouter(distributedTaskRouterConfig{
|
||||
Config: config,
|
||||
Token: resolveDistributedTaskForwardToken(config),
|
||||
|
||||
@ -99,7 +99,6 @@ type Server struct {
|
||||
providerOrder []string
|
||||
gateway *gatewayruntime.Manager
|
||||
openClawGate *openClawGatewayAdmissionGate
|
||||
openClawSessions *ThreadSessionMapper
|
||||
jobs *jobManager
|
||||
taskRouter *distributedTaskRouter
|
||||
|
||||
|
||||
@ -40,12 +40,13 @@ func sseFirstResultEnvelope(t *testing.T, body string) map[string]any {
|
||||
func taskGetHTTPResult(t *testing.T, handler http.Handler, handle map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
body := fmt.Sprintf(
|
||||
`{"jsonrpc":"2.0","id":"task-get","method":"xworkmate.tasks.get","params":{"sessionId":%q,"threadId":%q,"turnId":%q,"runId":%q,"sessionKey":%q,"artifactScope":%q,"artifactDirectory":%q,"gatewayProviderId":%q,"runtimeBudgetMinutes":%q,"taskLoadClass":%q,"expectedArtifactExtensions":%s,"requiredArtifactExtensions":%s}}`,
|
||||
`{"jsonrpc":"2.0","id":"task-get","method":"xworkmate.tasks.get","params":{"sessionId":%q,"threadId":%q,"turnId":%q,"runId":%q,"appThreadKey":%q,"openclawSessionKey":%q,"artifactScope":%q,"artifactDirectory":%q,"gatewayProviderId":%q,"runtimeBudgetMinutes":%q,"taskLoadClass":%q,"expectedArtifactExtensions":%s,"requiredArtifactExtensions":%s}}`,
|
||||
shared.StringArg(handle, "sessionId", ""),
|
||||
shared.StringArg(handle, "threadId", ""),
|
||||
shared.StringArg(handle, "turnId", ""),
|
||||
shared.StringArg(handle, "runId", ""),
|
||||
shared.StringArg(handle, "sessionKey", ""),
|
||||
shared.StringArg(handle, "appThreadKey", ""),
|
||||
shared.StringArg(handle, "openclawSessionKey", ""),
|
||||
shared.StringArg(handle, "artifactScope", ""),
|
||||
shared.StringArg(handle, "artifactDirectory", ""),
|
||||
shared.StringArg(handle, "resolvedGatewayProviderId", "openclaw"),
|
||||
@ -719,7 +720,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.export", "xworkmate.artifacts.collect-and-snapshot"}) {
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,7 +310,7 @@ func TestSessionEmitsNormalizedChatRunPushEvents(t *testing.T) {
|
||||
map[string]any{"seq": 7},
|
||||
map[string]any{
|
||||
"runId": "run-1",
|
||||
"sessionKey": "agent:main:main",
|
||||
"sessionKey": "main",
|
||||
"state": "final",
|
||||
"message": map[string]any{
|
||||
"role": "assistant",
|
||||
|
||||
@ -34,6 +34,7 @@ request_body="$(cat <<JSON
|
||||
"params": {
|
||||
"sessionId": "${session_id}",
|
||||
"threadId": "${session_id}",
|
||||
"appThreadKey": "${session_id}",
|
||||
"taskPrompt": "Reply exactly pong.",
|
||||
"workingDirectory": "/tmp",
|
||||
"routing": {
|
||||
@ -128,6 +129,25 @@ def output_text_from(payload):
|
||||
return " ".join(part for part in candidates if part)
|
||||
|
||||
|
||||
def require_nonempty(payload, key):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return
|
||||
raise SystemExit(f"OpenClaw smoke result missing {key}: {json.dumps(payload, ensure_ascii=False, sort_keys=True)[:1000]}")
|
||||
|
||||
|
||||
def is_valid_no_displayable_contract(payload):
|
||||
if not isinstance(payload, dict):
|
||||
return False
|
||||
if payload.get("code") != "OPENCLAW_NO_DISPLAYABLE_OUTPUT":
|
||||
return False
|
||||
if payload.get("resolvedGatewayProviderId") != "openclaw":
|
||||
return False
|
||||
for key in ("sessionId", "threadId", "runId", "openclawSessionKey", "artifactScope"):
|
||||
require_nonempty(payload, key)
|
||||
return True
|
||||
|
||||
|
||||
final = next(
|
||||
(item for item in payloads if isinstance(item, dict) and item.get("id") == "validate-openclaw"),
|
||||
None,
|
||||
@ -203,6 +223,9 @@ for marker in (
|
||||
|
||||
output_text = output_text_from(result)
|
||||
if "pong" not in output_text.lower():
|
||||
if is_valid_no_displayable_contract(result):
|
||||
print("OpenClaw smoke OK: session contract completed without displayable output")
|
||||
sys.exit(0)
|
||||
result_preview = json.dumps(result, ensure_ascii=False, sort_keys=True)[:1000]
|
||||
raise SystemExit(f"OpenClaw smoke did not return pong: {output_text[:500]}\nresult preview: {result_preview}")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user