11import { beforeEach , describe , expect , it , vi } from "vitest" ;
22
33const mocks = vi . hoisted ( ( ) => ( {
4- getCurrentPrePrCheckConfig : vi . fn ( ) ,
5- runPrePrChecksWithFixes : vi . fn ( ) ,
64 info : vi . fn ( ) ,
75 warn : vi . fn ( ) ,
86 error : vi . fn ( ) ,
97} ) ) ;
108
11- vi . mock ( "../db/client.js" , ( ) => ( { getDb : ( ) => ( { kind : "db" } ) } ) ) ;
12- vi . mock ( "../pre-pr-checks/store.js" , ( ) => ( {
13- getCurrentPrePrCheckConfig : ( ...args : any [ ] ) =>
14- mocks . getCurrentPrePrCheckConfig ( ...args ) ,
15- } ) ) ;
16- vi . mock ( "../pre-pr-checks/runner.js" , ( ) => ( {
17- runPrePrChecksWithFixes : ( ...args : any [ ] ) =>
18- mocks . runPrePrChecksWithFixes ( ...args ) ,
19- } ) ) ;
209vi . mock ( "../lib/logger.js" , ( ) => ( {
2110 logger : { info : mocks . info , warn : mocks . warn , error : mocks . error } ,
2211} ) ) ;
12+ // The real redactor reads process.env to find the secrets it blanks, which
13+ // makes its output depend on the machine running the suite. Redaction itself
14+ // is pinned below against an injected redactor.
15+ vi . mock ( "../sandbox/agents/protocol.js" , ( ) => ( {
16+ redactDiagnosticText : ( value : string ) => value ,
17+ } ) ) ;
2318vi . mock ( "../../env.js" , ( ) => ( {
2419 env : { DASHBOARD_ORIGIN : "https://dashboard.example.com" } ,
2520} ) ) ;
2621
2722import {
2823 PRE_PR_CHECKS_FAILURE_CAUSE_MAX_LENGTH ,
2924 PRE_PR_CHECKS_FAILURE_STACK_TAIL_MAX_LENGTH ,
25+ describePrePrChecksFailureStep ,
26+ prePrChecksFailureInput ,
3027 prePrChecksFailureMustPropagate ,
3128 prePrChecksFailureReport ,
32- runPrePrChecksStep ,
3329} from "./agent.js" ;
3430import { isDurationAbortError } from "./run-budget.js" ;
3531import { isRunControlError } from "./run-control-error.js" ;
@@ -43,51 +39,27 @@ function namedError(name: string, message: string): Error {
4339 return error ;
4440}
4541
46- function runStep ( ) {
47- return runPrePrChecksStep ( "sbx-test-123" , "codex" , "gpt-5" ) ;
42+ /**
43+ * What the call site does with a throw it may not propagate: flatten it in
44+ * workflow scope, then compose and log the sentence inside the step. The
45+ * checks stopped being a step of their own (they are launched detached and
46+ * polled), so this pair is the seam that carries what #316 landed.
47+ */
48+ function describe_ ( error : unknown , version : number | null = 7 ) {
49+ return describePrePrChecksFailureStep ( prePrChecksFailureInput ( error ) , version ) ;
4850}
4951
5052describe ( "pre-PR checks step failure cause" , ( ) => {
5153 beforeEach ( ( ) => {
5254 vi . clearAllMocks ( ) ;
53- mocks . getCurrentPrePrCheckConfig . mockResolvedValue ( {
54- version : 7 ,
55- config : { repositories : [ ] } ,
56- } ) ;
57- } ) ;
58-
59- it ( "returns the checks result and the loaded version when nothing throws" , async ( ) => {
60- mocks . runPrePrChecksWithFixes . mockResolvedValue ( {
61- outcome : "passed" ,
62- passed : true ,
63- fixCycles : 0 ,
64- fixCycleUsages : [ ] ,
65- budgetFailure : null ,
66- summary : "All checks passed." ,
67- } ) ;
68-
69- await expect ( runStep ( ) ) . resolves . toEqual ( {
70- outcome : "passed" ,
71- passed : true ,
72- fixCycles : 0 ,
73- fixCycleUsages : [ ] ,
74- budgetFailure : null ,
75- summary : "All checks passed." ,
76- configurationVersion : 7 ,
77- } ) ;
78- expect ( mocks . error ) . not . toHaveBeenCalled ( ) ;
7955 } ) ;
8056
8157 it ( "names the thrown cause instead of leaving Workflow's wrapper to speak alone" , async ( ) => {
8258 // Production runs wrun_01M0CBQNAX24STRMN5SGCKKGB2 and
8359 // wrun_01M0CAZKV3YMNFBCZJA8MT95GW died here reading only "exceeded max
8460 // retries", with no sanitized output captured and nothing in the runtime
8561 // logs. Whatever the step throws has to reach the operator-facing text.
86- mocks . runPrePrChecksWithFixes . mockRejectedValue (
87- new Error ( "sandbox connection reset" ) ,
88- ) ;
89-
90- await expect ( runStep ( ) ) . rejects . toThrow (
62+ await expect ( describe_ ( new Error ( "sandbox connection reset" ) ) ) . resolves . toBe (
9163 `${ MESSAGE_LEAD } sandbox connection reset` ,
9264 ) ;
9365 expect ( mocks . error ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -109,65 +81,46 @@ describe("pre-PR checks step failure cause", () => {
10981 } ) ;
11082
11183 it ( "prefers a system error code over a class name that says nothing" , async ( ) => {
112- mocks . runPrePrChecksWithFixes . mockRejectedValue (
113- Object . assign ( new Error ( "connect ECONNREFUSED 10.0.0.1:443" ) , {
114- code : "ECONNREFUSED" ,
115- } ) ,
116- ) ;
84+ const error = Object . assign ( new Error ( "connect ECONNREFUSED 10.0.0.1:443" ) , {
85+ code : "ECONNREFUSED" ,
86+ } ) ;
11787
118- await expect ( runStep ( ) ) . rejects . toThrow (
88+ await expect ( describe_ ( error ) ) . resolves . toBe (
11989 `${ MESSAGE_LEAD } ECONNREFUSED: connect ECONNREFUSED 10.0.0.1:443` ,
12090 ) ;
12191 } ) ;
12292
12393 it ( "bounds a runaway cause instead of embedding it whole" , async ( ) => {
12494 const thrownMessage = "sandbox refused the launch. " . repeat ( 200 ) ;
125- mocks . runPrePrChecksWithFixes . mockRejectedValue ( new Error ( thrownMessage ) ) ;
12695
127- const thrown = await runStep ( ) . then (
128- ( ) => null ,
129- ( err : unknown ) => err as Error ,
130- ) ;
96+ const message = await describe_ ( new Error ( thrownMessage ) ) ;
13197
132- expect ( thrown ?. message ) . toContain ( MESSAGE_LEAD ) ;
133- expect ( thrown ?. message ) . not . toContain ( thrownMessage ) ;
98+ expect ( message ) . toContain ( MESSAGE_LEAD ) ;
99+ expect ( message ) . not . toContain ( thrownMessage ) ;
134100 // Sentence plus the bound the cause is clamped to, and nothing more, so a
135101 // runaway error text cannot become the run status.
136- expect ( thrown ?. message . length ) . toBeLessThanOrEqual (
102+ expect ( message . length ) . toBeLessThanOrEqual (
137103 MESSAGE_LEAD . length + PRE_PR_CHECKS_FAILURE_CAUSE_MAX_LENGTH ,
138104 ) ;
139105 const logged = mocks . error . mock . calls [ 0 ] ?. [ 0 ] as { cause : string } ;
140106 expect ( logged . cause . length ) . toBe ( PRE_PR_CHECKS_FAILURE_CAUSE_MAX_LENGTH ) ;
141107 } ) ;
142108
143109 it . each ( runControlErrorCases ( ) ) (
144- "rethrows %s untouched so the call site still recognizes it" ,
145- async ( _label , error ) => {
146- mocks . runPrePrChecksWithFixes . mockRejectedValue ( error ) ;
147-
148- await expect ( runStep ( ) ) . rejects . toBe ( error ) ;
149- const thrown = await runStep ( ) . then (
150- ( ) => null ,
151- ( err : unknown ) => err ,
152- ) ;
153- expect ( isRunControlError ( thrown ) ) . toBe ( true ) ;
154- expect ( mocks . error ) . not . toHaveBeenCalled ( ) ;
110+ "keeps %s out of the wrap so the call site still recognizes it" ,
111+ ( _label , error ) => {
112+ expect ( prePrChecksFailureMustPropagate ( error ) ) . toBe ( true ) ;
113+ expect ( isRunControlError ( error ) ) . toBe ( true ) ;
155114 } ,
156115 ) ;
157116
158117 it . each ( [ [ "AbortError" ] , [ "TimeoutError" ] ] ) (
159- "rethrows a %s untouched so the duration budget stop survives" ,
160- async ( name ) => {
118+ "keeps a %s out of the wrap so the duration budget stop survives" ,
119+ ( name ) => {
161120 const error = namedError ( name , "The operation was aborted." ) ;
162- mocks . runPrePrChecksWithFixes . mockRejectedValue ( error ) ;
163-
164- await expect ( runStep ( ) ) . rejects . toBe ( error ) ;
165- const thrown = await runStep ( ) . then (
166- ( ) => null ,
167- ( err : unknown ) => err ,
168- ) ;
169- expect ( isDurationAbortError ( thrown ) ) . toBe ( true ) ;
170- expect ( mocks . error ) . not . toHaveBeenCalled ( ) ;
121+
122+ expect ( prePrChecksFailureMustPropagate ( error ) ) . toBe ( true ) ;
123+ expect ( isDurationAbortError ( error ) ) . toBe ( true ) ;
171124 } ,
172125 ) ;
173126
@@ -184,18 +137,18 @@ describe("pre-PR checks step failure cause", () => {
184137 expect ( prePrChecksFailureMustPropagate ( new Error ( "sandbox died" ) ) ) . toBe ( false ) ;
185138
186139 const rewrappedBudget = new Error (
187- prePrChecksFailureReport ( budgetStop , identity ) . message ,
140+ prePrChecksFailureReport ( prePrChecksFailureInput ( budgetStop ) , identity ) . message ,
188141 ) ;
189142 expect ( isRunControlError ( rewrappedBudget ) ) . toBe ( false ) ;
190143 const rewrappedAbort = new Error (
191- prePrChecksFailureReport ( abort , identity ) . message ,
144+ prePrChecksFailureReport ( prePrChecksFailureInput ( abort ) , identity ) . message ,
192145 ) ;
193146 expect ( isDurationAbortError ( rewrappedAbort ) ) . toBe ( false ) ;
194147 } ) ;
195148
196149 it ( "redacts the cause and the stack tail through the caller's redactor" , ( ) => {
197150 const report = prePrChecksFailureReport (
198- new Error ( "auth failed for token=abcd1234" ) ,
151+ prePrChecksFailureInput ( new Error ( "auth failed for token=abcd1234" ) ) ,
199152 ( value ) => value . replace ( "abcd1234" , "[REDACTED]" ) ,
200153 ) ;
201154
@@ -204,7 +157,10 @@ describe("pre-PR checks step failure cause", () => {
204157 } ) ;
205158
206159 it ( "names a non-Error throw rather than dropping it" , ( ) => {
207- const report = prePrChecksFailureReport ( "sandbox vanished" , ( v ) => v ) ;
160+ const report = prePrChecksFailureReport (
161+ prePrChecksFailureInput ( "sandbox vanished" ) ,
162+ ( v ) => v ,
163+ ) ;
208164
209165 expect ( report . message ) . toBe ( `${ MESSAGE_LEAD } sandbox vanished` ) ;
210166 expect ( report . name ) . toBe ( "string" ) ;
0 commit comments