11import { execFileSync } from 'node:child_process' ;
2- import { mkdtempSync , mkdirSync , readFileSync , readdirSync , rmSync , lstatSync , symlinkSync , writeFileSync } from 'node:fs' ;
2+ import { mkdtempSync , mkdirSync , readFileSync , readdirSync , rmSync , lstatSync , symlinkSync , writeFileSync , renameSync , opendirSync } from 'node:fs' ;
33import { tmpdir } from 'node:os' ;
44import { join } from 'node:path' ;
5- import { afterEach , describe , expect , it } from 'vitest' ;
5+ import { afterEach , describe , expect , it , vi } from 'vitest' ;
66import { createTaskClone } from '../git/clone.ts' ;
77
8+ vi . mock ( 'node:fs' , async importOriginal => {
9+ const actual = await importOriginal < typeof import ( 'node:fs' ) > ( ) ;
10+ return { ...actual , readdirSync : vi . fn ( actual . readdirSync ) , opendirSync : vi . fn ( actual . opendirSync ) } ;
11+ } ) ;
12+ vi . mock ( 'node:child_process' , async importOriginal => {
13+ const actual = await importOriginal < typeof import ( 'node:child_process' ) > ( ) ;
14+ return { ...actual , execFileSync : vi . fn ( actual . execFileSync ) } ;
15+ } ) ;
16+
817const roots : string [ ] = [ ] ;
918const git = ( cwd : string , ...args : string [ ] ) => execFileSync ( 'git' , [ '-c' , 'core.hooksPath=/dev/null' , ...args ] ,
1019 { cwd, encoding : 'utf8' , stdio : [ 'ignore' , 'pipe' , 'pipe' ] } ) . trim ( ) ;
@@ -16,7 +25,7 @@ function fixture() {
1625 writeFileSync ( join ( source , 'file.txt' ) , 'trusted\n' ) ; git ( source , 'add' , '.' ) ; git ( source , 'commit' , '-m' , 'baseline' ) ;
1726 return { source, parent, head : git ( source , 'rev-parse' , 'HEAD' ) , taskId : 'task-1' } ;
1827}
19- afterEach ( ( ) => { for ( const root of roots . splice ( 0 ) ) rmSync ( root , { recursive : true , force : true } ) ; } ) ;
28+ afterEach ( ( ) => { vi . restoreAllMocks ( ) ; vi . resetAllMocks ( ) ; for ( const root of roots . splice ( 0 ) ) rmSync ( root , { recursive : true , force : true } ) ; } ) ;
2029describe ( 'isolated staging clone' , ( ) => {
2130 it ( 'copies objects, ignores dirty source changes, and has no origin or shared metadata' , ( ) => {
2231 const input = fixture ( ) ;
@@ -69,6 +78,43 @@ describe('isolated staging clone', () => {
6978 expect ( ( ) => createTaskClone ( { ...input , timeoutMs : Infinity } ) ) . toThrow ( 'deadline' ) ;
7079 expect ( readdirSync ( input . parent ) ) . toEqual ( [ ] ) ;
7180 } ) ;
81+ it ( 'canonicalizes symlinked common metadata before checking storage containment' , ( ) => {
82+ const input = fixture ( ) , metadata = join ( input . parent , 'metadata' ) ;
83+ renameSync ( join ( input . source , '.git' ) , metadata ) ;
84+ symlinkSync ( metadata , join ( input . source , '.git' ) ) ;
85+ const parent = join ( metadata , 'tasks' ) ; mkdirSync ( parent ) ;
86+ // Disputed intermediate state: Git reports a lexical path through the link.
87+ expect ( git ( input . source , 'rev-parse' , '--git-common-dir' ) ) . toBe ( '.git' ) ;
88+ expect ( lstatSync ( join ( input . source , '.git' ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
89+ expect ( ( ) => createTaskClone ( { ...input , parent } ) ) . toThrow ( 'outside source metadata' ) ;
90+ expect ( readdirSync ( parent ) ) . toEqual ( [ ] ) ;
91+ } ) ;
92+ it ( 'never materializes an entire object directory before checking its entry budget' , ( ) => {
93+ const input = fixture ( ) ;
94+ // A bulk enumeration is forbidden even for a small fixture; this asserts
95+ // the disputed intermediate representation, not only a later limit error.
96+ vi . mocked ( readdirSync ) . mockClear ( ) ;
97+ createTaskClone ( input ) ;
98+ expect ( readdirSync ) . not . toHaveBeenCalled ( ) ;
99+ expect ( opendirSync ) . toHaveBeenCalled ( ) ;
100+ } ) ;
101+ it ( 'rejects a successful final Git call that returns after the overall deadline' , async ( ) => {
102+ const input = fixture ( ) ;
103+ const actual = await vi . importActual < typeof import ( 'node:child_process' ) > ( 'node:child_process' ) ;
104+ let elapsed = 0 , lateResult = false ;
105+ vi . spyOn ( performance , 'now' ) . mockImplementation ( ( ) => elapsed ) ;
106+ vi . mocked ( execFileSync ) . mockImplementation ( ( ( file : string , args : string [ ] , options : object ) => {
107+ const result = actual . execFileSync ( file , args , options ) ;
108+ if ( args . at ( - 2 ) === 'rev-parse' && args . at ( - 1 ) === 'HEAD' ) {
109+ elapsed = 1001 ; lateResult = true ;
110+ }
111+ return result ;
112+ } ) as typeof execFileSync ) ;
113+ expect ( ( ) => createTaskClone ( { ...input , timeoutMs : 1000 } ) ) . toThrow ( 'deadline' ) ;
114+ expect ( lateResult ) . toBe ( true ) ;
115+ expect ( readdirSync ( input . parent ) ) . toEqual ( [ ] ) ;
116+ vi . mocked ( execFileSync ) . mockImplementation ( actual . execFileSync ) ;
117+ } ) ;
72118 it ( 'does not inherit Git directory, index, configuration or object overrides' , ( ) => {
73119 const input = fixture ( ) ;
74120 const keys = [ 'GIT_DIR' , 'GIT_INDEX_FILE' , 'GIT_CONFIG_COUNT' , 'GIT_CONFIG_KEY_0' , 'GIT_CONFIG_VALUE_0' ] ;
0 commit comments