From 32e6a95e1f56373de04ff7b9b1a9b5ea199f8b25 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 21 Jul 2026 12:00:30 +0900 Subject: [PATCH] Extend pause-point timeout hint with non-firing pattern diagnostics The HitCount=0 timeout hint only covered inlining and fast-progressing scenarios, so agents had no diagnosis for two confirmed non-firing patterns: methods called from (not named as) a physics/message callback, and methods already bound into a delegate/event before enable-pause-point, whose pre-bound invocation path bypasses the Harmony patch. Both were validated against real round5/6 dogfooding sessions. - Append the two non-firing patterns to pausePointTimeoutHint's existing HitCount=0 branch (additive, same Hint field) - Add a regression test for enable-pause-point --await's composite wait path (runPausePointWaitAfterEnable) to confirm it shares the same hint via pausePointWaitError, preventing the mirror-miss regression called out after Round4 --- .../projectrunner/pause_point_enable_test.go | 66 +++++++++++++++++++ .../projectrunner/pause_point_errors.go | 3 +- .../projectrunner/pause_point_wait_test.go | 3 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/cli/project-runner/internal/projectrunner/pause_point_enable_test.go b/cli/project-runner/internal/projectrunner/pause_point_enable_test.go index 395a2938d5..fd31b27b89 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_enable_test.go +++ b/cli/project-runner/internal/projectrunner/pause_point_enable_test.go @@ -352,6 +352,72 @@ func TestRunEnablePausePointCommandDoesNotAwaitAfterFailedEnable(t *testing.T) { } } +// Verifies enable-pause-point --await's composite wait path mirrors await-pause-point's +// non-firing-pattern diagnosis hint on a HitCount=0 timeout (Round4 regression: a fix applied +// only to await-pause-point was missed in this composite path). +func TestRunEnablePausePointCommandAwaitTimeoutIncludesNonFiringHint(t *testing.T) { + originalQuery := queryPausePointStatus + originalPoll := pausePointStatusPoll + originalClear := clearPausePointStatus + pausePointStatusPoll = time.Millisecond + t.Cleanup(func() { + queryPausePointStatus = originalQuery + pausePointStatusPoll = originalPoll + clearPausePointStatus = originalClear + }) + + queryPausePointStatus = func(ctx context.Context, connection unityipc.Connection, id string) (pausePointStatusResponse, error) { + return pausePointStatusResponse{ + Id: id, + Status: pausePointStatusEnabled, + IsEnabled: true, + HitCount: 0, + EditorState: pausePointEditorState{IsPlaying: true, CapturedAt: "Current"}, + }, nil + } + clearPausePointStatus = func(ctx context.Context, connection unityipc.Connection, id string) (pausePointStatusResponse, error) { + return pausePointStatusResponse{Id: id, Status: pausePointStatusCleared}, nil + } + + listener := newLoopbackIpcListener(t) + enableRequests := make(chan map[string]any, 1) + serverErr := make(chan error, 1) + go serveSingleIPCResponse( + listener, + pausePointEnableCommandName, + enableRequests, + serverErr, + `{"Success":true,"Id":"jump","Status":"Enabled","IsEnabled":true,"TimeoutSeconds":1}`, + ) + + connection := unityipc.Connection{ + Endpoint: unityipc.Endpoint{ + Network: listener.Addr().Network(), + Address: listener.Addr().String(), + }, + ProjectRoot: t.TempDir(), + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := runEnablePausePointCommand( + context.Background(), + connection, + []string{"--id", "jump", "--await"}, + t.TempDir(), + &stdout, + &stderr) + + if code != 1 { + t.Fatalf("expected timeout failure, got %d with stdout %s", code, stdout.String()) + } + envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) + hint, _ := envelope.Error.Details["Hint"].(string) + if !strings.Contains(hint, "non-firing patterns") { + t.Fatalf("expected non-firing pattern hint in composite await path, got: %q", hint) + } +} + func serveSingleIPCResponse( listener net.Listener, expectedMethod string, diff --git a/cli/project-runner/internal/projectrunner/pause_point_errors.go b/cli/project-runner/internal/projectrunner/pause_point_errors.go index 8a6fe50661..ff3695c8a9 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_errors.go +++ b/cli/project-runner/internal/projectrunner/pause_point_errors.go @@ -81,7 +81,8 @@ func pausePointTimeoutHint(response pausePointStatusResponse) string { return "Marker was enabled but never hit. Confirm the id matches UloopPausePoint.Pause(\"\") and that the code path was executed. In fast-progressing games the state may have already moved past the marker (for example back to Ready or GameOver), so re-trigger the code path and wait again. " + "If the marker targets a Unity message method such as OnCollisionEnter2D/OnTriggerEnter2D, check whether `enable-pause-point`'s response carried a Warning about cached message dispatch: Unity can resolve a GameObject's message dispatch before the marker patch is installed, so a GameObject that already existed at enable time may never reach the marker even though the method body runs. Recreating the GameObject after enabling, or embedding UloopPausePoint.Pause(\"id\") directly in the method body, avoids this. " + "If the target line is inside a very small method, Mono's JIT may have inlined it into callers and the pause point never fires; move the pause point into the calling method. " + - "If PlayMode kept progressing on its own while you were arranging state (timers, gravity, spawners), the scenario may have already been consumed before this marker could fire; next time, run `control-play-mode --action Pause` before setup and resume with `control-play-mode --action Play` only after `enable-pause-point` succeeds." + "If PlayMode kept progressing on its own while you were arranging state (timers, gravity, spawners), the scenario may have already been consumed before this marker could fire; next time, run `control-play-mode --action Pause` before setup and resume with `control-play-mode --action Play` only after `enable-pause-point` succeeds. " + + "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch." } return "" } diff --git a/cli/project-runner/internal/projectrunner/pause_point_wait_test.go b/cli/project-runner/internal/projectrunner/pause_point_wait_test.go index 7cd3b24615..db3888b0b7 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_wait_test.go +++ b/cli/project-runner/internal/projectrunner/pause_point_wait_test.go @@ -950,7 +950,8 @@ func TestPausePointTimeoutErrorIncludesDiagnosisHint(t *testing.T) { wantHint: "Marker was enabled but never hit. Confirm the id matches UloopPausePoint.Pause(\"\") and that the code path was executed. In fast-progressing games the state may have already moved past the marker (for example back to Ready or GameOver), so re-trigger the code path and wait again. " + "If the marker targets a Unity message method such as OnCollisionEnter2D/OnTriggerEnter2D, check whether `enable-pause-point`'s response carried a Warning about cached message dispatch: Unity can resolve a GameObject's message dispatch before the marker patch is installed, so a GameObject that already existed at enable time may never reach the marker even though the method body runs. Recreating the GameObject after enabling, or embedding UloopPausePoint.Pause(\"id\") directly in the method body, avoids this. " + "If the target line is inside a very small method, Mono's JIT may have inlined it into callers and the pause point never fires; move the pause point into the calling method. " + - "If PlayMode kept progressing on its own while you were arranging state (timers, gravity, spawners), the scenario may have already been consumed before this marker could fire; next time, run `control-play-mode --action Pause` before setup and resume with `control-play-mode --action Play` only after `enable-pause-point` succeeds.", + "If PlayMode kept progressing on its own while you were arranging state (timers, gravity, spawners), the scenario may have already been consumed before this marker could fire; next time, run `control-play-mode --action Pause` before setup and resume with `control-play-mode --action Play` only after `enable-pause-point` succeeds. " + + "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch.", }, }