1- import { describe , expect , test } from "bun:test"
2- import { Project } from "../../src/project/project"
1+ import { describe , expect , mock , test } from "bun:test"
2+ import type { Project as ProjectNS } from "../../src/project/project"
33import { Log } from "../../src/util/log"
44import { Storage } from "../../src/storage/storage"
55import { $ } from "bun"
@@ -8,12 +8,78 @@ import { tmpdir } from "../fixture/fixture"
88
99Log . init ( { print : false } )
1010
11+ const bunModule = await import ( "bun" )
12+ type Mode = "none" | "rev-list-fail" | "top-fail" | "common-dir-fail"
13+ let mode : Mode = "none"
14+
15+ function render ( parts : TemplateStringsArray , vals : unknown [ ] ) {
16+ return parts . reduce ( ( acc , part , i ) => `${ acc } ${ part } ${ i < vals . length ? String ( vals [ i ] ) : "" } ` , "" )
17+ }
18+
19+ function fakeShell ( output : { exitCode : number ; stdout : string ; stderr : string } ) {
20+ const result = {
21+ exitCode : output . exitCode ,
22+ stdout : Buffer . from ( output . stdout ) ,
23+ stderr : Buffer . from ( output . stderr ) ,
24+ text : async ( ) => output . stdout ,
25+ }
26+ const shell = {
27+ quiet : ( ) => shell ,
28+ nothrow : ( ) => shell ,
29+ cwd : ( ) => shell ,
30+ env : ( ) => shell ,
31+ text : async ( ) => output . stdout ,
32+ then : ( onfulfilled : ( value : typeof result ) => unknown , onrejected ?: ( reason : unknown ) => unknown ) =>
33+ Promise . resolve ( result ) . then ( onfulfilled , onrejected ) ,
34+ catch : ( onrejected : ( reason : unknown ) => unknown ) => Promise . resolve ( result ) . catch ( onrejected ) ,
35+ finally : ( onfinally : ( ( ) => void ) | undefined | null ) => Promise . resolve ( result ) . finally ( onfinally ) ,
36+ }
37+ return shell
38+ }
39+
40+ mock . module ( "bun" , ( ) => ( {
41+ ...bunModule ,
42+ $ : ( parts : TemplateStringsArray , ...vals : unknown [ ] ) => {
43+ const cmd = render ( parts , vals ) . replaceAll ( "," , " " ) . replace ( / \s + / g, " " ) . trim ( )
44+ if (
45+ mode === "rev-list-fail" &&
46+ cmd . includes ( "git rev-list" ) &&
47+ cmd . includes ( "--max-parents=0" ) &&
48+ cmd . includes ( "--all" )
49+ ) {
50+ return fakeShell ( { exitCode : 128 , stdout : "" , stderr : "fatal" } )
51+ }
52+ if ( mode === "top-fail" && cmd . includes ( "git rev-parse" ) && cmd . includes ( "--show-toplevel" ) ) {
53+ return fakeShell ( { exitCode : 128 , stdout : "" , stderr : "fatal" } )
54+ }
55+ if ( mode === "common-dir-fail" && cmd . includes ( "git rev-parse" ) && cmd . includes ( "--git-common-dir" ) ) {
56+ return fakeShell ( { exitCode : 128 , stdout : "" , stderr : "fatal" } )
57+ }
58+ return ( bunModule . $ as any ) ( parts , ...vals )
59+ } ,
60+ } ) )
61+
62+ async function withMode ( next : Mode , run : ( ) => Promise < void > ) {
63+ const prev = mode
64+ mode = next
65+ try {
66+ await run ( )
67+ } finally {
68+ mode = prev
69+ }
70+ }
71+
72+ async function loadProject ( ) {
73+ return ( await import ( "../../src/project/project" ) ) . Project
74+ }
75+
1176describe ( "Project.fromDirectory" , ( ) => {
1277 test ( "should handle git repository with no commits" , async ( ) => {
78+ const p = await loadProject ( )
1379 await using tmp = await tmpdir ( )
1480 await $ `git init` . cwd ( tmp . path ) . quiet ( )
1581
16- const { project } = await Project . fromDirectory ( tmp . path )
82+ const { project } = await p . fromDirectory ( tmp . path )
1783
1884 expect ( project ) . toBeDefined ( )
1985 expect ( project . id ) . toBe ( "global" )
@@ -26,9 +92,10 @@ describe("Project.fromDirectory", () => {
2692 } )
2793
2894 test ( "should handle git repository with commits" , async ( ) => {
95+ const p = await loadProject ( )
2996 await using tmp = await tmpdir ( { git : true } )
3097
31- const { project } = await Project . fromDirectory ( tmp . path )
98+ const { project } = await p . fromDirectory ( tmp . path )
3299
33100 expect ( project ) . toBeDefined ( )
34101 expect ( project . id ) . not . toBe ( "global" )
@@ -39,26 +106,65 @@ describe("Project.fromDirectory", () => {
39106 const fileExists = await Bun . file ( opencodeFile ) . exists ( )
40107 expect ( fileExists ) . toBe ( true )
41108 } )
109+
110+ test ( "keeps git vcs when rev-list exits non-zero with empty output" , async ( ) => {
111+ const p = await loadProject ( )
112+ await using tmp = await tmpdir ( )
113+ await $ `git init` . cwd ( tmp . path ) . quiet ( )
114+
115+ await withMode ( "rev-list-fail" , async ( ) => {
116+ const { project } = await p . fromDirectory ( tmp . path )
117+ expect ( project . vcs ) . toBe ( "git" )
118+ expect ( project . id ) . toBe ( "global" )
119+ expect ( project . worktree ) . toBe ( tmp . path )
120+ } )
121+ } )
122+
123+ test ( "keeps git vcs when show-toplevel exits non-zero with empty output" , async ( ) => {
124+ const p = await loadProject ( )
125+ await using tmp = await tmpdir ( { git : true } )
126+
127+ await withMode ( "top-fail" , async ( ) => {
128+ const { project, sandbox } = await p . fromDirectory ( tmp . path )
129+ expect ( project . vcs ) . toBe ( "git" )
130+ expect ( project . worktree ) . toBe ( tmp . path )
131+ expect ( sandbox ) . toBe ( tmp . path )
132+ } )
133+ } )
134+
135+ test ( "keeps git vcs when git-common-dir exits non-zero with empty output" , async ( ) => {
136+ const p = await loadProject ( )
137+ await using tmp = await tmpdir ( { git : true } )
138+
139+ await withMode ( "common-dir-fail" , async ( ) => {
140+ const { project, sandbox } = await p . fromDirectory ( tmp . path )
141+ expect ( project . vcs ) . toBe ( "git" )
142+ expect ( project . worktree ) . toBe ( tmp . path )
143+ expect ( sandbox ) . toBe ( tmp . path )
144+ } )
145+ } )
42146} )
43147
44148describe ( "Project.fromDirectory with worktrees" , ( ) => {
45149 test ( "should set worktree to root when called from root" , async ( ) => {
150+ const p = await loadProject ( )
46151 await using tmp = await tmpdir ( { git : true } )
47152
48- const { project, sandbox } = await Project . fromDirectory ( tmp . path )
153+ const { project, sandbox } = await p . fromDirectory ( tmp . path )
49154
50155 expect ( project . worktree ) . toBe ( tmp . path )
51156 expect ( sandbox ) . toBe ( tmp . path )
52157 expect ( project . sandboxes ) . not . toContain ( tmp . path )
53158 } )
54159
55160 test ( "should set worktree to root when called from a worktree" , async ( ) => {
161+ const p = await loadProject ( )
56162 await using tmp = await tmpdir ( { git : true } )
57163
58164 const worktreePath = path . join ( tmp . path , ".." , "worktree-test" )
59165 await $ `git worktree add ${ worktreePath } -b test-branch` . cwd ( tmp . path ) . quiet ( )
60166
61- const { project, sandbox } = await Project . fromDirectory ( worktreePath )
167+ const { project, sandbox } = await p . fromDirectory ( worktreePath )
62168
63169 expect ( project . worktree ) . toBe ( tmp . path )
64170 expect ( sandbox ) . toBe ( worktreePath )
@@ -69,15 +175,16 @@ describe("Project.fromDirectory with worktrees", () => {
69175 } )
70176
71177 test ( "should accumulate multiple worktrees in sandboxes" , async ( ) => {
178+ const p = await loadProject ( )
72179 await using tmp = await tmpdir ( { git : true } )
73180
74181 const worktree1 = path . join ( tmp . path , ".." , "worktree-1" )
75182 const worktree2 = path . join ( tmp . path , ".." , "worktree-2" )
76183 await $ `git worktree add ${ worktree1 } -b branch-1` . cwd ( tmp . path ) . quiet ( )
77184 await $ `git worktree add ${ worktree2 } -b branch-2` . cwd ( tmp . path ) . quiet ( )
78185
79- await Project . fromDirectory ( worktree1 )
80- const { project } = await Project . fromDirectory ( worktree2 )
186+ await p . fromDirectory ( worktree1 )
187+ const { project } = await p . fromDirectory ( worktree2 )
81188
82189 expect ( project . worktree ) . toBe ( tmp . path )
83190 expect ( project . sandboxes ) . toContain ( worktree1 )
@@ -91,30 +198,32 @@ describe("Project.fromDirectory with worktrees", () => {
91198
92199describe ( "Project.discover" , ( ) => {
93200 test ( "should discover favicon.png in root" , async ( ) => {
201+ const p = await loadProject ( )
94202 await using tmp = await tmpdir ( { git : true } )
95- const { project } = await Project . fromDirectory ( tmp . path )
203+ const { project } = await p . fromDirectory ( tmp . path )
96204
97205 const pngData = Buffer . from ( [ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] )
98206 await Bun . write ( path . join ( tmp . path , "favicon.png" ) , pngData )
99207
100- await Project . discover ( project )
208+ await p . discover ( project )
101209
102- const updated = await Storage . read < Project . Info > ( [ "project" , project . id ] )
210+ const updated = await Storage . read < ProjectNS . Info > ( [ "project" , project . id ] )
103211 expect ( updated . icon ) . toBeDefined ( )
104212 expect ( updated . icon ?. url ) . toStartWith ( "data:" )
105213 expect ( updated . icon ?. url ) . toContain ( "base64" )
106214 expect ( updated . icon ?. color ) . toBeUndefined ( )
107215 } )
108216
109217 test ( "should not discover non-image files" , async ( ) => {
218+ const p = await loadProject ( )
110219 await using tmp = await tmpdir ( { git : true } )
111- const { project } = await Project . fromDirectory ( tmp . path )
220+ const { project } = await p . fromDirectory ( tmp . path )
112221
113222 await Bun . write ( path . join ( tmp . path , "favicon.txt" ) , "not an image" )
114223
115- await Project . discover ( project )
224+ await p . discover ( project )
116225
117- const updated = await Storage . read < Project . Info > ( [ "project" , project . id ] )
226+ const updated = await Storage . read < ProjectNS . Info > ( [ "project" , project . id ] )
118227 expect ( updated . icon ) . toBeUndefined ( )
119228 } )
120229} )
0 commit comments