@@ -32,6 +32,7 @@ import {
3232} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
3333import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
3434import { runCli } from '@/lib/mothership/agent-cli/run-cli'
35+ import { handoffFileCreatorsInTx } from '@/lib/uploads/contexts/workspace/creator-handoff'
3536import {
3637 deleteWorkspaceFileVersion ,
3738 fetchWorkspaceFileBuffer ,
@@ -133,6 +134,110 @@ describe('workspace file version history in PostgreSQL', () => {
133134 expect ( await versionRows ( fixture . fileId ) ) . toEqual ( [ ] )
134135 } )
135136
137+ it ( 'keeps original attribution, bytes and revision through repeated creator handoff and a later edit' , async ( ) => {
138+ const fixture = await seedFile ( 'original' )
139+ const [ before ] = await db
140+ . select ( )
141+ . from ( workspaceFiles )
142+ . where ( eq ( workspaceFiles . id , fixture . fileId ) )
143+ for ( const successor of [ fixture . bobId , fixture . aliceId , fixture . bobId ] ) {
144+ await db . transaction ( ( tx ) =>
145+ handoffFileCreatorsInTx ( tx , eq ( workspaceFiles . id , fixture . fileId ) , successor )
146+ )
147+ }
148+ const [ after ] = await db
149+ . select ( )
150+ . from ( workspaceFiles )
151+ . where ( eq ( workspaceFiles . id , fixture . fileId ) )
152+ expect ( after ) . toMatchObject ( {
153+ key : before . key ,
154+ contentUpdatedAt : before . contentUpdatedAt ,
155+ uploadedAt : before . uploadedAt ,
156+ sizeBytes : before . sizeBytes ,
157+ userId : fixture . bobId ,
158+ } )
159+ expect ( await versionRows ( fixture . fileId ) ) . toHaveLength ( 1 )
160+ await updateWorkspaceFileContent (
161+ fixture . workspaceId ,
162+ fixture . fileId ,
163+ fixture . bobId ,
164+ Buffer . from ( 'second' ) ,
165+ undefined ,
166+ { version : { source : 'api' , authorUserId : fixture . bobId } }
167+ )
168+ const rows = await versionRows ( fixture . fileId )
169+ expect ( rows . map ( ( row ) => [ row . version , row . source , row . authorUserIds ] ) ) . toEqual ( [
170+ [ 1 , 'upload' , [ fixture . aliceId ] ] ,
171+ [ 2 , 'api' , [ fixture . bobId ] ] ,
172+ ] )
173+ expect ( await readVersionBytes ( fixture . workspaceId , fixture . fileId , rows [ 0 ] . key ) ) . toBe (
174+ 'original'
175+ )
176+ await db . transaction ( ( tx ) =>
177+ handoffFileCreatorsInTx ( tx , eq ( workspaceFiles . id , fixture . fileId ) , fixture . aliceId )
178+ )
179+ expect ( await versionRows ( fixture . fileId ) ) . toEqual ( rows )
180+ } )
181+
182+ it ( 'does not manufacture original authorship for previously overwritten unrecorded content' , async ( ) => {
183+ const fixture = await seedFile ( 'overwritten' )
184+ await db
185+ . update ( workspaceFiles )
186+ . set ( { contentUpdatedAt : new Date ( Date . now ( ) + 5000 ) } )
187+ . where ( eq ( workspaceFiles . id , fixture . fileId ) )
188+ await db . transaction ( ( tx ) =>
189+ handoffFileCreatorsInTx ( tx , eq ( workspaceFiles . id , fixture . fileId ) , fixture . bobId )
190+ )
191+ expect ( await versionRows ( fixture . fileId ) ) . toMatchObject ( [
192+ { version : 1 , source : 'unknown' , authorUserIds : [ ] } ,
193+ ] )
194+ } )
195+
196+ it ( 'keeps an empty shell unversioned until its first content write after handoff' , async ( ) => {
197+ const fixture = await seedFile ( '' )
198+ await db . transaction ( ( tx ) =>
199+ handoffFileCreatorsInTx ( tx , eq ( workspaceFiles . id , fixture . fileId ) , fixture . bobId )
200+ )
201+ expect ( await versionRows ( fixture . fileId ) ) . toEqual ( [ ] )
202+ await updateWorkspaceFileContent (
203+ fixture . workspaceId ,
204+ fixture . fileId ,
205+ fixture . bobId ,
206+ Buffer . from ( 'first' ) ,
207+ undefined ,
208+ { version : { source : 'api' , authorUserId : fixture . bobId } }
209+ )
210+ expect ( await versionRows ( fixture . fileId ) ) . toMatchObject ( [
211+ { version : 1 , source : 'api' , authorUserIds : [ fixture . bobId ] } ,
212+ ] )
213+ } )
214+
215+ it ( 'serializes content writes with creator handoff without duplicate or misattributed history' , async ( ) => {
216+ const fixture = await seedFile ( 'original' )
217+ await Promise . all ( [
218+ db . transaction ( ( tx ) =>
219+ handoffFileCreatorsInTx ( tx , eq ( workspaceFiles . id , fixture . fileId ) , fixture . bobId )
220+ ) ,
221+ updateWorkspaceFileContent (
222+ fixture . workspaceId ,
223+ fixture . fileId ,
224+ fixture . aliceId ,
225+ Buffer . from ( 'second' ) ,
226+ undefined ,
227+ { version : { source : 'api' , authorUserId : fixture . aliceId } }
228+ ) ,
229+ ] )
230+ const rows = await versionRows ( fixture . fileId )
231+ expect ( rows . map ( ( row ) => [ row . version , row . authorUserIds ] ) ) . toEqual ( [
232+ [ 1 , [ fixture . aliceId ] ] ,
233+ [ 2 , [ fixture . aliceId ] ] ,
234+ ] )
235+ expect ( rows . filter ( ( row ) => row . supersededAt === null ) ) . toHaveLength ( 1 )
236+ expect ( await readVersionBytes ( fixture . workspaceId , fixture . fileId , rows [ 0 ] . key ) ) . toBe (
237+ 'original'
238+ )
239+ } )
240+
136241 it . each ( [
137242 [ 'named' , 'download' ] ,
138243 [ 'anonymous' , 'download' ] ,
0 commit comments