@@ -201,6 +201,11 @@ async function gatewayWith(
201201 resetResult ?: { cleared : boolean } ;
202202 locations ?: ComputerLocation [ ] ;
203203 token ?: string ;
204+ /** Endpoints the computer answers differently, for the calls that have to fail. */
205+ routes ?: Record <
206+ string ,
207+ ( init ?: RequestInit ) => Response | Promise < Response >
208+ > ;
204209 } ,
205210) {
206211 const { provider, fetchImpl, calls, addressedAs, requests } =
@@ -444,6 +449,68 @@ describe("the computer gateway", () => {
444449 expect ( rows [ 0 ] ?. payload . command ) . toBe ( "cat secrets.txt" ) ;
445450 } ) ;
446451
452+ /**
453+ * The two rows describe one action, so they have to describe the same one.
454+ *
455+ * Asserted as agreement rather than field by field on purpose: the failure row is a second
456+ * hand-maintained argument list, and what goes wrong with one of those is not a particular field
457+ * being wrong, it is a field being added to one and not the other. Comparing the payloads catches
458+ * the next one too.
459+ */
460+ test ( "a permitted action that fails is recorded the same way it was decided" , async ( ) => {
461+ const failing = ( ) =>
462+ Response . json ( { error : "device or resource busy" } , { status : 500 } ) ;
463+
464+ for ( const action of [
465+ {
466+ what : "a command" ,
467+ route : "/exec" ,
468+ run : ( gateway : Awaited < ReturnType < typeof gatewayWith > > [ "gateway" ] ) =>
469+ gateway . runCommand ( "bot-1" , ACTOR , {
470+ command : "rm -rf /workspace/build" ,
471+ } ) ,
472+ } ,
473+ {
474+ what : "a keypress" ,
475+ route : "/key" ,
476+ run : ( gateway : Awaited < ReturnType < typeof gatewayWith > > [ "gateway" ] ) =>
477+ gateway . key ( "bot-1" , ACTOR , {
478+ ref : "e1" ,
479+ snapshotId : 7 ,
480+ key : "Enter" ,
481+ } ) ,
482+ } ,
483+ {
484+ what : "a file write" ,
485+ route : "/files/write" ,
486+ run : ( gateway : Awaited < ReturnType < typeof gatewayWith > > [ "gateway" ] ) =>
487+ gateway . writeFile ( "bot-1" , ACTOR , { path : "notes.md" , text : "kept" } ) ,
488+ } ,
489+ ] ) {
490+ const { gateway, rows } = await gatewayWith ( PERMISSIVE , {
491+ routes : { [ action . route ] : failing } ,
492+ } ) ;
493+ rows . length = 0 ;
494+
495+ await expect ( action . run ( gateway ) ) . rejects . toThrow ( ) ;
496+
497+ const allowed = rows . find (
498+ ( row ) => row . eventType === "computer.action_allowed" ,
499+ ) ;
500+ const failed = rows . find (
501+ ( row ) => row . eventType === "computer.action_failed" ,
502+ ) ;
503+ expect ( allowed , action . what ) . toBeDefined ( ) ;
504+ expect ( failed , action . what ) . toBeDefined ( ) ;
505+
506+ // The outcome is the only thing the failure row adds. Everything describing what was attempted
507+ // is the same action and reads the same on both rows.
508+ const { failure, ...attempted } = failed ?. payload ?? { } ;
509+ expect ( failure , action . what ) . toBeString ( ) ;
510+ expect ( attempted , action . what ) . toEqual ( allowed ?. payload ?? { } ) ;
511+ }
512+ } ) ;
513+
447514 test ( "the computer is told WHICH Bot is asking" , async ( ) => {
448515 // Every per-Bot behaviour on the computer keys off this id: the profile it opens, the logins it
449516 // has, the proxy its traffic leaves through, and who holds its wheel.
0 commit comments