11import { afterEach , describe , expect , it , mock } from "bun:test" ;
2- import { execFileSync } from "node:child_process" ;
2+ import type { execFileSync } from "node:child_process" ;
33import { createHash } from "node:crypto" ;
4- import { chmodSync , mkdirSync , mkdtempSync , rmSync , symlinkSync , writeFileSync } from "node:fs" ;
4+ import { chmodSync , mkdirSync , mkdtempSync , rmSync , symlinkSync } from "node:fs" ;
55import { tmpdir } from "node:os" ;
66import path , { join } from "node:path" ;
77import {
@@ -18,6 +18,8 @@ import {
1818} from "./project-identity" ;
1919
2020const tempDirs : string [ ] = [ ] ;
21+ const FIRST_ROOT_COMMIT = "abcdef1234567890abcdef1234567890abcdef12" ;
22+ const SECOND_ROOT_COMMIT = "1234567890abcdef1234567890abcdef12345678" ;
2123
2224afterEach ( ( ) => {
2325 __resetProjectIdentityForTests ( ) ;
@@ -38,36 +40,17 @@ function makeTempDir(prefix: string): string {
3840 return dir ;
3941}
4042
41- function runGit ( directory : string , args : string [ ] ) : string {
42- return execFileSync ( "git" , args , {
43- cwd : directory ,
44- encoding : "utf8" ,
45- env : { ...process . env , LC_ALL : "C" , LANG : "C" } ,
46- stdio : [ "ignore" , "pipe" , "pipe" ] ,
47- } ) ;
48- }
49-
50- function makeGitRepo ( ) : string {
51- const dir = makeTempDir ( "project-identity-git-" ) ;
52- runGit ( dir , [ "init" ] ) ;
53- writeFileSync ( join ( dir , "README.md" ) , "# test\n" , "utf8" ) ;
54- runGit ( dir , [ "add" , "README.md" ] ) ;
55- runGit ( dir , [
56- "-c" ,
57- "user.email=test@example.com" ,
58- "-c" ,
59- "user.name=Test User" ,
60- "-c" ,
61- "commit.gpgsign=false" ,
62- "commit" ,
63- "-m" ,
64- "initial commit" ,
65- ] ) ;
43+ function makeRepoWithGitMetadata ( prefix : string ) : string {
44+ const dir = makeTempDir ( prefix ) ;
45+ mkdirSync ( join ( dir , ".git" ) ) ;
6646 return dir ;
6747}
6848
69- function rootCommit ( directory : string ) : string {
70- return runGit ( directory , [ "rev-list" , "--max-parents=0" , "HEAD" ] ) . split ( "\n" ) [ 0 ] ! . trim ( ) ;
49+ function returningRootCommit ( rootCommit : string ) : typeof execFileSync {
50+ // Keep the real `.git` walk on disk, but route git output through the test
51+ // seam. Under heavy machine load, launching git can stall while the
52+ // operating system assesses the binary, which makes direct calls flaky.
53+ return ( ( ) => `${ rootCommit } \n` ) as typeof execFileSync ;
7154}
7255
7356function expectedDirIdentity ( directory : string ) : string {
@@ -113,12 +96,12 @@ function makeGitFailure(fields: {
11396
11497describe ( "project identity" , ( ) => {
11598 it ( "resolveProjectIdentityStrict returns the git root commit identity" , ( ) => {
116- const repo = makeGitRepo ( ) ;
117- const commit = rootCommit ( repo ) ;
99+ const repo = makeRepoWithGitMetadata ( "project-identity-git-" ) ;
100+ __setProjectIdentityTestHooks ( { execFileSync : returningRootCommit ( FIRST_ROOT_COMMIT ) } ) ;
118101
119102 const identity = resolveProjectIdentityStrict ( repo ) ;
120103
121- expect ( identity ) . toBe ( `git:${ commit } ` ) ;
104+ expect ( identity ) . toBe ( `git:${ FIRST_ROOT_COMMIT } ` ) ;
122105 expect ( identity . slice ( "git:" . length ) . length ) . toBeGreaterThanOrEqual ( 7 ) ;
123106 } ) ;
124107
@@ -142,7 +125,8 @@ describe("project identity", () => {
142125 } ) ;
143126
144127 it ( "resolveProjectIdentityStrict caches git identities across calls" , ( ) => {
145- const repo = makeGitRepo ( ) ;
128+ const repo = makeRepoWithGitMetadata ( "project-identity-cache-" ) ;
129+ __setProjectIdentityTestHooks ( { execFileSync : returningRootCommit ( FIRST_ROOT_COMMIT ) } ) ;
146130 const first = resolveProjectIdentityStrict ( repo ) ;
147131
148132 chmodSync ( repo , 0o000 ) ;
@@ -159,10 +143,10 @@ describe("project identity", () => {
159143 expect ( resolveProjectIdentity ( directory ) ) . toBe ( expectedDirIdentity ( directory ) ) ;
160144 } ) ;
161145
162- it ( "uses the no-git fast path without spawning git" , ( ) => {
146+ it ( "uses the no-git fast path without invoking git" , ( ) => {
163147 const directory = makeTempDir ( "project-identity-no-git-fast-" ) ;
164148 const execMock = mock ( ( ) => {
165- throw new Error ( "git should not be spawned for a directory with no .git ancestor" ) ;
149+ throw new Error ( "git should not be launched for a directory with no .git ancestor" ) ;
166150 } ) ;
167151 __setProjectIdentityTestHooks ( { execFileSync : execMock as unknown as typeof execFileSync } ) ;
168152
@@ -171,16 +155,16 @@ describe("project identity", () => {
171155 } ) ;
172156
173157 it ( "detects git metadata in ancestors for subdirectory sessions" , ( ) => {
174- const repo = makeGitRepo ( ) ;
158+ const repo = makeRepoWithGitMetadata ( "project-identity-ancestor-" ) ;
175159 const subdir = join ( repo , "nested" , "session" ) ;
176160 mkdirSync ( subdir , { recursive : true } ) ;
161+ __setProjectIdentityTestHooks ( { execFileSync : returningRootCommit ( FIRST_ROOT_COMMIT ) } ) ;
177162
178- expect ( resolveProjectIdentity ( subdir ) ) . toBe ( `git:${ rootCommit ( repo ) } ` ) ;
163+ expect ( resolveProjectIdentity ( subdir ) ) . toBe ( `git:${ FIRST_ROOT_COMMIT } ` ) ;
179164 } ) ;
180165
181166 it ( "detects git metadata through symlinked checkout paths" , ( ) => {
182- const repo = makeTempDir ( "project-identity-symlink-repo-" ) ;
183- writeFileSync ( join ( repo , ".git" ) , "gitdir: .git/worktrees/main\n" , "utf8" ) ;
167+ const repo = makeRepoWithGitMetadata ( "project-identity-symlink-repo-" ) ;
184168 const subdir = join ( repo , "nested" , "session" ) ;
185169 mkdirSync ( subdir , { recursive : true } ) ;
186170 const linkParent = makeTempDir ( "project-identity-symlink-parent-" ) ;
@@ -191,49 +175,47 @@ describe("project identity", () => {
191175 if ( ( error as { code ?: unknown } ) . code === "EPERM" ) return ;
192176 throw error ;
193177 }
194- const execMock = mock ( ( ) => "abcdef1234567890\n" ) ;
178+ const execMock = mock ( ( ) => ` ${ FIRST_ROOT_COMMIT } \n` ) ;
195179 __setProjectIdentityTestHooks ( { execFileSync : execMock as unknown as typeof execFileSync } ) ;
196180
197- expect ( resolveProjectIdentity ( link ) ) . toBe ( " git:abcdef1234567890" ) ;
181+ expect ( resolveProjectIdentity ( link ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
198182 expect ( execMock ) . toHaveBeenCalledTimes ( 1 ) ;
199183 } ) ;
200184
201185 it ( "reuses the last successful git identity during transient failures and cooldown" , ( ) => {
202- const directory = makeTempDir ( "project-identity-last-known-" ) ;
203- mkdirSync ( join ( directory , ".git" ) ) ;
186+ const directory = makeRepoWithGitMetadata ( "project-identity-last-known-" ) ;
204187 let now = 1_000 ;
205188 let mode : "first" | "timeout" | "second" = "first" ;
206189 const execMock = mock ( ( ) => {
207- if ( mode === "first" ) return "abcdef1234567890\n" ;
208- if ( mode === "second" ) return "1234567890abcdef\n" ;
190+ if ( mode === "first" ) return ` ${ FIRST_ROOT_COMMIT } \n` ;
191+ if ( mode === "second" ) return ` ${ SECOND_ROOT_COMMIT } \n` ;
209192 throw makeGitFailure ( { code : "ETIMEDOUT" } ) ;
210193 } ) ;
211194 __setProjectIdentityTestHooks ( {
212195 execFileSync : execMock as unknown as typeof execFileSync ,
213196 nowMs : ( ) => now ,
214197 } ) ;
215198
216- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:abcdef1234567890" ) ;
199+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
217200 __clearProjectIdentityResolutionCacheForTests ( directory ) ;
218201 mode = "timeout" ;
219202
220- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:abcdef1234567890" ) ;
221- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:abcdef1234567890" ) ;
203+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
204+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
222205 expect ( execMock ) . toHaveBeenCalledTimes ( 2 ) ;
223206
224207 mode = "second" ;
225208 now += 5 * 60 * 1000 + 1 ;
226- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:1234567890abcdef" ) ;
209+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ SECOND_ROOT_COMMIT } ` ) ;
227210 expect ( execMock ) . toHaveBeenCalledTimes ( 3 ) ;
228211 } ) ;
229212
230213 it ( "classifies dubious ownership and falls back until the cooldown expires" , ( ) => {
231- const directory = makeTempDir ( "project-identity-dubious-" ) ;
232- mkdirSync ( join ( directory , ".git" ) ) ;
214+ const directory = makeRepoWithGitMetadata ( "project-identity-dubious-" ) ;
233215 let now = 1_000 ;
234216 let recovered = false ;
235217 const execMock = mock ( ( ) => {
236- if ( recovered ) return "abcdef1234567890\n" ;
218+ if ( recovered ) return ` ${ FIRST_ROOT_COMMIT } \n` ;
237219 throw makeGitFailure ( {
238220 stderr :
239221 "fatal: detected dubious ownership in repository at '/repo'\n" +
@@ -261,13 +243,12 @@ describe("project identity", () => {
261243 expect ( execMock ) . toHaveBeenCalledTimes ( 2 ) ;
262244
263245 now += 5 * 60 * 1000 + 1 ;
264- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:abcdef1234567890" ) ;
246+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
265247 expect ( execMock ) . toHaveBeenCalledTimes ( 3 ) ;
266248 } ) ;
267249
268- it ( "cools down git timeouts so immediate retries do not respawn git" , ( ) => {
269- const directory = makeTempDir ( "project-identity-timeout-" ) ;
270- mkdirSync ( join ( directory , ".git" ) ) ;
250+ it ( "cools down git timeouts so immediate retries do not rerun git" , ( ) => {
251+ const directory = makeRepoWithGitMetadata ( "project-identity-timeout-" ) ;
271252 const execMock = mock ( ( ) => {
272253 throw makeGitFailure ( { code : "ETIMEDOUT" } ) ;
273254 } ) ;
@@ -279,11 +260,10 @@ describe("project identity", () => {
279260 } ) ;
280261
281262 it ( "re-probes after a transient cooldown is cleared" , ( ) => {
282- const directory = makeTempDir ( "project-identity-clear-cooldown-" ) ;
283- mkdirSync ( join ( directory , ".git" ) ) ;
263+ const directory = makeRepoWithGitMetadata ( "project-identity-clear-cooldown-" ) ;
284264 let recovered = false ;
285265 const execMock = mock ( ( ) => {
286- if ( recovered ) return "abcdef1234567890\n" ;
266+ if ( recovered ) return ` ${ FIRST_ROOT_COMMIT } \n` ;
287267 throw makeGitFailure ( { code : "ETIMEDOUT" } ) ;
288268 } ) ;
289269 __setProjectIdentityTestHooks ( { execFileSync : execMock as unknown as typeof execFileSync } ) ;
@@ -292,13 +272,12 @@ describe("project identity", () => {
292272 recovered = true ;
293273 __clearProjectIdentityTransientCooldownForTests ( directory ) ;
294274
295- expect ( resolveProjectIdentity ( directory ) ) . toBe ( " git:abcdef1234567890" ) ;
275+ expect ( resolveProjectIdentity ( directory ) ) . toBe ( ` git:${ FIRST_ROOT_COMMIT } ` ) ;
296276 expect ( execMock ) . toHaveBeenCalledTimes ( 2 ) ;
297277 } ) ;
298278
299279 it ( "still propagates permission_denied git failures" , ( ) => {
300- const directory = makeTempDir ( "project-identity-permission-" ) ;
301- mkdirSync ( join ( directory , ".git" ) ) ;
280+ const directory = makeRepoWithGitMetadata ( "project-identity-permission-" ) ;
302281 __setProjectIdentityTestHooks ( {
303282 execFileSync : mock ( ( ) => {
304283 throw makeGitFailure ( { code : "EACCES" } ) ;
@@ -329,8 +308,8 @@ describe("project identity", () => {
329308 expect ( storedPathBelongsToIdentity ( "dir:deadbeef" , "dir:deadbeef" ) ) . toBe ( true ) ;
330309 // Mismatched identity.
331310 expect ( storedPathBelongsToIdentity ( "git:abc123" , "git:other" ) ) . toBe ( false ) ;
332- // A raw filesystem path stored before normalization must still match the
333- // identity it normalizes to (the #11 case Pi previously rejected) .
311+ // Raw filesystem paths stored before normalization must still match the
312+ // identity they normalize to.
334313 const directory = makeTempDir ( "project-identity-belongs-" ) ;
335314 const identity = expectedDirIdentity ( directory ) ;
336315 expect ( storedPathBelongsToIdentity ( directory , identity ) ) . toBe ( true ) ;
0 commit comments