@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
33import { createServer , type IncomingMessage } from "node:http" ;
44
55import { expect } from "@effect/vitest" ;
6- import { Effect } from "effect" ;
6+ import { Effect , Schema } from "effect" ;
77import { composePluginApi } from "@executor-js/api/server" ;
88import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api" ;
99import {
@@ -16,7 +16,23 @@ import {
1616import { decodeToolSearch } from "./support/search-invoke" ;
1717
1818import { scenario } from "../src/scenario" ;
19- import { Api , Mcp , Target } from "../src/services" ;
19+ import { Api , Browser , Mcp , Target } from "../src/services" ;
20+ import { visit , settle } from "../src/surfaces/browser" ;
21+
22+ const decodeInventory = Schema . decodeUnknownSync (
23+ Schema . Struct ( {
24+ structuredContent : Schema . Struct ( {
25+ items : Schema . Array (
26+ Schema . Struct ( {
27+ integration : Schema . String ,
28+ owner : Schema . String ,
29+ connection : Schema . String ,
30+ } ) ,
31+ ) ,
32+ total : Schema . Number ,
33+ } ) ,
34+ } ) ,
35+ ) ;
2036
2137const api = composePluginApi ( [ openApiHttpPlugin ( ) ] as const ) ;
2238
@@ -133,6 +149,7 @@ scenario(
133149 Effect . gen ( function * ( ) {
134150 const target = yield * Target ;
135151 const mcp = yield * Mcp ;
152+ const browser = yield * Browser ;
136153 const { client : makeClient } = yield * Api ;
137154
138155 const identity = yield * target . newIdentity ( ) ;
@@ -170,12 +187,35 @@ scenario(
170187 } ) ;
171188 }
172189
190+ yield * browser . session ( identity , async ( { page, step } ) => {
191+ await step ( "Choose Search and invoke in the Connect card" , async ( ) => {
192+ await visit ( page , "/" ) ;
193+ await page . getByRole ( "button" , { name : "Advanced" } ) . click ( ) ;
194+ await page . getByRole ( "switch" , { name : "Search and invoke" } ) . check ( ) ;
195+ await settle ( page ) ;
196+ expect ( await page . locator ( "code" ) . first ( ) . innerText ( ) ) . toContain ( "mode=passthrough" ) ;
197+ expect (
198+ await page
199+ . getByText (
200+ "Discover connected accounts with integrations and read the guide with skills." ,
201+ { exact : false } ,
202+ )
203+ . isVisible ( ) ,
204+ ) . toBe ( true ) ;
205+ } ) ;
206+ } ) ;
207+
173208 const codemode = mcp . session ( identity ) ;
174209 expect ( yield * codemode . listTools ( ) ) . toContain ( "execute" ) ;
175210
176211 const passthrough = mcp . session ( identity , { mode : "passthrough" } ) ;
177212 const described = yield * passthrough . describeTools ( ) ;
178- expect ( described . map ( ( tool ) => tool . name ) . sort ( ) ) . toEqual ( [ "invoke" , "search" ] ) ;
213+ expect ( described . map ( ( tool ) => tool . name ) . sort ( ) ) . toEqual ( [
214+ "integrations" ,
215+ "invoke" ,
216+ "search" ,
217+ "skills" ,
218+ ] ) ;
179219 expect ( described . find ( ( tool ) => tool . name === "search" ) ?. annotations ) . toMatchObject ( {
180220 readOnlyHint : true ,
181221 destructiveHint : false ,
@@ -184,9 +224,42 @@ scenario(
184224 readOnlyHint : false ,
185225 destructiveHint : true ,
186226 } ) ;
227+ const inventory = yield * passthrough . call ( "integrations" , {
228+ integration : slug ,
229+ owner : "org" ,
230+ } ) ;
231+ expect ( inventory . ok ) . toBe ( true ) ;
232+ const decodedInventory = decodeInventory ( inventory . raw ) . structuredContent ;
233+ expect ( decodedInventory ) . toEqual ( {
234+ items : [ { integration : slug , owner : "org" , connection : "main" } ] ,
235+ total : 1 ,
236+ } ) ;
237+ expect ( inventory . text ) . not . toContain ( `tok_${ slug } ` ) ;
238+ const skills = yield * passthrough . call ( "skills" , { } ) ;
239+ expect ( skills . ok ) . toBe ( true ) ;
240+ expect ( skills . text ) . toContain ( "search-invoke" ) ;
241+ const guide = yield * passthrough . call ( "skills" , { name : "search-invoke" } ) ;
242+ expect ( guide . ok ) . toBe ( true ) ;
243+ expect ( guide . text ) . toContain ( "integrations({})" ) ;
244+ expect ( ( yield * passthrough . call ( "skills" , { name : "execute" } ) ) . ok ) . toBe ( false ) ;
187245 const found = decodeToolSearch (
188- ( yield * passthrough . call ( "search" , { query : slug } ) ) . raw ,
246+ ( yield * passthrough . call ( "search" , {
247+ query : "notes" ,
248+ integration : slug ,
249+ owner : "org" ,
250+ connection : "main" ,
251+ } ) ) . raw ,
252+ ) . structuredContent ;
253+ expect ( found . items . every ( ( tool ) => tool . integration === slug ) ) . toBe ( true ) ;
254+ const missingAccount = decodeToolSearch (
255+ ( yield * passthrough . call ( "search" , {
256+ query : "notes" ,
257+ integration : slug ,
258+ owner : "user" ,
259+ connection : "main" ,
260+ } ) ) . raw ,
189261 ) . structuredContent ;
262+ expect ( missingAccount . items ) . toEqual ( [ ] ) ;
190263 const listDef = found . items . find ( ( tool ) => tool . id . endsWith ( ".listNotes" ) ) ;
191264 const createDef = found . items . find ( ( tool ) => tool . id . endsWith ( ".createNote" ) ) ;
192265 if ( ! listDef || ! createDef ) return yield * Effect . die ( "Search omitted notes operations" ) ;
0 commit comments