@@ -2264,6 +2264,51 @@ async function selfTest() {
22642264 [ 'repo-scope-refused' , [ 'rate:anon' , 'repo' ] ] ,
22652265 ) ;
22662266
2267+ // -- renderFixLines: one remedy is one `fix:` marker (#10362) -------------
2268+ // Driven through the REAL producer, not a hand-written array: what needs
2269+ // pinning is that this file renders what `classifyTransportProbe` actually
2270+ // emits. A fixture would only restate the printer's own assumption about the
2271+ // shape, which is the assumption that was wrong.
2272+ const refusedFix = classifyTransportProbe ( {
2273+ token : 'ghp_x' ,
2274+ authed : { status : 200 , rateLimitRemaining : 4999 } ,
2275+ repo : { status : 403 , rateLimitRemaining : 4999 } ,
2276+ } ) . fix ;
2277+ t ( 'the producer still wraps this remedy across several lines' , refusedFix . length > 1 , true ) ;
2278+ t (
2279+ 'a multi-line remedy renders with exactly ONE fix: marker' ,
2280+ renderFixLines ( refusedFix ) . filter ( ( l ) => l . startsWith ( ' fix: ' ) ) . length ,
2281+ 1 ,
2282+ ) ;
2283+ t (
2284+ 'its continuations are padded under the marker rather than re-marked' ,
2285+ renderFixLines ( refusedFix ) . slice ( 1 ) . every ( ( l ) => l . startsWith ( ' ' ) && ! l . includes ( 'fix: ' ) ) ,
2286+ true ,
2287+ ) ;
2288+ t ( 'rendering neither adds a line nor drops one' , renderFixLines ( refusedFix ) . length , refusedFix . length ) ;
2289+
2290+ // The branch that makes re-wrapping unsafe: a copy-pasteable command whose
2291+ // annotation carries its own indentation and an arrow pointing back at it.
2292+ const anonFix = classifyTransportProbe ( {
2293+ token : 'ghp_x' ,
2294+ authed : { status : 401 } ,
2295+ anon : { status : 200 , rateLimitRemaining : 59 } ,
2296+ } ) . fix ;
2297+ t (
2298+ 'the copy-pasteable command keeps the marked line to itself' ,
2299+ renderFixLines ( anonFix ) [ 0 ] ,
2300+ ' fix: GITHUB_TOKEN= GH_TOKEN= node scripts/pm/check-half-states.mjs' ,
2301+ ) ;
2302+ t (
2303+ 'the annotation keeps its own indent, so its arrow still points at that command' ,
2304+ renderFixLines ( anonFix ) [ 1 ] . startsWith ( ' ↑' ) ,
2305+ true ,
2306+ ) ;
2307+
2308+ t ( 'an empty fix renders nothing at all' , renderFixLines ( [ ] ) , [ ] ) ;
2309+ t ( 'an absent fix renders nothing at all' , renderFixLines ( undefined ) , [ ] ) ;
2310+ t ( 'a one-line remedy is just the marked line' , renderFixLines ( [ 'do the thing' ] ) , [ ' fix: do the thing' ] ) ;
2311+
22672312 if ( failures . length > 0 ) {
22682313 console . error ( `✗ ci-failure --self-test (${ failures . length } failure(s)):\n` ) ;
22692314 for ( const f of failures ) console . error ( ` • ${ f } ` ) ;
@@ -2295,10 +2340,42 @@ async function selfTest() {
22952340 ' annotations carried none, a log that anchored nothing is still a shortfall rather than a\n' +
22962341 ' manufactured answer, a 410 is named as expired retention rather than as an absence of\n' +
22972342 ' evidence, a CONNECT refusal lands as transport with the blocked host to report, and a\n' +
2298- ' tail with no `##[error]` in it is labelled a window rather than an anchor.' ,
2343+ ' tail with no `##[error]` in it is labelled a window rather than an anchor. And a `fix`\n' +
2344+ ' renders as the ONE remedy it is: a single `fix:` marker with its continuations padded\n' +
2345+ ' under it, keeping a copy-pasteable command on a line of its own.' ,
22992346 ) ;
23002347}
23012348
2349+ /**
2350+ * Render a verdict's `fix` as the ONE remedy it is: the marker goes on the first
2351+ * line, and every continuation is padded to sit under it (#10362).
2352+ *
2353+ * `fix` arrives as the wrapped lines of a single sentence — never as a list of
2354+ * independent remedies (measured across every branch of
2355+ * `classifyTransportProbe`). Marking each element `fix:` rendered one remedy as
2356+ * three, and a reader counting "how many things do I have to do" counted wrong.
2357+ *
2358+ * The lines are padded rather than re-flowed because the producer's breaks are
2359+ * load-bearing in at least one branch: `bad-credential-anon-reachable` is a
2360+ * copy-pasteable command followed by an annotation carrying its own
2361+ * indentation, whose `↑` points at the command above it. Joining and
2362+ * re-wrapping would swallow the command into the prose and leave the arrow
2363+ * pointing at nothing, so continuations keep their own leading whitespace.
2364+ *
2365+ * This is the idiom `check-half-states.mjs` — the file that PRODUCES these
2366+ * verdicts — already prints with. Both of this file's printers go through here
2367+ * so the two cannot drift apart again, which is how the second one came to
2368+ * exist (#10155 matched the entry point's spelling rather than fixing it in an
2369+ * unrelated PR).
2370+ *
2371+ * Presentation only: no exit code, no verdict, no claim about the tree.
2372+ */
2373+ function renderFixLines ( fix ) {
2374+ const lines = fix ?? [ ] ;
2375+ if ( lines . length === 0 ) return [ ] ;
2376+ return [ ` fix: ${ lines [ 0 ] } ` , ...lines . slice ( 1 ) . map ( ( l ) => ( l ? ` ${ l } ` : '' ) ) ] ;
2377+ }
2378+
23022379/**
23032380 * The mid-walk net's printer (#10155). The DECISION is `midWalkVerdict`, pure
23042381 * and self-tested; what lives here is the one thing it cannot be handed — the
@@ -2320,7 +2397,7 @@ async function reportMidWalkFailure(error, stage) {
23202397 const decision = midWalkVerdict ( { probe, error, read : error ?. read ?? null , stage } ) ;
23212398 console . error ( `\nci-failure: ${ decision . verdict } — ${ decision . headline } \n` ) ;
23222399 for ( const line of decision . detail ) console . error ( line ? ` ${ line } ` : '' ) ;
2323- for ( const line of decision . fix ?? [ ] ) console . error ( ` fix: ${ line } ` ) ;
2400+ for ( const line of renderFixLines ( decision . fix ) ) console . error ( line ) ;
23242401 console . error ( " Piping reports the PIPE's status, so `... | tail` reads green either way. Use `echo \"EXIT=$?\"`." ) ;
23252402 process . exit ( decision . exit ) ;
23262403}
@@ -2377,7 +2454,7 @@ if (!invokedDirectly) {
23772454 if ( probe . kind !== 'reachable' ) {
23782455 console . error ( `ci-failure: PREREQUISITE NOT MET — ${ probe . headline } ` ) ;
23792456 for ( const line of probe . detail ?? [ ] ) console . error ( ` ${ line } ` ) ;
2380- for ( const line of probe . fix ?? [ ] ) console . error ( ` fix: ${ line } ` ) ;
2457+ for ( const line of renderFixLines ( probe . fix ) ) console . error ( line ) ;
23812458 console . error ( ` (Exit ${ EXIT_PREREQUISITE_NOT_MET } . This classifies the ENVIRONMENT, not the tree.)` ) ;
23822459 process . exit ( EXIT_PREREQUISITE_NOT_MET ) ;
23832460 }
0 commit comments