1010// scenario × target matrix) plus whatever artifacts the surfaces produced
1111// (browser video/trace/screenshots, terminal casts).
1212import { execFileSync } from "node:child_process" ;
13- import { existsSync , mkdirSync , readFileSync , readdirSync , rmSync , writeFileSync } from "node:fs" ;
13+ import {
14+ existsSync ,
15+ mkdirSync ,
16+ readFileSync ,
17+ readdirSync ,
18+ rmSync ,
19+ writeFileSync ,
20+ } from "node:fs" ;
1421import { join } from "node:path" ;
1522import { fileURLToPath } from "node:url" ;
1623
@@ -24,8 +31,23 @@ import { makeApiSurface } from "./surfaces/api";
2431import { makeBrowserSurface } from "./surfaces/browser" ;
2532import { makeCliSurface } from "./surfaces/cli" ;
2633import { makeMcpSurface } from "./surfaces/mcp" ;
27- import { completeOAuthConsent , hasOpenCode , makeOpenCodeHome , warmUp } from "./clients/opencode" ;
28- import { Api , Billing , Browser , Cli , Mcp , OpenCode , RunDir , Target , TtlControl } from "./services" ;
34+ import {
35+ completeOAuthConsent ,
36+ hasOpenCode ,
37+ makeOpenCodeHome ,
38+ warmUp ,
39+ } from "./clients/opencode" ;
40+ import {
41+ Api ,
42+ Billing ,
43+ Browser ,
44+ Cli ,
45+ Mcp ,
46+ OpenCode ,
47+ RunDir ,
48+ Target ,
49+ TtlControl ,
50+ } from "./services" ;
2951import { buildManifest } from "./viewer/manifest" ;
3052
3153export const RUNS_DIR = fileURLToPath ( new URL ( "../runs/" , import . meta. url ) ) ;
@@ -41,24 +63,38 @@ export interface ScenarioOptions {
4163 readonly timeout ?: number ;
4264}
4365
44- type AllServices = Target | RunDir | Cli | Api | Browser | Mcp | Billing | OpenCode | TtlControl ;
66+ type AllServices =
67+ | Target
68+ | RunDir
69+ | Cli
70+ | Api
71+ | Browser
72+ | Mcp
73+ | Billing
74+ | OpenCode
75+ | TtlControl ;
4576
4677/**
4778 * What this target on this host can provide. Services beyond the base are
4879 * conditional, so the claimed type is the full union — yielding an absent
4980 * one fails with Effect's missing-service defect, which the runner turns
5081 * into the skip.
5182 */
52- const contextFor = ( target : TargetShape , dir : string ) : Context . Context < AllServices > => {
83+ const contextFor = (
84+ target : TargetShape ,
85+ dir : string ,
86+ ) : Context . Context < AllServices > => {
5387 let context = Context . empty ( ) . pipe (
5488 Context . add ( Target , target ) ,
5589 Context . add ( RunDir , dir ) ,
5690 Context . add ( Cli , makeCliSurface ( ) ) ,
5791 ) as Context . Context < AllServices > ;
5892 const has = target . capabilities . has . bind ( target . capabilities ) ;
5993 if ( has ( "api" ) ) context = Context . add ( context , Api , makeApiSurface ( target ) ) ;
60- if ( has ( "browser" ) ) context = Context . add ( context , Browser , makeBrowserSurface ( dir , target ) ) ;
61- if ( has ( "mcp-oauth" ) ) context = Context . add ( context , Mcp , makeMcpSurface ( target ) ) ;
94+ if ( has ( "browser" ) )
95+ context = Context . add ( context , Browser , makeBrowserSurface ( dir , target ) ) ;
96+ if ( has ( "mcp-oauth" ) )
97+ context = Context . add ( context , Mcp , makeMcpSurface ( target , dir ) ) ;
6298 if ( has ( "billing" ) ) context = Context . add ( context , Billing , true ) ;
6399 if ( hasOpenCode ( ) ) {
64100 context = Context . add ( context , OpenCode , {
@@ -101,24 +137,32 @@ export const scenario = (
101137 const endedAt = Date . now ( ) ;
102138
103139 // Yielding a service this target can't provide is the skip signal.
104- const missing = exit . _tag === "Failure" ? missingServices ( exit . cause ) : [ ] ;
140+ const missing =
141+ exit . _tag === "Failure" ? missingServices ( exit . cause ) : [ ] ;
105142 if ( missing . length > 0 ) {
106143 rmSync ( dir , { recursive : true , force : true } ) ;
107144 mkdirSync ( dir , { recursive : true } ) ;
108145 writeFileSync (
109146 join ( dir , "skipped.json" ) ,
110- JSON . stringify ( { scenario : name , target : target . name , missing } , null , 1 ) ,
147+ JSON . stringify (
148+ { scenario : name , target : target . name , missing } ,
149+ null ,
150+ 1 ,
151+ ) ,
111152 ) ;
112153 buildManifest ( RUNS_DIR ) ;
113154 return yield * Effect . sync ( ( ) =>
114155 testCtx . skip ( `needs ${ missing . join ( ", " ) } — not on ${ target . name } ` ) ,
115156 ) ;
116157 }
117158
118- const error = exit . _tag === "Failure" ? failureMessage ( exit . cause ) : undefined ;
159+ const error =
160+ exit . _tag === "Failure" ? failureMessage ( exit . cause ) : undefined ;
119161 // The test source is the review artifact — ship this scenario's code
120162 // (imports + sibling scenarios stripped) alongside the run.
121- const source = testFile ? extractScenarioSource ( testFile , name ) : undefined ;
163+ const source = testFile
164+ ? extractScenarioSource ( testFile , name )
165+ : undefined ;
122166 if ( source ) writeFileSync ( join ( dir , "test.ts" ) , source ) ;
123167 writeFileSync (
124168 join ( dir , "result.json" ) ,
@@ -152,7 +196,10 @@ export const scenario = (
152196 try {
153197 execFileSync (
154198 "bun" ,
155- [ fileURLToPath ( new URL ( "../scripts/film.ts" , import . meta. url ) ) , dir ] ,
199+ [
200+ fileURLToPath ( new URL ( "../scripts/film.ts" , import . meta. url ) ) ,
201+ dir ,
202+ ] ,
156203 { stdio : "pipe" , timeout : 120_000 } ,
157204 ) ;
158205 } catch {
@@ -170,7 +217,9 @@ export const scenario = (
170217} ;
171218
172219/** Service keys (sans the e2e/ prefix) whose absence caused this failure. */
173- const missingServices = ( cause : Cause . Cause < unknown > ) : ReadonlyArray < string > => {
220+ const missingServices = (
221+ cause : Cause . Cause < unknown > ,
222+ ) : ReadonlyArray < string > => {
174223 const rendered = String ( Cause . squash ( cause ) ) ;
175224 return [ ...rendered . matchAll ( / S e r v i c e n o t f o u n d : e 2 e \/ ( [ ^ \s ( ] + ) / g) ]
176225 . map ( ( match ) => match [ 1 ] ?? "" )
@@ -198,9 +247,15 @@ const captureTestFile = (): string | undefined => {
198247 * part of understanding the test). Falls back to undefined on any surprise so
199248 * a parsing edge case can never fail a run.
200249 */
201- const extractScenarioSource = ( filePath : string , name : string ) : string | undefined => {
250+ const extractScenarioSource = (
251+ filePath : string ,
252+ name : string ,
253+ ) : string | undefined => {
202254 try {
203- const source = readFileSync ( filePath , "utf8" ) . replace ( / ^ i m p o r t [ \s \S ] * ?; [ ^ \S \n ] * $ / gm, "" ) ;
255+ const source = readFileSync ( filePath , "utf8" ) . replace (
256+ / ^ i m p o r t [ \s \S ] * ?; [ ^ \S \n ] * $ / gm,
257+ "" ,
258+ ) ;
204259 const needle = "scenario(" ;
205260 const blocks : Array < { start : number ; end : number ; mine : boolean } > = [ ] ;
206261 let index = 0 ;
@@ -218,7 +273,11 @@ const extractScenarioSource = (filePath: string, name: string): string | undefin
218273 }
219274 }
220275 if ( end === - 1 ) return undefined ; // unbalanced — bail to be safe
221- blocks . push ( { start : index , end, mine : source . slice ( index , end ) . includes ( `"${ name } "` ) } ) ;
276+ blocks . push ( {
277+ start : index ,
278+ end,
279+ mine : source . slice ( index , end ) . includes ( `"${ name } "` ) ,
280+ } ) ;
222281 index = end ;
223282 }
224283 if ( ! blocks . some ( ( b ) => b . mine ) ) return undefined ;
0 commit comments