@@ -5,7 +5,7 @@ import { join } from "node:path";
55import type { Fixture } from "./fixture.js" ;
66import type { ConformanceAdapter } from "./adapter.js" ;
77import { UnknownCapabilityError } from "./adapter.js" ;
8- import { parseOperationScript } from "./operation-script.js" ;
8+ import { parseOperationScript , parseExpectedErrors } from "./operation-script.js" ;
99import { resultsEqual , type NormalizedResult } from "./result.js" ;
1010import type { CheckResult , FixtureReport } from "./report.js" ;
1111
@@ -23,16 +23,29 @@ export async function runFixture(
2323 const outcome = await adapter . loadFixture ( fix . inputDir , fix . providers ) ;
2424
2525 if ( fix . hasExpectedErrors ) {
26- const expected = ( await readJson ( join ( fix . dir , "expected-errors.json" ) ) ) as
27- { code : string } [ ] ;
28- const want = expected . map ( ( e ) => e . code ) . sort ( ) ;
29- const got = [ ...outcome . errorCodes ] . sort ( ) ;
30- const passed = want . length === got . length && want . every ( ( c , i ) => c === got [ i ] ) ;
31- checks . push ( {
32- kind : "expected-errors" ,
33- passed,
34- ...( passed ? { } : { detail : `expected [${ want } ], got [${ got } ]` } ) ,
35- } ) ;
26+ // Fix 1: use parseExpectedErrors — throws a clear Error on malformed input.
27+ // Fix 2: wrap the read+parse so a bad file produces a failed check, not a throw.
28+ let want : string [ ] | undefined ;
29+ try {
30+ want = parseExpectedErrors ( await readJson ( join ( fix . dir , "expected-errors.json" ) ) )
31+ . map ( ( e ) => e . code )
32+ . sort ( ) ;
33+ } catch ( err ) {
34+ checks . push ( {
35+ kind : "expected-errors" ,
36+ passed : false ,
37+ detail : `expected-errors.json parse error: ${ ( err as Error ) . message } ` ,
38+ } ) ;
39+ }
40+ if ( want !== undefined ) {
41+ const got = [ ...outcome . errorCodes ] . sort ( ) ;
42+ const passed = want . length === got . length && want . every ( ( c , i ) => c === got [ i ] ) ;
43+ checks . push ( {
44+ kind : "expected-errors" ,
45+ passed,
46+ ...( passed ? { } : { detail : `expected [${ want } ], got [${ got } ]` } ) ,
47+ } ) ;
48+ }
3649 }
3750
3851 // If the load produced no tree but tree-dependent checks are expected, push a
@@ -48,60 +61,102 @@ export async function runFixture(
4861 } ) ;
4962 }
5063
64+ // Fix 6: hoist tree after the undefined guard so tree-dependent blocks never
65+ // need outcome.tree! non-null assertions.
5166 if ( fix . hasExpected && outcome . tree !== undefined ) {
52- const want = ( await readFile ( join ( fix . dir , "expected.json" ) , "utf8" ) ) . trim ( ) ;
53- const got = adapter . canonicalSerialize ( outcome . tree ) . trim ( ) ;
54- const passed = want === got ;
55- checks . push ( {
56- kind : "expected" ,
57- passed,
58- ...( passed ? { } : { detail : "canonical serialization mismatch" } ) ,
59- } ) ;
67+ const tree = outcome . tree ;
68+ let want : string | undefined ;
69+ try {
70+ // Fix 2: wrap file read so parse errors become failed checks, not throws.
71+ want = ( await readFile ( join ( fix . dir , "expected.json" ) , "utf8" ) ) . trim ( ) ;
72+ } catch ( err ) {
73+ checks . push ( {
74+ kind : "expected" ,
75+ passed : false ,
76+ detail : `expected.json read error: ${ ( err as Error ) . message } ` ,
77+ } ) ;
78+ }
79+ if ( want !== undefined ) {
80+ const got = adapter . canonicalSerialize ( tree ) . trim ( ) ;
81+ const passed = want === got ;
82+ checks . push ( {
83+ kind : "expected" ,
84+ passed,
85+ ...( passed ? { } : { detail : "canonical serialization mismatch" } ) ,
86+ } ) ;
87+ }
6088 }
6189
6290 if ( fix . hasExpectedEffective && outcome . tree !== undefined ) {
63- const want = ( await readFile ( join ( fix . dir , "expected-effective.json" ) , "utf8" ) ) . trim ( ) ;
64- const got = adapter . canonicalSerializeEffective ( outcome . tree ) . trim ( ) ;
65- const passed = want === got ;
66- checks . push ( {
67- kind : "expected-effective" ,
68- passed,
69- ...( passed ? { } : { detail : "effective serialization mismatch" } ) ,
70- } ) ;
91+ const tree = outcome . tree ;
92+ let want : string | undefined ;
93+ try {
94+ // Fix 2: wrap file read so parse errors become failed checks, not throws.
95+ want = ( await readFile ( join ( fix . dir , "expected-effective.json" ) , "utf8" ) ) . trim ( ) ;
96+ } catch ( err ) {
97+ checks . push ( {
98+ kind : "expected-effective" ,
99+ passed : false ,
100+ detail : `expected-effective.json read error: ${ ( err as Error ) . message } ` ,
101+ } ) ;
102+ }
103+ if ( want !== undefined ) {
104+ const got = adapter . canonicalSerializeEffective ( tree ) . trim ( ) ;
105+ const passed = want === got ;
106+ checks . push ( {
107+ kind : "expected-effective" ,
108+ passed,
109+ ...( passed ? { } : { detail : "effective serialization mismatch" } ) ,
110+ } ) ;
111+ }
71112 }
72113
73114 if ( fix . hasScript && outcome . tree !== undefined ) {
74- const script = parseOperationScript ( await readJson ( join ( fix . dir , "script.json" ) ) ) ;
75- script . operations . forEach ( ( op , i ) => {
76- if ( ! capabilities . includes ( op . invoke ) ) capabilities . push ( op . invoke ) ;
77- const node = adapter . navigate ( outcome . tree ! , op . navigate ) ;
78- if ( node === undefined ) {
115+ const tree = outcome . tree ;
116+ let script : ReturnType < typeof parseOperationScript > | undefined ;
117+ try {
118+ // Fix 2: wrap script parse so malformed script.json produces a failed check.
119+ script = parseOperationScript ( await readJson ( join ( fix . dir , "script.json" ) ) ) ;
120+ } catch ( err ) {
121+ checks . push ( {
122+ kind : "operation" ,
123+ operationIndex : 0 ,
124+ passed : false ,
125+ detail : `script.json parse error: ${ ( err as Error ) . message } ` ,
126+ } ) ;
127+ }
128+ if ( script !== undefined ) {
129+ script . operations . forEach ( ( op , i ) => {
130+ if ( ! capabilities . includes ( op . invoke ) ) capabilities . push ( op . invoke ) ;
131+ const node = adapter . navigate ( tree , op . navigate ) ;
132+ if ( node === undefined ) {
133+ checks . push ( {
134+ kind : "operation" ,
135+ operationIndex : i ,
136+ passed : false ,
137+ detail : `navigate [${ op . navigate } ] did not resolve` ,
138+ } ) ;
139+ return ;
140+ }
141+ let actual : NormalizedResult ;
142+ try {
143+ actual = adapter . invoke ( node , op . invoke , op . args ?? { } ) ;
144+ } catch ( err ) {
145+ const detail = err instanceof UnknownCapabilityError
146+ ? `unbound capability '${ op . invoke } '`
147+ : `invoke threw: ${ ( err as Error ) . message } ` ;
148+ checks . push ( { kind : "operation" , operationIndex : i , passed : false , detail } ) ;
149+ return ;
150+ }
151+ const passed = resultsEqual ( actual , op . expect ) ;
79152 checks . push ( {
80153 kind : "operation" ,
81154 operationIndex : i ,
82- passed : false ,
83- detail : `navigate [ ${ op . navigate } ] did not resolve` ,
155+ passed,
156+ ... ( passed ? { } : { detail : `expected ${ JSON . stringify ( op . expect ) } , got ${ JSON . stringify ( actual ) } ` } ) ,
84157 } ) ;
85- return ;
86- }
87- let actual : NormalizedResult ;
88- try {
89- actual = adapter . invoke ( node , op . invoke , op . args ?? { } ) ;
90- } catch ( err ) {
91- const detail = err instanceof UnknownCapabilityError
92- ? `unbound capability '${ op . invoke } '`
93- : `invoke threw: ${ ( err as Error ) . message } ` ;
94- checks . push ( { kind : "operation" , operationIndex : i , passed : false , detail } ) ;
95- return ;
96- }
97- const passed = resultsEqual ( actual , op . expect ) ;
98- checks . push ( {
99- kind : "operation" ,
100- operationIndex : i ,
101- passed,
102- ...( passed ? { } : { detail : `expected ${ JSON . stringify ( op . expect ) } , got ${ JSON . stringify ( actual ) } ` } ) ,
103158 } ) ;
104- } ) ;
159+ }
105160 }
106161
107162 // A fixture with no expectation files at all is a configuration error — push an
0 commit comments