1- import { afterEach , describe , expect , test } from "bun:test"
1+ import { afterEach , describe , expect } from "bun:test"
22import type { UpgradeWebSocket } from "hono/ws"
33import { Effect } from "effect"
44import { Flag } from "@opencode-ai/core/flag/flag"
@@ -11,7 +11,8 @@ import { MessageID, PartID } from "../../src/session/schema"
1111import { Session } from "@/session/session"
1212import * as Log from "@opencode-ai/core/util/log"
1313import { resetDatabase } from "../fixture/db"
14- import { tmpdir } from "../fixture/fixture"
14+ import { provideInstance , tmpdir } from "../fixture/fixture"
15+ import { it } from "../lib/effect"
1516
1617void Log . init ( { print : false } )
1718
@@ -23,70 +24,63 @@ function app(experimental: boolean) {
2324 return InstanceRoutes ( websocket )
2425}
2526
26- function runSession < A , E > ( fx : Effect . Effect < A , E , Session . Service > ) {
27- return Effect . runPromise ( fx . pipe ( Effect . provide ( Session . defaultLayer ) ) )
28- }
29-
3027function pathFor ( path : string , params : Record < string , string > ) {
3128 return Object . entries ( params ) . reduce ( ( result , [ key , value ] ) => result . replace ( `:${ key } ` , value ) , path )
3229}
3330
34- async function seedSessions ( directory : string ) {
35- return await Instance . provide ( {
36- directory,
37- fn : ( ) =>
38- runSession (
39- Effect . gen ( function * ( ) {
40- const svc = yield * Session . Service
41- const parent = yield * svc . create ( { title : "parent" } )
42- yield * svc . create ( { title : "child" , parentID : parent . id } )
43- const message = yield * svc . updateMessage ( {
44- id : MessageID . ascending ( ) ,
45- role : "user" ,
46- sessionID : parent . id ,
47- agent : "build" ,
48- model : { providerID : ProviderID . make ( "test" ) , modelID : ModelID . make ( "test" ) } ,
49- time : { created : Date . now ( ) } ,
50- } )
51- yield * svc . updatePart ( {
52- id : PartID . ascending ( ) ,
53- sessionID : parent . id ,
54- messageID : message . id ,
55- type : "text" ,
56- text : "hello" ,
57- } )
58- return { parent, message }
59- } ) ,
60- ) ,
31+ const seedSessions = Effect . gen ( function * ( ) {
32+ const svc = yield * Session . Service
33+ const parent = yield * svc . create ( { title : "parent" } )
34+ yield * svc . create ( { title : "child" , parentID : parent . id } )
35+ const message = yield * svc . updateMessage ( {
36+ id : MessageID . ascending ( ) ,
37+ role : "user" ,
38+ sessionID : parent . id ,
39+ agent : "build" ,
40+ model : { providerID : ProviderID . make ( "test" ) , modelID : ModelID . make ( "test" ) } ,
41+ time : { created : Date . now ( ) } ,
6142 } )
62- }
43+ yield * svc . updatePart ( {
44+ id : PartID . ascending ( ) ,
45+ sessionID : parent . id ,
46+ messageID : message . id ,
47+ type : "text" ,
48+ text : "hello" ,
49+ } )
50+ return { parent, message }
51+ } )
6352
64- async function readJson (
65- label : string ,
66- app : ReturnType < typeof InstanceRoutes > ,
67- directory : string ,
68- path : string ,
69- headers : HeadersInit ,
53+ function withTmp < A , E , R > (
54+ options : Parameters < typeof tmpdir > [ 0 ] ,
55+ fn : ( tmp : Awaited < ReturnType < typeof tmpdir > > ) => Effect . Effect < A , E , R > ,
7056) {
71- const response = await Instance . provide ( {
72- directory,
73- fn : ( ) => app . request ( path , { headers } ) ,
57+ return Effect . acquireRelease (
58+ Effect . promise ( ( ) => tmpdir ( options ) ) ,
59+ ( tmp ) => Effect . promise ( ( ) => tmp [ Symbol . asyncDispose ] ( ) ) ,
60+ ) . pipe ( Effect . flatMap ( ( tmp ) => fn ( tmp ) . pipe ( provideInstance ( tmp . path ) ) ) )
61+ }
62+
63+ function readJson ( label : string , app : ReturnType < typeof InstanceRoutes > , path : string , headers : HeadersInit ) {
64+ return Effect . promise ( async ( ) => {
65+ const response = await app . request ( path , { headers } )
66+ if ( response . status !== 200 ) throw new Error ( `${ label } returned ${ response . status } : ${ await response . text ( ) } ` )
67+ return await response . json ( )
7468 } )
75- if ( response . status !== 200 ) throw new Error ( `${ label } returned ${ response . status } : ${ await response . text ( ) } ` )
76- return await response . json ( )
7769}
7870
79- async function expectJsonParity ( input : {
71+ function expectJsonParity ( input : {
8072 label : string
8173 legacy : ReturnType < typeof InstanceRoutes >
8274 httpapi : ReturnType < typeof InstanceRoutes >
83- directory : string
8475 path : string
8576 headers : HeadersInit
8677} ) {
87- const legacy = await readJson ( input . label , input . legacy , input . directory , input . path , input . headers )
88- const httpapi = await readJson ( input . label , input . httpapi , input . directory , input . path , input . headers )
89- expect ( { label : input . label , body : httpapi } ) . toEqual ( { label : input . label , body : legacy } )
78+ return Effect . gen ( function * ( ) {
79+ const legacy = yield * readJson ( input . label , input . legacy , input . path , input . headers )
80+ const httpapi = yield * readJson ( input . label , input . httpapi , input . path , input . headers )
81+ expect ( { label : input . label , body : httpapi } ) . toEqual ( { label : input . label , body : legacy } )
82+ return httpapi
83+ } )
9084}
9185
9286afterEach ( async ( ) => {
@@ -96,32 +90,78 @@ afterEach(async () => {
9690} )
9791
9892describe ( "HttpApi JSON parity" , ( ) => {
99- test ( "matches legacy JSON shape for session read endpoints" , async ( ) => {
100- await using tmp = await tmpdir ( { git : true , config : { formatter : false , lsp : false } } )
101- const headers = { "x-opencode-directory" : tmp . path }
102- const seeded = await seedSessions ( tmp . path )
103- const legacy = app ( false )
104- const httpapi = app ( true )
93+ it . live (
94+ "matches legacy JSON shape for session read endpoints" ,
95+ withTmp ( { git : true , config : { formatter : false , lsp : false } } , ( tmp ) =>
96+ Effect . gen ( function * ( ) {
97+ const headers = { "x-opencode-directory" : tmp . path }
98+ const seeded = yield * seedSessions . pipe ( Effect . provide ( Session . defaultLayer ) )
99+ const legacy = app ( false )
100+ const httpapi = app ( true )
105101
106- await [
107- { label : "session.list roots" , path : `${ SessionPaths . list } ?roots=true` , headers } ,
108- { label : "session.list all" , path : SessionPaths . list , headers } ,
109- { label : "session.get" , path : pathFor ( SessionPaths . get , { sessionID : seeded . parent . id } ) , headers } ,
110- { label : "session.children" , path : pathFor ( SessionPaths . children , { sessionID : seeded . parent . id } ) , headers } ,
111- { label : "session.messages" , path : pathFor ( SessionPaths . messages , { sessionID : seeded . parent . id } ) , headers } ,
112- {
113- label : "session.message" ,
114- path : pathFor ( SessionPaths . message , { sessionID : seeded . parent . id , messageID : seeded . message . id } ) ,
115- headers,
116- } ,
117- {
118- label : "experimental.session" ,
119- path : `${ ExperimentalPaths . session } ?${ new URLSearchParams ( { directory : tmp . path , limit : "10" } ) } ` ,
120- headers,
121- } ,
122- ] . reduce (
123- ( promise , input ) => promise . then ( ( ) => expectJsonParity ( { ...input , legacy, httpapi, directory : tmp . path } ) ) ,
124- Promise . resolve ( ) ,
125- )
126- } )
102+ const rootsFalse = yield * expectJsonParity ( {
103+ label : "session.list roots false" ,
104+ legacy,
105+ httpapi,
106+ path : `${ SessionPaths . list } ?roots=false` ,
107+ headers,
108+ } )
109+ expect ( ( rootsFalse as Session . Info [ ] ) . map ( ( session ) => session . id ) ) . toContain ( seeded . parent . id )
110+ expect ( ( rootsFalse as Session . Info [ ] ) . length ) . toBe ( 2 )
111+
112+ const experimentalRootsFalse = yield * expectJsonParity ( {
113+ label : "experimental.session roots false" ,
114+ legacy,
115+ httpapi,
116+ path : `${ ExperimentalPaths . session } ?${ new URLSearchParams ( { directory : tmp . path , limit : "10" , roots : "false" } ) } ` ,
117+ headers,
118+ } )
119+ expect ( ( experimentalRootsFalse as Session . GlobalInfo [ ] ) . length ) . toBe ( 2 )
120+
121+ const experimentalArchivedFalse = yield * expectJsonParity ( {
122+ label : "experimental.session archived false" ,
123+ legacy,
124+ httpapi,
125+ path : `${ ExperimentalPaths . session } ?${ new URLSearchParams ( { directory : tmp . path , limit : "10" , archived : "false" } ) } ` ,
126+ headers,
127+ } )
128+ expect ( ( experimentalArchivedFalse as Session . GlobalInfo [ ] ) . length ) . toBe ( 2 )
129+
130+ yield * Effect . forEach (
131+ [
132+ { label : "session.list roots" , path : `${ SessionPaths . list } ?roots=true` , headers } ,
133+ { label : "session.list all" , path : SessionPaths . list , headers } ,
134+ { label : "session.get" , path : pathFor ( SessionPaths . get , { sessionID : seeded . parent . id } ) , headers } ,
135+ {
136+ label : "session.children" ,
137+ path : pathFor ( SessionPaths . children , { sessionID : seeded . parent . id } ) ,
138+ headers,
139+ } ,
140+ {
141+ label : "session.messages" ,
142+ path : pathFor ( SessionPaths . messages , { sessionID : seeded . parent . id } ) ,
143+ headers,
144+ } ,
145+ {
146+ label : "session.messages empty before" ,
147+ path : `${ pathFor ( SessionPaths . messages , { sessionID : seeded . parent . id } ) } ?before=` ,
148+ headers,
149+ } ,
150+ {
151+ label : "session.message" ,
152+ path : pathFor ( SessionPaths . message , { sessionID : seeded . parent . id , messageID : seeded . message . id } ) ,
153+ headers,
154+ } ,
155+ {
156+ label : "experimental.session" ,
157+ path : `${ ExperimentalPaths . session } ?${ new URLSearchParams ( { directory : tmp . path , limit : "10" } ) } ` ,
158+ headers,
159+ } ,
160+ ] ,
161+ ( input ) => expectJsonParity ( { ...input , legacy, httpapi } ) ,
162+ { concurrency : 1 } ,
163+ )
164+ } ) ,
165+ ) ,
166+ )
127167} )
0 commit comments