11import { describe , expect , it } from "@effect/vitest" ;
2- import { Effect , Predicate } from "effect" ;
2+ import { Effect , Predicate , Schema } from "effect" ;
33
4- import { createExecutor , definePlugin } from "@executor-js/sdk" ;
4+ import { createExecutor , definePlugin , ElicitationResponse , tool } from "@executor-js/sdk" ;
55import type { ExecutionEvent , ExecutionObserver } from "@executor-js/sdk" ;
66import { makeTestConfig } from "@executor-js/sdk/testing" ;
77import type { CodeExecutor , ExecuteResult } from "@executor-js/codemode-core" ;
@@ -14,8 +14,32 @@ const emptyPlugin = definePlugin(() => ({
1414 staticSources : ( ) => [ ] ,
1515} ) ) ;
1616
17+ const approvalPlugin = definePlugin ( ( ) => ( {
18+ id : "observer-approval-test" as const ,
19+ storage : ( ) => ( { } ) ,
20+ staticSources : ( ) => [
21+ {
22+ id : "approval.ctl" ,
23+ kind : "control" as const ,
24+ name : "Approval Ctl" ,
25+ tools : [
26+ tool ( {
27+ name : "run" ,
28+ description : "Requires approval" ,
29+ annotations : { requiresApproval : true } as const ,
30+ inputSchema : Schema . toStandardSchemaV1 ( Schema . toStandardJSONSchemaV1 ( Schema . Struct ( { } ) ) ) ,
31+ execute : ( ) => Effect . succeed ( "ran" ) ,
32+ } ) ,
33+ ] ,
34+ } ,
35+ ] ,
36+ } ) ) ;
37+
1738const makeExecutor = ( ) => createExecutor ( makeTestConfig ( { plugins : [ emptyPlugin ( ) ] as const } ) ) ;
1839
40+ const makeApprovalExecutor = ( ) =>
41+ createExecutor ( makeTestConfig ( { plugins : [ approvalPlugin ( ) ] as const } ) ) ;
42+
1943// A code executor that issues one builtin tool call (tools.search) and then
2044// completes, enough to exercise the full event sequence.
2145const toolCallingExecutor : CodeExecutor = {
@@ -25,6 +49,13 @@ const toolCallingExecutor: CodeExecutor = {
2549 . pipe ( Effect . as ( { result : "ok" , logs : [ ] } satisfies ExecuteResult ) , Effect . orDie ) ,
2650} ;
2751
52+ const approvalCallingExecutor : CodeExecutor = {
53+ execute : ( _code , invoker ) =>
54+ invoker
55+ . invoke ( { path : "approval.ctl.run" , args : { } } )
56+ . pipe ( Effect . as ( { result : "ok" , logs : [ ] } satisfies ExecuteResult ) , Effect . orDie ) ,
57+ } ;
58+
2859const collectingObserver = ( ) => {
2960 const events : ExecutionEvent [ ] = [ ] ;
3061 const observer : ExecutionObserver = {
@@ -70,6 +101,35 @@ describe("execution engine observer emission", () => {
70101 } ) ,
71102 ) ;
72103
104+ it . effect ( "emits inline interaction events when execute handles elicitation" , ( ) =>
105+ Effect . gen ( function * ( ) {
106+ const executor = yield * makeApprovalExecutor ( ) ;
107+ const { events, observer } = collectingObserver ( ) ;
108+ const engine = createExecutionEngine ( {
109+ executor,
110+ codeExecutor : approvalCallingExecutor ,
111+ observer,
112+ } ) ;
113+
114+ const result = yield * engine . execute ( "noop" , {
115+ trigger : { kind : "test" } ,
116+ onElicitation : ( ) => Effect . succeed ( ElicitationResponse . make ( { action : "accept" } ) ) ,
117+ } ) ;
118+ expect ( result . result ) . toBe ( "ok" ) ;
119+
120+ const started = events . find ( ( e ) => Predicate . isTagged ( e , "ExecutionStarted" ) ) ;
121+ const interactionStarted = events . find ( ( e ) => Predicate . isTagged ( e , "InteractionStarted" ) ) ;
122+ const interactionResolved = events . find ( ( e ) => Predicate . isTagged ( e , "InteractionResolved" ) ) ;
123+
124+ expect ( interactionStarted ?. executionId ) . toBe ( started ?. executionId ) ;
125+ expect ( interactionResolved ?. executionId ) . toBe ( started ?. executionId ) ;
126+ expect ( interactionResolved ?. interactionId ) . toBe ( interactionStarted ?. interactionId ) ;
127+ expect ( interactionStarted ?. context . request . message ) . toContain ( "approval" ) ;
128+ expect ( interactionResolved ?. status ) . toBe ( "accepted" ) ;
129+ expect ( interactionResolved ?. response ?. action ) . toBe ( "accept" ) ;
130+ } ) ,
131+ ) ;
132+
73133 it . effect ( "does nothing observable when no observer is configured" , ( ) =>
74134 Effect . gen ( function * ( ) {
75135 const executor = yield * makeExecutor ( ) ;
0 commit comments