Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 0 additions & 79 deletions cli/common/errors/busy_status.go
Original file line number Diff line number Diff line change
@@ -1,80 +1,9 @@
package clierrors

import (
"encoding/json"
"fmt"
"io"
)

const cliStatusBusy = "Busy"

type cliStatusEnvelope struct {
Status string `json:"Status"`
Message string `json:"Message"`
RunningToolName string `json:"RunningToolName,omitempty"`
RequestedToolName string `json:"RequestedToolName,omitempty"`
IsPlaying *bool `json:"IsPlaying,omitempty"`
IsPaused *bool `json:"IsPaused,omitempty"`
}

type serverBusyStatusDetails struct {
runningToolName string
requestedToolName string
isPlaying bool
hasIsPlaying bool
isPaused bool
hasIsPaused bool
}

func writeBusyStatusEnvelope(writer io.Writer, message string, details serverBusyStatusDetails) {
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
_ = encoder.Encode(cliStatusEnvelope{
Status: cliStatusBusy,
Message: message,
RunningToolName: details.runningToolName,
RequestedToolName: details.requestedToolName,
IsPlaying: optionalBool(details.hasIsPlaying, details.isPlaying),
IsPaused: optionalBool(details.hasIsPaused, details.isPaused),
})
}

func serverBusyStatusDetailsFromError(err CLIError) serverBusyStatusDetails {
data, ok := serverBusyDetailsData(err.Details)
if !ok {
return serverBusyStatusDetails{
requestedToolName: err.Command,
}
}

isPlaying, hasIsPlaying := rpcBoolData(data, "isPlaying")
isPaused, hasIsPaused := rpcBoolData(data, "isPaused")
return serverBusyStatusDetails{
runningToolName: rpcStringData(data, "runningToolName"),
requestedToolName: firstNonEmpty(rpcStringData(data, "requestedToolName"), err.Command),
isPlaying: isPlaying,
hasIsPlaying: hasIsPlaying,
isPaused: isPaused,
hasIsPaused: hasIsPaused,
}
}

func serverBusyDetailsData(details map[string]any) (map[string]any, bool) {
data, ok := details["Data"].(map[string]any)
if ok {
return data, true
}
data, ok = details["data"].(map[string]any)
return data, ok
}

func optionalBool(hasValue bool, value bool) *bool {
if !hasValue {
return nil
}
return &value
}

func unityServerBusyMessage(fallback string, data map[string]any, requestedCommand string) string {
runningToolName := rpcStringData(data, "runningToolName")
requestedToolName := firstNonEmpty(rpcStringData(data, "requestedToolName"), requestedCommand)
Expand All @@ -97,11 +26,3 @@ func rpcStringData(data map[string]any, key string) string {
}
return value
}

func rpcBoolData(data map[string]any, key string) (bool, bool) {
value, ok := data[key].(bool)
if !ok {
return false, false
}
return value, true
}
4 changes: 0 additions & 4 deletions cli/common/errors/error_envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ type ErrorContext struct {
}

