1+ import { DurableObject } from "cloudflare:workers" ;
12import { describe , expect , it } from "@effect/vitest" ;
3+ import { Effect } from "effect" ;
24
35import {
46 McpExecutionOwnerDirectoryDO ,
7+ mcpExecutionOwnerDirectoryFromNamespace ,
8+ type McpExecutionOwnerDirectoryNamespace ,
59 type McpExecutionOwnerRecord ,
610} from "./execution-owner-directory" ;
711
8- class FakeStorage {
12+ class FakeStorage implements DurableObjectStorage {
913 private readonly values = new Map < string , unknown > ( ) ;
14+ readonly sql = { } as DurableObjectStorage [ "sql" ] ;
15+ readonly kv = { } as DurableObjectStorage [ "kv" ] ;
1016 alarmAt : number | null = null ;
1117
12- async get < T > ( key : string ) : Promise < T | undefined > {
13- return this . values . get ( key ) as T | undefined ;
18+ async get < T > ( key : string ) : Promise < T | undefined > ;
19+ async get < T > ( keys : string [ ] ) : Promise < Map < string , T > > ;
20+ async get < T > ( keyOrKeys : string | string [ ] ) : Promise < T | undefined | Map < string , T > > {
21+ if ( Array . isArray ( keyOrKeys ) ) {
22+ return new Map ( keyOrKeys . map ( ( key ) => [ key , this . values . get ( key ) as T ] ) ) ;
23+ }
24+ return this . values . get ( keyOrKeys ) as T | undefined ;
1425 }
1526
16- async put < T > ( key : string , value : T ) : Promise < void > {
17- this . values . set ( key , value ) ;
27+ async put < T > ( key : string , value : T ) : Promise < void > ;
28+ async put < T > ( entries : Record < string , T > | Map < string , T > ) : Promise < void > ;
29+ async put < T > (
30+ keyOrEntries : string | Record < string , T > | Map < string , T > ,
31+ value ?: T ,
32+ ) : Promise < void > {
33+ if ( typeof keyOrEntries === "string" ) {
34+ this . values . set ( keyOrEntries , value ) ;
35+ return ;
36+ }
37+ const entries =
38+ keyOrEntries instanceof Map ? keyOrEntries . entries ( ) : Object . entries ( keyOrEntries ) ;
39+ for ( const [ key , entry ] of entries ) this . values . set ( key , entry ) ;
1840 }
1941
20- async delete ( key : string ) : Promise < boolean > {
21- return this . values . delete ( key ) ;
42+ async delete ( key : string ) : Promise < boolean > ;
43+ async delete ( keys : string [ ] ) : Promise < number > ;
44+ async delete ( keyOrKeys : string | string [ ] ) : Promise < boolean | number > {
45+ if ( ! Array . isArray ( keyOrKeys ) ) return this . values . delete ( keyOrKeys ) ;
46+ let deleted = 0 ;
47+ for ( const key of keyOrKeys ) {
48+ if ( this . values . delete ( key ) ) deleted += 1 ;
49+ }
50+ return deleted ;
51+ }
52+
53+ async list < T = unknown > ( ) : Promise < Map < string , T > > {
54+ return new Map ( [ ...this . values . entries ( ) ] . map ( ( [ key , value ] ) => [ key , value as T ] ) ) ;
55+ }
56+
57+ async deleteAll ( ) : Promise < void > {
58+ this . values . clear ( ) ;
59+ this . alarmAt = null ;
60+ }
61+
62+ transaction < T > ( _closure : ( txn : DurableObjectTransaction ) => Promise < T > ) : Promise < T > {
63+ return Promise . resolve ( undefined as T ) ;
64+ }
65+
66+ transactionSync < T > ( _closure : ( ) => T ) : T {
67+ return undefined as T ;
68+ }
69+
70+ async sync ( ) : Promise < void > { }
71+
72+ async getAlarm ( ) : Promise < number | null > {
73+ return this . alarmAt ;
74+ }
75+
76+ async getCurrentBookmark ( ) : Promise < string > {
77+ return "test-bookmark" ;
78+ }
79+
80+ async getBookmarkForTime ( _timestamp : number | Date ) : Promise < string > {
81+ return "test-bookmark" ;
82+ }
83+
84+ onNextSessionRestoreBookmark ( _bookmark : string ) : Promise < string > {
85+ return Promise . resolve ( "test-bookmark" ) ;
2286 }
2387
2488 async setAlarm ( scheduledTime : number | Date ) : Promise < void > {
@@ -30,14 +94,93 @@ class FakeStorage {
3094 }
3195}
3296
97+ class FakeDurableObjectState implements DurableObjectState {
98+ readonly id : DurableObjectId ;
99+ readonly props : unknown = undefined ;
100+ readonly facets = { } as DurableObjectState [ "facets" ] ;
101+ readonly ctx = this ;
102+ private readonly waitUntilPromises : Promise < unknown > [ ] = [ ] ;
103+
104+ constructor (
105+ name : string ,
106+ readonly storage : FakeStorage ,
107+ ) {
108+ const id : Pick < DurableObjectId , "equals" | "name" | "toString" > = {
109+ equals : ( other : DurableObjectId ) => other . toString ( ) === name ,
110+ name,
111+ toString : ( ) => name ,
112+ } ;
113+ this . id = id as DurableObjectId ;
114+ }
115+
116+ waitUntil ( promise : Promise < unknown > ) : void {
117+ this . waitUntilPromises . push ( promise ) ;
118+ }
119+
120+ blockConcurrencyWhile < T > ( callback : ( ) => Promise < T > ) : Promise < T > {
121+ return callback ( ) ;
122+ }
123+
124+ acceptWebSocket ( _ws : WebSocket , _tags ?: string [ ] ) : void { }
125+
126+ getWebSockets ( _tag ?: string ) : WebSocket [ ] {
127+ return [ ] ;
128+ }
129+
130+ getTags ( _ws : WebSocket ) : string [ ] {
131+ return [ ] ;
132+ }
133+
134+ setWebSocketAutoResponse ( _pair ?: WebSocketRequestResponsePair ) : void { }
135+
136+ getWebSocketAutoResponse ( ) : WebSocketRequestResponsePair | null {
137+ return null ;
138+ }
139+
140+ getWebSocketAutoResponseTimestamp ( _ws : WebSocket ) : Date | null {
141+ return null ;
142+ }
143+
144+ setHibernatableWebSocketEventTimeout ( _timeoutMs ?: number ) : void { }
145+
146+ getHibernatableWebSocketEventTimeout ( ) : number | null {
147+ return null ;
148+ }
149+
150+ abort ( _reason ?: string ) : void { }
151+ }
152+
33153const makeDirectory = ( ) => {
34154 const storage = new FakeStorage ( ) ;
35- const directory = new McpExecutionOwnerDirectoryDO ( {
36- storage,
37- } ) ;
155+ const directory = new McpExecutionOwnerDirectoryDO (
156+ new FakeDurableObjectState ( "execution-owner" , storage ) ,
157+ { } ,
158+ ) ;
38159 return { directory, storage } ;
39160} ;
40161
162+ class FakeDirectoryNamespace implements McpExecutionOwnerDirectoryNamespace < string > {
163+ private readonly directories = new Map < string , McpExecutionOwnerDirectoryDO > ( ) ;
164+
165+ idFromName ( name : string ) : string {
166+ return name ;
167+ }
168+
169+ get ( id : string ) : unknown {
170+ let directory = this . directories . get ( id ) ;
171+ if ( ! directory ) {
172+ directory = makeDirectory ( ) . directory ;
173+ this . directories . set ( id , directory ) ;
174+ }
175+ if ( ! ( directory instanceof DurableObject ) ) return { } ;
176+ return {
177+ put : ( entry : McpExecutionOwnerRecord ) => directory . put ( entry ) ,
178+ get : ( executionId : string ) => directory . get ( executionId ) ,
179+ delete : ( executionId : string ) => directory . delete ( executionId ) ,
180+ } ;
181+ }
182+ }
183+
41184const record = ( input ?: Partial < McpExecutionOwnerRecord > ) : McpExecutionOwnerRecord => ( {
42185 executionId : "exec_1" ,
43186 owner : { sessionId : "session_a" } ,
@@ -49,6 +192,11 @@ const record = (input?: Partial<McpExecutionOwnerRecord>): McpExecutionOwnerReco
49192} ) ;
50193
51194describe ( "McpExecutionOwnerDirectoryDO" , ( ) => {
195+ it ( "exports a DurableObject subclass for RPC method dispatch" , ( ) => {
196+ expect ( Object . getPrototypeOf ( McpExecutionOwnerDirectoryDO ) ) . toBe ( DurableObject ) ;
197+ expect ( makeDirectory ( ) . directory ) . toBeInstanceOf ( DurableObject ) ;
198+ } ) ;
199+
52200 it ( "stores and reads an unexpired execution owner record" , async ( ) => {
53201 const { directory, storage } = makeDirectory ( ) ;
54202 const entry = record ( ) ;
@@ -81,4 +229,18 @@ describe("McpExecutionOwnerDirectoryDO", () => {
81229 expect ( await storage . get ( "owner" ) ) . toBeUndefined ( ) ;
82230 expect ( storage . alarmAt ) . toBeNull ( ) ;
83231 } ) ;
232+
233+ it ( "stores, reads, and deletes records through the namespace RPC path" , async ( ) => {
234+ const directory = mcpExecutionOwnerDirectoryFromNamespace ( new FakeDirectoryNamespace ( ) ) ;
235+ const entry = record ( { executionId : "exec_rpc" } ) ;
236+
237+ expect ( directory ) . not . toBeNull ( ) ;
238+ await Effect . runPromise ( directory ! . put ( entry ) ) ;
239+
240+ expect ( await Effect . runPromise ( directory ! . get ( "exec_rpc" ) ) ) . toEqual ( entry ) ;
241+
242+ await Effect . runPromise ( directory ! . delete ( "exec_rpc" ) ) ;
243+
244+ expect ( await Effect . runPromise ( directory ! . get ( "exec_rpc" ) ) ) . toBeNull ( ) ;
245+ } ) ;
84246} ) ;
0 commit comments