Merge pull request #5 from x-evor/fix/validate-openclaw-smoke-test

Fix/validate openclaw smoke test
This commit is contained in:
Haitao Pan 2026-06-06 06:28:24 +08:00 committed by GitHub
commit 77cd9551fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 247 additions and 95 deletions

View File

@ -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,

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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{

View File

@ -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),

View File

@ -99,7 +99,6 @@ type Server struct {
providerOrder []string
gateway *gatewayruntime.Manager
openClawGate *openClawGatewayAdmissionGate
openClawSessions *ThreadSessionMapper
jobs *jobManager
taskRouter *distributedTaskRouter

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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}")