func WriteErrorEnvelope(writer io.Writer, err CLIError) {
if err.ErrorCode == errorCodeUnityServerBusy {
writeBusyStatusEnvelope(writer, err.Message, serverBusyStatusDetailsFromError(err))
return
}
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
_ = encoder.Encode(CLIErrorEnvelope{
Expand Down
48 changes: 23 additions & 25 deletions cli/common/errors/error_envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ func TestClassifyServerBusyRPCError(t *testing.T) {
}
}

func TestWriteClassifiedServerBusyRPCErrorWritesBusyStatus(t *testing.T) {
// Verifies server_busy output avoids the full error envelope because busy is a temporary state.
func TestWriteClassifiedServerBusyRPCErrorWritesErrorEnvelope(t *testing.T) {
// Verifies server_busy output uses the same machine-readable error envelope as other failures.
err := &unityipc.RPCError{
Code: -32603,
Message: "Unity is busy running 'compile'. Retry 'get-logs' after the running tool completes.",
Expand All @@ -279,33 +279,31 @@ func TestWriteClassifiedServerBusyRPCErrorWritesBusyStatus(t *testing.T) {

WriteClassifiedError(&stderr, err, ErrorContext{ProjectRoot: "/tmp/MyProject", Command: "get-logs"})

var envelope cliStatusEnvelope
var envelope CLIErrorEnvelope
if err := json.Unmarshal(stderr.Bytes(), &envelope); err != nil {
t.Fatalf("stderr is not valid JSON: %v\n%s", err, stderr.String())
}
if envelope.Status != cliStatusBusy {
t.Fatalf("status mismatch: %#v", envelope)
if envelope.Success {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
t.Fatalf("busy envelope reported success: %#v", envelope)
}
if envelope.Error.ErrorCode != errorCodeUnityServerBusy {
t.Fatalf("error code mismatch: %#v", envelope)
}
expectedMessage := "'get-logs' was not executed because Unity is busy running 'compile'. uloop is single-flight by design; never run uloop commands in parallel. The CLI already retried for up to 10 seconds, so wait for 'compile' to complete and run the command again."
if envelope.Message != expectedMessage {
if envelope.Error.Message != expectedMessage {
t.Fatalf("message mismatch: %#v", envelope)
}
if envelope.RunningToolName != "compile" || envelope.RequestedToolName != "get-logs" {
t.Fatalf("tool names mismatch: %#v", envelope)
}
if envelope.IsPlaying == nil || *envelope.IsPlaying != true {
t.Fatalf("isPlaying mismatch: %#v", envelope)
}
if envelope.IsPaused == nil || *envelope.IsPaused != true {
t.Fatalf("isPaused mismatch: %#v", envelope)
data, ok := envelope.Error.Details["Data"].(map[string]any)
if !ok {
t.Fatalf("busy data missing: %#v", envelope)
}
if bytes.Contains(stderr.Bytes(), []byte("Success")) || bytes.Contains(stderr.Bytes(), []byte("errorCode")) {
t.Fatalf("busy output should not include error envelope fields: %s", stderr.String())
if data["runningToolName"] != "compile" || data["requestedToolName"] != "get-logs" {
t.Fatalf("tool names mismatch: %#v", data)
}
}

func TestWriteErrorEnvelopeServerBusyAcceptsLegacyLowercaseDataDetails(t *testing.T) {
// Verifies legacy lower-camel error details still preserve busy tool names in status output.
func TestWriteErrorEnvelopeServerBusyUsesUnifiedEnvelopeForLegacyLowercaseDataDetails(t *testing.T) {
// Verifies legacy lower-camel busy details no longer switch to the old status schema.
cliErr := CLIError{
ErrorCode: errorCodeUnityServerBusy,
Message: "Unity is busy.",
Expand All @@ -323,18 +321,18 @@ func TestWriteErrorEnvelopeServerBusyAcceptsLegacyLowercaseDataDetails(t *testin

WriteErrorEnvelope(&stderr, cliErr)

var envelope cliStatusEnvelope
var envelope CLIErrorEnvelope
if err := json.Unmarshal(stderr.Bytes(), &envelope); err != nil {
t.Fatalf("stderr is not valid JSON: %v\n%s", err, stderr.String())
}
if envelope.RunningToolName != "compile" || envelope.RequestedToolName != "get-logs" {
t.Fatalf("tool names mismatch: %#v", envelope)
if envelope.Success {
t.Fatalf("busy envelope reported success: %#v", envelope)
}
if envelope.IsPlaying == nil || *envelope.IsPlaying != true {
t.Fatalf("isPlaying mismatch: %#v", envelope)
if envelope.Error.ErrorCode != errorCodeUnityServerBusy {
t.Fatalf("error code mismatch: %#v", envelope)
}
if envelope.IsPaused == nil || *envelope.IsPaused != false {
t.Fatalf("isPaused mismatch: %#v", envelope)
if bytes.Contains(stderr.Bytes(), []byte(`"Status"`)) {
t.Fatalf("busy output should not use status schema: %s", stderr.String())
}
}

Expand Down
2 changes: 1 addition & 1 deletion cli/dispatcher/shared-inputs-stamp.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schemaVersion": 1,
"sharedInputsHash": "231c199b4cdb5343ecda216098befab6bf29737e"
"sharedInputsHash": "058dfc6153eecc5bd339def577253f792fa486de"
}
2 changes: 1 addition & 1 deletion cli/project-runner/shared-inputs-stamp.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schemaVersion": 1,
"sharedInputsHash": "6c9ceaf4baea6b1ed24fcd2a493f7d977da58e87"
"sharedInputsHash": "8a15e5515c0c9df68396cc71e34c4df10d3f8546"
}
Loading