@@ -80,6 +80,23 @@ async function spawnGh(args: string[], timeoutMs: number): Promise<{ status: num
8080}
8181
8282const defaultDeps : StarDeps = { runGh : spawnGh , nowMs : ( ) => Date . now ( ) } ;
83+ /**
84+ * Route tests call the real management dispatcher, which has no place to pass
85+ * `StarDeps`. Without an override, Windows CI spawns the real `gh.cmd` shim and
86+ * burns the full `AUTH_TIMEOUT_MS` — equal to Bun's default test timeout — so
87+ * `GET /api/github/star` flakes as a 5s hang. Tests install a fast fake here.
88+ */
89+ let overrideDeps : StarDeps | null = null ;
90+
91+ function resolveDeps ( deps ?: StarDeps ) : StarDeps {
92+ return deps ?? overrideDeps ?? defaultDeps ;
93+ }
94+
95+ /** Test-only: swap the `gh` runner used when callers omit `deps`. Pass `null` to clear. */
96+ export function setStarDepsForTests ( deps : StarDeps | null ) : void {
97+ overrideDeps = deps ;
98+ invalidateStarStatusCache ( ) ;
99+ }
83100
84101let cached : { timestamp : number ; state : StarState } | null = null ;
85102/** Coalesces concurrent probes so parallel sidebar polls share one `gh` run. */
@@ -97,10 +114,11 @@ let generation = 0;
97114 * starred and 404 when not, so a non-zero exit is only meaningful once we know
98115 * the CLI is authenticated — hence the auth check first.
99116 */
100- export async function probeStarState ( deps : StarDeps = defaultDeps ) : Promise < StarState > {
101- const auth = await deps . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
117+ export async function probeStarState ( deps ?: StarDeps ) : Promise < StarState > {
118+ const resolved = resolveDeps ( deps ) ;
119+ const auth = await resolved . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
102120 if ( ! auth || auth . status !== 0 ) return "unauthenticated" ;
103- const starred = await deps . runGh (
121+ const starred = await resolved . runGh (
104122 [ "api" , "--hostname" , GH_HOSTNAME , `/user/starred/${ STAR_REPO } ` ] ,
105123 API_TIMEOUT_MS ,
106124 ) ;
@@ -109,8 +127,9 @@ export async function probeStarState(deps: StarDeps = defaultDeps): Promise<Star
109127}
110128
111129/** Cached star state; `gh` is only spawned when the cache is cold or expired. */
112- export async function getStarStatus ( deps : StarDeps = defaultDeps ) : Promise < StarStatus > {
113- const now = deps . nowMs ( ) ;
130+ export async function getStarStatus ( deps ?: StarDeps ) : Promise < StarStatus > {
131+ const resolved = resolveDeps ( deps ) ;
132+ const now = resolved . nowMs ( ) ;
114133 if ( cached && now - cached . timestamp < CACHE_TTL_MS ) {
115134 return { state : cached . state , repo : STAR_REPO , url : STAR_REPO_URL } ;
116135 }
@@ -123,7 +142,7 @@ export async function getStarStatus(deps: StarDeps = defaultDeps): Promise<StarS
123142 // The slot is cleared inside the same continuation that commits the cache. Using
124143 // `.finally()` for that defers it by a microtask, which leaves a window where the
125144 // next caller awaits an already-settled probe instead of starting a fresh one.
126- const probe = probeStarState ( deps ) . then (
145+ const probe = probeStarState ( resolved ) . then (
127146 state => {
128147 if ( inflight === probe ) inflight = null ;
129148 // A write landed while this read was in flight — its result is authoritative.
@@ -159,19 +178,20 @@ export function invalidateStarStatusCache(): void {
159178 * management API.
160179 */
161180export async function starRepository (
162- deps : StarDeps = defaultDeps ,
181+ deps ? : StarDeps ,
163182) : Promise < { ok : boolean ; status : StarStatus ; code ?: StarErrorCode } > {
164- const auth = await deps . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
183+ const resolved = resolveDeps ( deps ) ;
184+ const auth = await resolved . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
165185 if ( ! auth || auth . status !== 0 ) {
166186 generation += 1 ;
167- cached = { timestamp : deps . nowMs ( ) , state : "unauthenticated" } ;
187+ cached = { timestamp : resolved . nowMs ( ) , state : "unauthenticated" } ;
168188 return {
169189 ok : false ,
170190 status : { state : "unauthenticated" , repo : STAR_REPO , url : STAR_REPO_URL } ,
171191 code : "gh_unavailable" ,
172192 } ;
173193 }
174- const result = await deps . runGh (
194+ const result = await resolved . runGh (
175195 [ "api" , "--hostname" , GH_HOSTNAME , "-X" , "PUT" , `/user/starred/${ STAR_REPO } ` ] ,
176196 API_TIMEOUT_MS ,
177197 ) ;
@@ -186,6 +206,6 @@ export async function starRepository(
186206 // Authoritative: this call just starred the repo. Bumping the generation makes any
187207 // read that is still in flight discard its now-obsolete observation.
188208 generation += 1 ;
189- cached = { timestamp : deps . nowMs ( ) , state : "starred" } ;
209+ cached = { timestamp : resolved . nowMs ( ) , state : "starred" } ;
190210 return { ok : true , status : { state : "starred" , repo : STAR_REPO , url : STAR_REPO_URL } } ;
191211}
0 commit comments