From 0c1004bbb36cafe02f2f6a28c049fcce723a17f8 Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 7 Oct 2026 23:28:20 +0900 Subject: [PATCH 1/2] Return the compile result without warming execute-dynamic-code first A successful compile used to send a readiness probe (get-version, then execute-dynamic-code) before the command returned. It only sped up a following execute-dynamic-code, took several seconds on large projects, and held the Editor's single-flight slot, delaying a hot-reload sent meanwhile. The Editor keeps its own first-run warm-up, so dropping the CLI one only moves the cold start to the next execute-dynamic-code. - Tests that cancelled at a successful answer to dodge the 180 s probe now run to completion, one with an under-30s time limit - New tests cover a successful stored result and a successful attached wait, the two attach paths that had no success case --- .../internal/projectrunner/compile_attach.go | 21 ++--- .../projectrunner/compile_attach_test.go | 91 +++++++++++++++++-- .../compile_current_sources_test.go | 3 +- .../compile_fresh_recovery_test.go | 2 +- .../compile_status_response_contract_test.go | 10 +- .../projectrunner/compile_wait_test.go | 31 +------ .../pause_point_release_recovery_test.go | 25 ++--- .../internal/projectrunner/run.go | 33 +------ .../internal/projectrunner/run_test.go | 7 -- 9 files changed, 106 insertions(+), 117 deletions(-) diff --git a/cli/project-runner/internal/projectrunner/compile_attach.go b/cli/project-runner/internal/projectrunner/compile_attach.go index 3b892f2bca..54fef7deaa 100644 --- a/cli/project-runner/internal/projectrunner/compile_attach.go +++ b/cli/project-runner/internal/projectrunner/compile_attach.go @@ -90,7 +90,7 @@ func tryAttachToPendingCompile( clearCompilePendingRecord(connection.ProjectRoot) return false, compileExecutionResult{} } - return true, returnAttachedStoredCompileResult(ctx, connection, record, status.Result, stderr) + return true, returnAttachedStoredCompileResult(connection, record, status.Result, stderr) } if !status.Ready { @@ -219,7 +219,7 @@ func attachWaitForPendingCompile( spinner.Stop() return false, compileExecutionResult{} } - return true, completeCompileResult(ctx, connection, result, stderr, spinner, startedAt, unityipc.UnitySendOutcome{}) + return true, completeCompileResult(result, stderr, spinner, startedAt, unityipc.UnitySendOutcome{}) default: spinner.Stop() logCompileAttachResult(connection, record.RequestID, "error", false) @@ -289,7 +289,6 @@ func waitForAttachedCompileCompletion( } func returnAttachedStoredCompileResult( - ctx context.Context, connection unityipc.Connection, record compilePendingRecord, result json.RawMessage, @@ -300,26 +299,20 @@ func returnAttachedStoredCompileResult( spinner := clicore.NewToolSpinner(stderr, clicore.CompileCommandName) clearCompilePendingRecord(connection.ProjectRoot) logCompileAttachResult(connection, record.RequestID, "stored_result", true) - return completeCompileResult(ctx, connection, result, stderr, spinner, startedAt, unityipc.UnitySendOutcome{}) + return completeCompileResult(result, stderr, spinner, startedAt, unityipc.UnitySendOutcome{}) } +// completeCompileResult turns the compile answer into the command's result. +// Why nothing is sent after the answer: the answer came from the Editor that finished the compile, +// and a readiness probe here would hold the Editor's single-flight slot for seconds that only the +// next execute-dynamic-code would have gained. func completeCompileResult( - ctx context.Context, - connection unityipc.Connection, result json.RawMessage, stderr io.Writer, spinner *ui.TerminalSpinner, startedAt time.Time, outcome unityipc.UnitySendOutcome, ) compileExecutionResult { - switch compileResultReadinessWaitMode(result) { - case compileReadinessWaitWarmup: - spinner.Update("Warming execute-dynamic-code after compile...") - if err := clicore.WaitForToolReadiness(ctx, connection.ProjectRoot); err != nil { - spinner.Stop() - writePostCompileWarmupWarning(stderr, err) - } - } spinner.Stop() writeDebugTiming(stderr, clicore.CompileCommandName, time.Since(startedAt), outcome) return compileExecutionResult{result: result, exitCode: toolEnvelopeExitCode(result)} diff --git a/cli/project-runner/internal/projectrunner/compile_attach_test.go b/cli/project-runner/internal/projectrunner/compile_attach_test.go index 9ba0ee42b5..121e615c8a 100644 --- a/cli/project-runner/internal/projectrunner/compile_attach_test.go +++ b/cli/project-runner/internal/projectrunner/compile_attach_test.go @@ -242,6 +242,83 @@ func TestRunCompileAttachReturnsStoredResultAndClearsRecord(t *testing.T) { } } +// Verifies a successful stored result is returned as soon as it is read, with exit code 0 and +// without a post-compile warm-up. +func TestRunCompileAttachReturnsAStoredSuccessAtOnce(t *testing.T) { + projectRoot := t.TempDir() + if err := writeCompilePendingRecord(projectRoot, compilePendingRecord{ + RequestID: "compile_attach_stored_success", + TimedOutAtUtc: time.Now().UTC(), + }); err != nil { + t.Fatalf("write pending record failed: %v", err) + } + deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { + return compileStatusResponse{ + Ready: true, + HasResult: true, + Result: json.RawMessage(`{"Success":true,"ErrorCount":0}`), + }, nil + }) + + assertAttachedSuccessReturnsAtOnce(t, projectRoot, deps) +} + +// Verifies a successful result that an attached wait sees once the in-flight compile finishes is +// returned as soon as it arrives, with exit code 0 and without a post-compile warm-up. +func TestRunCompileAttachReturnsAWaitedSuccessAtOnce(t *testing.T) { + projectRoot := t.TempDir() + if err := writeCompilePendingRecord(projectRoot, compilePendingRecord{ + RequestID: "compile_attach_waited_success", + TimedOutAtUtc: time.Now().UTC().Add(-time.Minute), + }); err != nil { + t.Fatalf("write pending record failed: %v", err) + } + callCount := 0 + deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { + callCount++ + if callCount == 1 { + return compileStatusResponse{Ready: false, IsCompiling: true}, nil + } + return compileStatusResponse{ + Ready: true, + HasResult: true, + Result: json.RawMessage(`{"Success":true,"ErrorCount":0}`), + }, nil + }) + + assertAttachedSuccessReturnsAtOnce(t, projectRoot, deps) +} + +// assertAttachedSuccessReturnsAtOnce runs compile against a pending record whose result is a +// success and checks the result is written to stdout with exit code 0 well within the time a +// readiness probe would take. +func assertAttachedSuccessReturnsAtOnce(t *testing.T, projectRoot string, deps compileWaitDeps) { + t.Helper() + connection := unityipc.Connection{ + Endpoint: unityipc.Endpoint{Network: "tcp", Address: "127.0.0.1:1"}, + ProjectRoot: projectRoot, + } + var stdout, stderr bytes.Buffer + startedAt := time.Now() + + code := runCompileWithDomainReloadWaitWithDeps(context.Background(), connection, map[string]any{}, &stdout, &stderr, deps) + + // Why a time limit: the post-compile warm-up this command no longer runs would wait 180 s for a + // readiness a temp project never reaches. + if elapsed := time.Since(startedAt); elapsed >= 30*time.Second { + t.Fatalf("compile took %s after a successful answer, want under 30s", elapsed) + } + if code != 0 { + t.Fatalf("exit code = %d, want 0\nstderr:\n%s", code, stderr.String()) + } + if !strings.Contains(stdout.String(), `"Success": true`) && !strings.Contains(stdout.String(), `"Success":true`) { + t.Fatalf("the successful result missing from stdout: %s", stdout.String()) + } + if strings.Contains(stderr.String(), "warning") { + t.Fatalf("a successful compile must not warn:\n%s", stderr.String()) + } +} + // Verifies ForceRecompile clears the pending record and starts a new compile. func TestRunCompileAttachForceRecompileClearsRecordAndStartsNewCompile(t *testing.T) { if runtime.GOOS == "windows" { @@ -717,17 +794,13 @@ func TestRunCompileAttachReportsCancellationDuringWait(t *testing.T) { } } -// Verifies a successful compile whose post-compile warmup fails still returns the compile -// result and only warns about the skipped warmup. -func TestCompleteCompileResultWarnsWhenWarmupFails(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() +// Verifies a successful compile answer becomes the command's result as it is, with exit code 0 +// and no warning. +func TestCompleteCompileResultReturnsTheAnswerAsIs(t *testing.T) { var stderr bytes.Buffer result := json.RawMessage(`{"Success":true,"ErrorCount":0}`) execution := completeCompileResult( - ctx, - unityipc.Connection{ProjectRoot: t.TempDir()}, result, &stderr, clicore.NewToolSpinner(&stderr, clicore.CompileCommandName), @@ -738,8 +811,8 @@ func TestCompleteCompileResultWarnsWhenWarmupFails(t *testing.T) { if execution.exitCode != 0 || string(execution.result) != string(result) { t.Fatalf("unexpected execution: %#v", execution) } - if !strings.Contains(stderr.String(), "warning: post-compile warmup skipped: context canceled") { - t.Fatalf("stderr must warn about the skipped warmup:\n%s", stderr.String()) + if strings.Contains(stderr.String(), "warning") { + t.Fatalf("a successful compile must not warn:\n%s", stderr.String()) } } diff --git a/cli/project-runner/internal/projectrunner/compile_current_sources_test.go b/cli/project-runner/internal/projectrunner/compile_current_sources_test.go index a252cac8f8..de78946224 100644 --- a/cli/project-runner/internal/projectrunner/compile_current_sources_test.go +++ b/cli/project-runner/internal/projectrunner/compile_current_sources_test.go @@ -14,8 +14,7 @@ import ( "github.com/hatayama/unity-cli-loop/common/unityipc" ) -// Why Success:false in both: a successful result triggers the post-compile warmup, which waits for -// a tool readiness that a temp project never reaches. The error counts tell the two results apart. +// The error counts tell the two results apart. const ( earlierCompileResult = `{"Success":false,"ErrorCount":9}` currentCompileResult = `{"Success":false,"ErrorCount":1}` diff --git a/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go b/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go index 97f231395d..806c6d3e29 100644 --- a/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go +++ b/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go @@ -19,7 +19,7 @@ import ( const ( // Stops a wait that never ends instead of letting it poll through the whole default wait. compileRecoveryQueryLimit = 200 - // A compile error: definitive, and it does not start the post-compile warmup. + // A compile error: definitive. compileRecoveryDefinitiveResult = `{"Success":false,"ErrorCount":1,"WarningCount":0,"ErrorCode":null}` compileRecoveryAlreadyInProgressResult = `{"Success":false,"ErrorCode":"COMPILE_ALREADY_IN_PROGRESS","ErrorCount":1}` ) diff --git a/cli/project-runner/internal/projectrunner/compile_status_response_contract_test.go b/cli/project-runner/internal/projectrunner/compile_status_response_contract_test.go index 71d3d906a7..cbdb66bcc5 100644 --- a/cli/project-runner/internal/projectrunner/compile_status_response_contract_test.go +++ b/cli/project-runner/internal/projectrunner/compile_status_response_contract_test.go @@ -10,7 +10,7 @@ import ( const compileStatusResponseContractPath = "tests/contracts/compile_status_response_contract.json" // Verifies the Go compileStatusResponse DTO preserves every field in the shared Unity -// get-compile-status response contract, including nested Result.Success used by compileResultStatus. +// get-compile-status response contract, including a nested Result whose Success the CLI reads as the exit code. func TestCompileStatusResponseMatchesSharedContract(t *testing.T) { fixturePath := findRepoRelativeFile(t, compileStatusResponseContractPath) fixture, err := os.ReadFile(fixturePath) @@ -25,12 +25,8 @@ func TestCompileStatusResponseMatchesSharedContract(t *testing.T) { assertCompileStatusResponseFieldsPopulated(t, response) - var resultStatus compileResultStatus - if err := json.Unmarshal(response.Result, &resultStatus); err != nil { - t.Fatalf("failed to unmarshal Result into compileResultStatus: %v", err) - } - if resultStatus.Success == nil { - t.Fatal("compileResultStatus.Success must be non-nil after unmarshaling the shared contract") + if toolEnvelopeExitCode(response.Result) != 0 { + t.Fatalf("the shared contract's Result must read as a successful compile: %s", response.Result) } roundTripped, err := json.Marshal(response) diff --git a/cli/project-runner/internal/projectrunner/compile_wait_test.go b/cli/project-runner/internal/projectrunner/compile_wait_test.go index f45fe4406d..0adb3132f3 100644 --- a/cli/project-runner/internal/projectrunner/compile_wait_test.go +++ b/cli/project-runner/internal/projectrunner/compile_wait_test.go @@ -564,8 +564,7 @@ func TestRunCompileWithDomainReloadWaitWarnsWhenTimeoutExceedsRetention(t *testi } endpoint, serverErr := startCompileAcceptOnceServer(t) - // Why Success:false: Success:true triggers post-compile warmup against a live Editor - // and would hang this unit test. The warning is emitted before send/wait. + // The warning is emitted before send/wait. deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { return compileStatusResponse{ Ready: true, @@ -669,34 +668,6 @@ func TestShouldWaitForCompileStatusAllowsAcceptedFinalResponseTimeout(t *testing } } -// Verifies compile readiness warmup only runs after confirmed successful results. -func TestCompileResultReadinessWaitMode(t *testing.T) { - cases := map[string]compileReadinessWaitMode{ - `{"Success":true}`: compileReadinessWaitWarmup, - `{"Success":false,"Errors":[{"Message":"boom"}]}`: compileReadinessWaitNone, - `{"Success":false,"Message":"indeterminate"}`: compileReadinessWaitNone, - `{"Message":"indeterminate"}`: compileReadinessWaitNone, - } - - for result, expected := range cases { - actual := compileResultReadinessWaitMode([]byte(result)) - if actual != expected { - t.Fatalf("readiness wait mode mismatch for %s: %v", result, actual) - } - } -} - -// Verifies that failed best-effort warmup reports a warning without taking over compile output. -func TestWritePostCompileWarmupWarningReportsNonFatalFailure(t *testing.T) { - var stderr bytes.Buffer - - writePostCompileWarmupWarning(&stderr, fmt.Errorf("probe failed")) - - if !strings.Contains(stderr.String(), "warning: post-compile warmup skipped: probe failed") { - t.Fatalf("warning mismatch: %s", stderr.String()) - } -} - // Verifies TimeoutSeconds is parsed from tool params with the default // kept when absent and non-positive or non-integer values rejected. func TestCompileWaitTimeoutFromParams(t *testing.T) { diff --git a/cli/project-runner/internal/projectrunner/pause_point_release_recovery_test.go b/cli/project-runner/internal/projectrunner/pause_point_release_recovery_test.go index 42fad59be8..1a47cfd35a 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_release_recovery_test.go +++ b/cli/project-runner/internal/projectrunner/pause_point_release_recovery_test.go @@ -732,8 +732,7 @@ func TestSendCompileWithBusyRetryStopsRetrying(t *testing.T) { }) } -// A successful compile result. It starts the post-compile warmup, which waits for a Unity project -// these tests do not have, so a test that returns it cancels the command at that answer. +// A successful compile result. const pausePointRecoveryCompileSuccess = `{"Success":true}` // stubPausePointRecoveryBusyRetryWaits makes the wait between server_busy sends return at once and @@ -767,17 +766,20 @@ func runPausePointRecoveryCompile( // Verifies the recovery compile sends a request Unity lost again under a new request ID instead of // waiting out the whole timeout, and that the successful compile leaves stdout empty. func TestPausePointRecoveryCompileResendsWhenUnityLostTheRequest(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() scenario := newCompileRecoveryScenario(t, []compileRecoverySend{recoverySendDisconnected(), recoverySendAnswered()}, []compileRecoveryAnswer{recoveryMissing()}, []compileRecoveryAnswer{recoveryDone(pausePointRecoveryCompileSuccess)}, ) - scenario.cancelWhen(cancel, 1, 1) + startedAt := time.Now() - code, stdout, stderr := runPausePointRecoveryCompile(t, ctx, map[string]any{}, scenario.deps()) + code, stdout, stderr := runPausePointRecoveryCompile(t, context.Background(), map[string]any{}, scenario.deps()) + // Why a time limit: the post-compile warm-up this command no longer runs would wait 180 s for a + // readiness a temp project never reaches. + if elapsed := time.Since(startedAt); elapsed >= 30*time.Second { + t.Fatalf("the recovery compile took %s after a successful answer, want under 30s", elapsed) + } if code != 0 { t.Fatalf("exit code = %d, want 0\nstderr:\n%s", code, stderr) } @@ -804,16 +806,13 @@ func TestPausePointRecoveryCompileResendsAfterABusyRejection(t *testing.T) { for _, errorCode := range []string{"COMPILE_ALREADY_IN_PROGRESS", "COMPILE_EDITOR_UPDATING"} { t.Run(errorCode, func(t *testing.T) { waits := stubPausePointRecoveryBusyRetryWaits(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() scenario := newCompileRecoveryScenario(t, []compileRecoverySend{recoverySendAnswered(), recoverySendAnswered()}, compileRecoveryBusyRejectionAnswers(errorCode), []compileRecoveryAnswer{recoveryDone(pausePointRecoveryCompileSuccess)}, ) - scenario.cancelWhen(cancel, 1, 1) - code, stdout, stderr := runPausePointRecoveryCompile(t, ctx, map[string]any{}, scenario.deps()) + code, stdout, stderr := runPausePointRecoveryCompile(t, context.Background(), map[string]any{}, scenario.deps()) if code != 0 { t.Fatalf("exit code = %d, want 0\nstderr:\n%s", code, stderr) @@ -879,14 +878,10 @@ func TestPausePointRecoveryCompileWritesADefinitiveFailureOnce(t *testing.T) { // again after one wait that the time left in the compile wait caps below the retry interval. func TestPausePointRecoveryCompileRetriesAServerBusySend(t *testing.T) { waits := stubPausePointRecoveryBusyRetryWaits(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() scenario := newCompileRecoveryScenario(t, []compileRecoverySend{recoverySendAnswered()}, []compileRecoveryAnswer{recoveryDone(pausePointRecoveryCompileSuccess)}, ) - // The refused send never reaches the scenario, so the scenario's first send is the retry. - scenario.cancelWhen(cancel, 0, 1) deps := scenario.deps() scriptedSend := deps.sendCompile busy := serverBusyRPCError(t) @@ -908,7 +903,7 @@ func TestPausePointRecoveryCompileRetriesAServerBusySend(t *testing.T) { // Why a 1s wait: it is shorter than the retry interval, so only a budget taken from the time left // in the compile wait keeps the retry wait at or under 1s. - code, stdout, stderr := runPausePointRecoveryCompile(t, ctx, map[string]any{compileWaitTimeoutParam: 1}, deps) + code, stdout, stderr := runPausePointRecoveryCompile(t, context.Background(), map[string]any{compileWaitTimeoutParam: 1}, deps) if code != 0 { t.Fatalf("exit code = %d, want 0\nstderr:\n%s", code, stderr) diff --git a/cli/project-runner/internal/projectrunner/run.go b/cli/project-runner/internal/projectrunner/run.go index 9b6abf1d32..7e2f98517d 100644 --- a/cli/project-runner/internal/projectrunner/run.go +++ b/cli/project-runner/internal/projectrunner/run.go @@ -354,13 +354,7 @@ func runFreshCompileAttempt( spinner.Stop() return compileExecutionResult{}, freshCompileAttemptEditorBusy } - return completeCompileResult(ctx, connection, result, stderr, spinner, startedAt, outcome), freshCompileAttemptFinal -} - -func writePostCompileWarmupWarning(stderr io.Writer, err error) { - // Why: this warmup is a hidden optimization, so it must not turn a - // successful compile result into a user-visible command failure. - _, _ = fmt.Fprintf(stderr, "warning: post-compile warmup skipped: %v\n", err) + return completeCompileResult(result, stderr, spinner, startedAt, outcome), freshCompileAttemptFinal } func runList(ctx context.Context, connection unityipc.Connection, commandArgs []string, stdout io.Writer, stderr io.Writer) int { @@ -433,28 +427,3 @@ func runSync(ctx context.Context, connection unityipc.Connection, stdout io.Writ clicore.WriteFormat(stdout, "Tools synced to %s\n", cachePath) return 0 } - -type compileResultStatus struct { - Success *bool `json:"Success"` -} - -type compileReadinessWaitMode int - -const ( - compileReadinessWaitNone compileReadinessWaitMode = iota - compileReadinessWaitWarmup -) - -func compileResultReadinessWaitMode(result json.RawMessage) compileReadinessWaitMode { - var status compileResultStatus - if json.Unmarshal(result, &status) != nil { - return compileReadinessWaitNone - } - if status.Success == nil { - return compileReadinessWaitNone - } - if *status.Success { - return compileReadinessWaitWarmup - } - return compileReadinessWaitNone -} diff --git a/cli/project-runner/internal/projectrunner/run_test.go b/cli/project-runner/internal/projectrunner/run_test.go index 6a3a74b48b..69f9e08b16 100644 --- a/cli/project-runner/internal/projectrunner/run_test.go +++ b/cli/project-runner/internal/projectrunner/run_test.go @@ -341,10 +341,3 @@ func TestRunListPrintsLiveCatalog(t *testing.T) { t.Fatalf("stdout must list the live tool:\n%s", stdout.String()) } } - -// Verifies that an undecodable compile result never triggers the post-compile readiness wait. -func TestCompileResultReadinessWaitModeIgnoresMalformedResult(t *testing.T) { - if mode := compileResultReadinessWaitMode([]byte("not json")); mode != compileReadinessWaitNone { - t.Fatalf("mode = %v, want compileReadinessWaitNone", mode) - } -} From a15b4e3c4f643e22e26b540bbb6375e2c172015d Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 7 Oct 2026 23:36:24 +0900 Subject: [PATCH 2/2] Point the C# contract test comments at the CLI's exit-code read The Go type they named was removed with the post-compile warm-up; the CLI now reads Result.Success only as the compile's exit code. --- .../Tests/Editor/GetCompileStatusResponseContractTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs b/Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs index 12ad062480..e87dfee945 100644 --- a/Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs +++ b/Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs @@ -22,7 +22,8 @@ public void GetCompileStatusResponse_WhenSerialized_MatchesSharedContractFieldSh { // Verifies C# does not add, remove, or rename get-compile-status fields without updating the // shared CLI contract. Result must come from a real CompileResponse serialization because that - // is what CompileStatusBridgeCommand restores from ResultJson for Go compileResultStatus.Success. + // is what CompileStatusBridgeCommand restores from ResultJson for the Go CLI, which reads + // Result.Success as the compile's exit code. JObject expected = ReadSharedContractFieldShape(); JObject compileResultJson = SerializeCompileResponseResult(); GetCompileStatusResponse response = new() @@ -47,8 +48,8 @@ public void GetCompileStatusResponse_WhenSerialized_MatchesSharedContractFieldSh [Test] public void CompileResponse_WhenSerializedForCompileStatusResult_IncludesSuccessProperty() { - // Verifies the wire Result payload still exposes Success under the name Go unmarshals into - // compileResultStatus — a rename on CompileResponse must fail this test. + // Verifies the wire Result payload still exposes Success under the name the Go CLI reads for + // the exit code (toolEnvelopeExitCode) — a rename on CompileResponse must fail this test. JObject compileResultJson = SerializeCompileResponseResult(); Assert.That(compileResultJson.Property("Success"), Is.Not.Null); Assert.That(compileResultJson["Success"]!.Type, Is.EqualTo(JTokenType.Boolean));