1+ import * as DocumentText from "../provider/DocumentText.ts" ;
12// @effect -diagnostics nodeBuiltinImport:off
23import * as NodeFS from "node:fs" ;
34import * as NodePath from "node:path" ;
@@ -8,7 +9,9 @@ import { ChatAttachmentId } from "@t3tools/contracts";
89import * as Deferred from "effect/Deferred" ;
910import * as Effect from "effect/Effect" ;
1011import * as Fiber from "effect/Fiber" ;
12+ import * as FileSystem from "effect/FileSystem" ;
1113import * as Layer from "effect/Layer" ;
14+ import * as PlatformError from "effect/PlatformError" ;
1215import * as Schema from "effect/Schema" ;
1316import * as Stream from "effect/Stream" ;
1417
@@ -29,7 +32,7 @@ import {
2932
3033const layerTest = ServerSecretStore . layer . pipe (
3134 Layer . provideMerge ( ServerConfig . layerTest ( process . cwd ( ) , { prefix : "t3-attachment-upload-" } ) ) ,
32- Layer . provideMerge ( NodeServices . layer ) ,
35+ Layer . provideMerge ( DocumentText . layer . pipe ( Layer . provideMerge ( NodeServices . layer ) ) ) ,
3336) ;
3437
3538const uploadInput = {
@@ -52,6 +55,83 @@ const encodeLegacyAttachmentUploadClaims = Schema.encodeEffect(
5255) ;
5356
5457describe ( "AttachmentUpload" , ( ) => {
58+ it . effect . each ( [ false , true ] ) (
59+ "persists and replays immutable uploads (hard links unsupported: %s)" ,
60+ ( unsupported ) =>
61+ Effect . gen ( function * ( ) {
62+ const config = yield * ServerConfig . ServerConfig ;
63+ const fileSystem = yield * FileSystem . FileSystem ;
64+ const store = ( claims : Parameters < typeof storeAttachmentUpload > [ 0 ] , bytes : Uint8Array ) =>
65+ storeAttachmentUpload ( claims , bytes ) . pipe (
66+ Effect . provideService (
67+ FileSystem . FileSystem ,
68+ unsupported
69+ ? {
70+ ...fileSystem ,
71+ link : ( ) =>
72+ Effect . fail (
73+ PlatformError . systemError ( {
74+ _tag : "Unknown" ,
75+ module : "FileSystem" ,
76+ method : "link" ,
77+ } ) ,
78+ ) ,
79+ }
80+ : fileSystem ,
81+ ) ,
82+ ) ;
83+ const bytes = new TextEncoder ( ) . encode (
84+ JSON . stringify ( {
85+ cells : [ { cell_type : "markdown" , source : [ "# Cargo\n" , "Violet crates: 19" ] } ] ,
86+ } ) ,
87+ ) ;
88+ const issued = yield * issueAttachmentUploadUrl ( {
89+ type : "file" ,
90+ name : "cargo.ipynb" ,
91+ mimeType : "application/x-ipynb+json" ,
92+ sizeBytes : bytes . length ,
93+ } ) ;
94+ const claims = yield * validateAttachmentUploadToken (
95+ issued . relativeUrl . slice ( `${ ATTACHMENT_UPLOAD_ROUTE_PREFIX } /` . length ) ,
96+ ) ;
97+ if ( ! claims ) throw new Error ( "Expected upload claims" ) ;
98+ expect ( yield * store ( claims , bytes ) ) . toEqual ( { ok : true } ) ;
99+ expect ( yield * store ( claims , bytes ) ) . toEqual ( { ok : true } ) ;
100+ const path = NodePath . join ( config . attachmentsDir , `${ issued . attachmentId } .ipynb` ) ;
101+ expect ( NodeFS . readFileSync ( `${ path } .txt` , "utf8" ) ) . toContain ( "Violet crates: 19" ) ;
102+ const replacement = new TextEncoder ( ) . encode (
103+ new TextDecoder ( ) . decode ( bytes ) . replace ( "Violet crates: 19" , "Violet crates: 28" ) ,
104+ ) ;
105+ expect ( yield * store ( claims , replacement ) ) . toMatchObject ( {
106+ ok : false ,
107+ status : 409 ,
108+ } ) ;
109+ expect ( NodeFS . readFileSync ( path ) ) . toEqual ( Buffer . from ( bytes ) ) ;
110+ expect ( NodeFS . readFileSync ( `${ path } .txt` , "utf8" ) ) . toContain ( "Violet crates: 19" ) ;
111+ const claimed = yield * claimPendingAttachments ( {
112+ threadId : "document-thread" ,
113+ attachments : [
114+ {
115+ type : "file" ,
116+ id : ChatAttachmentId . make ( issued . attachmentId ) ,
117+ name : claims . name ,
118+ mimeType : claims . mimeType ,
119+ sizeBytes : bytes . length ,
120+ } ,
121+ ] ,
122+ } ) ;
123+ expect ( claimed . claimedPaths ) . toHaveLength ( 2 ) ;
124+ expect ( NodeFS . readFileSync ( claimed . claimedPaths [ 1 ] ! , "utf8" ) ) . toContain (
125+ "Violet crates: 19" ,
126+ ) ;
127+ yield * deletePendingAttachment ( issued . attachmentId ) ;
128+ expect ( NodeFS . existsSync ( path ) ) . toBe ( false ) ;
129+ expect ( NodeFS . existsSync ( `${ path } .txt` ) ) . toBe ( false ) ;
130+ expect ( NodeFS . existsSync ( claimed . claimedPaths [ 1 ] ! ) ) . toBe ( true ) ;
131+ yield * releaseClaimedAttachments ( claimed . claimedPaths ) ;
132+ expect ( NodeFS . existsSync ( claimed . claimedPaths [ 1 ] ! ) ) . toBe ( false ) ;
133+ } ) . pipe ( Effect . provide ( layerTest ) ) ,
134+ ) ;
55135 it . effect ( "rejects tampered and malformed upload tokens" , ( ) =>
56136 Effect . gen ( function * ( ) {
57137 const issued = yield * issueAttachmentUploadUrl ( uploadInput ) ;
0 commit comments