@@ -4,6 +4,7 @@ import { expect, it } from "@effect/vitest";
44import * as Effect from "effect/Effect" ;
55import * as FileSystem from "effect/FileSystem" ;
66import * as Layer from "effect/Layer" ;
7+ import * as PlatformError from "effect/PlatformError" ;
78import { makeClaudeHistoryStore , makePiHistoryStore } from "./JsonlHistoryStore.ts" ;
89import { makeLocalHistory } from "./LocalHistory.ts" ;
910
@@ -89,8 +90,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("local JSONL hist
8990 . read ( store , { operation : "read" , ref : first . matches [ 0 ] ! . ref } )
9091 . pipe ( Effect . flip ) ) . reason ,
9192 ) . toBe ( "changed" ) ;
92- const restarted = yield * history . search ( store , { ...paged , cursor : second . nextCursor ! } ) ;
93- expect ( restarted . matches [ 0 ] ?. itemId ) . toBe ( "new-0" ) ;
93+ expect (
94+ ( yield * history . search ( store , { ...paged , cursor : second . nextCursor ! } ) . pipe ( Effect . flip ) )
95+ . reason ,
96+ ) . toBe ( "changed" ) ;
9497 expect (
9598 ( yield * store
9699 . loadThread ( { key : "../../outside.jsonl" , version : "" , updatedAt : 0 } )
@@ -135,6 +138,80 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("local JSONL hist
135138 } ) . pipe ( Effect . scoped ) ,
136139 ) ;
137140
141+ it . effect ( "isolates unreadable entries and retries incomplete listings after recovery" , ( ) =>
142+ Effect . gen ( function * ( ) {
143+ const fs = yield * FileSystem . FileSystem ;
144+ const root = yield * fs . makeTempDirectoryScoped ( ) ;
145+ const directory = `${ root } /projects` ;
146+ for ( const project of [ "good" , "blocked" ] ) {
147+ yield * fs . makeDirectory ( `${ directory } /${ project } ` , { recursive : true } ) ;
148+ yield * fs . writeFileString (
149+ `${ directory } /${ project } /session.jsonl` ,
150+ JSON . stringify ( { type : "user" , uuid : project , message : { content : "needle" } } ) + "\n" ,
151+ ) ;
152+ }
153+ const history = yield * makeLocalHistory ;
154+ const allKeys = [ "blocked/session.jsonl" , "good/session.jsonl" ] ;
155+ for ( const failure of [
156+ { method : "stat" , path : `${ directory } /blocked` , reason : "NotFound" } ,
157+ { method : "readDirectory" , path : `${ directory } /blocked` , reason : "PermissionDenied" } ,
158+ { method : "stat" , path : `${ directory } /blocked/session.jsonl` , reason : "NotFound" } ,
159+ { method : "stat" , path : `${ directory } /blocked/session.jsonl` , reason : "PermissionDenied" } ,
160+ ] as const ) {
161+ let unavailable = true ;
162+ const error = PlatformError . systemError ( {
163+ _tag : failure . reason ,
164+ module : "FileSystem" ,
165+ method : failure . method ,
166+ pathOrDescriptor : failure . path ,
167+ } ) ;
168+ const partialFs = FileSystem . make ( {
169+ ...fs ,
170+ stat : ( path ) =>
171+ unavailable && failure . method === "stat" && path === failure . path
172+ ? Effect . fail ( error )
173+ : fs . stat ( path ) ,
174+ readDirectory : ( path ) =>
175+ unavailable && failure . method === "readDirectory" && path === failure . path
176+ ? Effect . fail ( error )
177+ : fs . readDirectory ( path ) ,
178+ } ) ;
179+ const store = yield * makeClaudeHistoryStore ( root , history . cache ) . pipe (
180+ Effect . provideService ( FileSystem . FileSystem , partialFs ) ,
181+ ) ;
182+ expect ( ( yield * store . listThreads ) . map ( ( ref ) => ref . key ) ) . toEqual ( [ "good/session.jsonl" ] ) ;
183+ unavailable = false ;
184+ expect ( ( yield * store . listThreads ) . map ( ( ref ) => ref . key ) . toSorted ( ) ) . toEqual ( allKeys ) ;
185+ if ( failure . method === "stat" ) {
186+ unavailable = true ;
187+ expect ( ( yield * store . listThreads ) . map ( ( ref ) => ref . key ) ) . toEqual ( [ "good/session.jsonl" ] ) ;
188+ unavailable = false ;
189+ expect ( ( yield * store . listThreads ) . map ( ( ref ) => ref . key ) . toSorted ( ) ) . toEqual ( allKeys ) ;
190+ }
191+ }
192+ const inaccessibleRoot = yield * makeClaudeHistoryStore ( root , history . cache ) . pipe (
193+ Effect . provideService (
194+ FileSystem . FileSystem ,
195+ FileSystem . make ( {
196+ ...fs ,
197+ readDirectory : ( path ) =>
198+ path === directory
199+ ? Effect . fail (
200+ PlatformError . systemError ( {
201+ _tag : "PermissionDenied" ,
202+ module : "FileSystem" ,
203+ method : "readDirectory" ,
204+ pathOrDescriptor : path ,
205+ } ) ,
206+ )
207+ : fs . readDirectory ( path ) ,
208+ } ) ,
209+ ) ,
210+ ) ;
211+ expect ( ( yield * inaccessibleRoot . listThreads . pipe ( Effect . flip ) ) . reason ) . toBe ( "unavailable" ) ;
212+ } ) . pipe ( Effect . scoped ) ,
213+ ) ;
214+
138215 it . effect ( "searches sessions larger than the old allocation cap and verifies their source" , ( ) =>
139216 Effect . gen ( function * ( ) {
140217 const fs = yield * FileSystem . FileSystem ;
0 commit comments