@@ -18,6 +18,7 @@ import {
1818 type OrchestrationV2ThreadShell ,
1919} from "@t3tools/contracts" ;
2020import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions" ;
21+ import * as Deferred from "effect/Deferred" ;
2122import * as Effect from "effect/Effect" ;
2223import * as TestClock from "effect/testing/TestClock" ;
2324import * as Layer from "effect/Layer" ;
@@ -48,6 +49,7 @@ import * as ServerSettings from "../../serverSettings.ts";
4849import * as McpHttpServer from "../McpHttpServer.ts" ;
4950import * as McpInvocationContext from "../McpInvocationContext.ts" ;
5051import * as McpToolAccessTestkit from "../McpToolAccess.testkit.ts" ;
52+ import * as AnalyticsService from "../../telemetry/AnalyticsService.ts" ;
5153import { dispatchFailure } from "../threadAccess.ts" ;
5254import * as EnvironmentHandlers from "./environment/handlers.ts" ;
5355import * as AttachmentHandlers from "./attachment/handlers.ts" ;
@@ -287,6 +289,61 @@ it.effect("returns an HTML render reference that Codex and Claude tool rows both
287289 ) ,
288290) ;
289291
292+ it . effect ( "records only failed calls of tools that hand off no work" , ( ) => {
293+ // Recording runs detached from the tool response, so the first event is awaited.
294+ const recorded = Deferred . makeUnsafe < { event : string ; properties : object } > ( ) ;
295+ return Effect . gen ( function * ( ) {
296+ const server = yield * McpServer . McpServer ;
297+ const organize = ( capabilities : McpInvocationContext . McpInvocationScope [ "capabilities" ] ) =>
298+ server . callTool ( { name : "t3_thread_organize" , arguments : { action : "pin" } } ) . pipe (
299+ Effect . provideService ( McpInvocationContext . McpInvocationContext , {
300+ ...scope ,
301+ capabilities,
302+ } ) ,
303+ Effect . provideService ( McpSchema . McpServerClient , client ) ,
304+ ) ;
305+ expect ( ( yield * organize ( scope . capabilities ) ) . isError ) . not . toBe ( true ) ;
306+ expect ( declaredFailure ( yield * organize ( new Set < never > ( ) ) ) ) . toMatchObject ( {
307+ code : "capability_denied" ,
308+ } ) ;
309+ // The successful call ran first, so it would be the first event if it were recorded.
310+ expect ( yield * Deferred . await ( recorded ) ) . toMatchObject ( {
311+ event : "mcp.tool.invoked" ,
312+ properties : { tool : "t3_thread_organize" , outcome : "error" } ,
313+ } ) ;
314+ } ) . pipe (
315+ Effect . provide (
316+ layerThreadToolkit . pipe (
317+ Layer . provideMerge ( McpServer . McpServer . layer ) ,
318+ Layer . provide ( NodeCrypto . layer ) ,
319+ Layer . provide (
320+ Layer . mock ( ThreadManagement . ThreadManagementService ) ( {
321+ getThreadShell : ( id ) => Effect . succeed ( McpToolAccessTestkit . liveThreadShell ( id ) ) ,
322+ getProjectThreadRecords : ( { threadId : id } ) =>
323+ Effect . succeed (
324+ McpToolAccessTestkit . idleThreadProjection ( McpToolAccessTestkit . liveThreadShell ( id ) ) ,
325+ ) ,
326+ dispatch : ( ) => Effect . succeed ( { sequence : 1 } as never ) ,
327+ } ) ,
328+ ) ,
329+ Layer . provide (
330+ Layer . succeed (
331+ AnalyticsService . AnalyticsService ,
332+ AnalyticsService . AnalyticsService . of ( {
333+ record : ( event , properties ) =>
334+ Deferred . succeed ( recorded , { event, properties : properties ?? { } } ) . pipe (
335+ Effect . asVoid ,
336+ ) ,
337+ flush : Effect . void ,
338+ recordOwners : ( ) => Effect . void ,
339+ } ) ,
340+ ) ,
341+ ) ,
342+ ) ,
343+ ) ,
344+ ) ;
345+ } ) ;
346+
290347it . effect ( "keeps unexpected handler defects private through the production registration" , ( ) =>
291348 Effect . gen ( function * ( ) {
292349 const server = yield * McpServer . McpServer ;
0 commit comments