@@ -79,19 +79,8 @@ async function spawnGh(args: string[], timeoutMs: number): Promise<{ status: num
7979 }
8080}
8181
82- const defaultDeps : StarDeps = { runGh : spawnGh , nowMs : ( ) => Date . now ( ) } ;
83- /** Test-only override so route tests never spawn a real `gh` (Windows CI hangs at AUTH_TIMEOUT). */
84- let depsOverride : StarDeps | null = null ;
85-
86- function activeDeps ( deps ?: StarDeps ) : StarDeps {
87- return deps ?? depsOverride ?? defaultDeps ;
88- }
89-
90- /** Swap the default `gh` runner for tests; pass `null` to restore production deps. */
91- export function setStarDepsForTests ( deps : StarDeps | null ) : void {
92- depsOverride = deps ;
93- invalidateStarStatusCache ( ) ;
94- }
82+ const productionDeps : StarDeps = { runGh : spawnGh , nowMs : ( ) => Date . now ( ) } ;
83+ let defaultDeps = productionDeps ;
9584
9685let cached : { timestamp : number ; state : StarState } | null = null ;
9786/** Coalesces concurrent probes so parallel sidebar polls share one `gh` run. */
@@ -109,11 +98,10 @@ let generation = 0;
10998 * starred and 404 when not, so a non-zero exit is only meaningful once we know
11099 * the CLI is authenticated — hence the auth check first.
111100 */
112- export async function probeStarState ( deps ?: StarDeps ) : Promise < StarState > {
113- const d = activeDeps ( deps ) ;
114- const auth = await d . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
101+ export async function probeStarState ( deps : StarDeps = defaultDeps ) : Promise < StarState > {
102+ const auth = await deps . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
115103 if ( ! auth || auth . status !== 0 ) return "unauthenticated" ;
116- const starred = await d . runGh (
104+ const starred = await deps . runGh (
117105 [ "api" , "--hostname" , GH_HOSTNAME , `/user/starred/${ STAR_REPO } ` ] ,
118106 API_TIMEOUT_MS ,
119107 ) ;
@@ -122,9 +110,8 @@ export async function probeStarState(deps?: StarDeps): Promise<StarState> {
122110}
123111
124112/** Cached star state; `gh` is only spawned when the cache is cold or expired. */
125- export async function getStarStatus ( deps ?: StarDeps ) : Promise < StarStatus > {
126- const d = activeDeps ( deps ) ;
127- const now = d . nowMs ( ) ;
113+ export async function getStarStatus ( deps : StarDeps = defaultDeps ) : Promise < StarStatus > {
114+ const now = deps . nowMs ( ) ;
128115 if ( cached && now - cached . timestamp < CACHE_TTL_MS ) {
129116 return { state : cached . state , repo : STAR_REPO , url : STAR_REPO_URL } ;
130117 }
@@ -137,7 +124,7 @@ export async function getStarStatus(deps?: StarDeps): Promise<StarStatus> {
137124 // The slot is cleared inside the same continuation that commits the cache. Using
138125 // `.finally()` for that defers it by a microtask, which leaves a window where the
139126 // next caller awaits an already-settled probe instead of starting a fresh one.
140- const probe = probeStarState ( d ) . then (
127+ const probe = probeStarState ( deps ) . then (
141128 state => {
142129 if ( inflight === probe ) inflight = null ;
143130 // A write landed while this read was in flight — its result is authoritative.
@@ -162,6 +149,17 @@ export function invalidateStarStatusCache(): void {
162149 inflight = null ;
163150}
164151
152+ /**
153+ * Route tests must not launch the user's `gh` executable: its installation,
154+ * authentication helper, and Windows shim are all outside the route contract.
155+ * Production keeps the real runner; tests install an explicit deterministic
156+ * dependency and must reset it afterwards.
157+ */
158+ export function setStarDepsForTests ( deps : StarDeps | null ) : void {
159+ defaultDeps = deps ?? productionDeps ;
160+ invalidateStarStatusCache ( ) ;
161+ }
162+
165163/**
166164 * Star the repository through the user's `gh` login. Returns the resulting
167165 * state so the caller does not need a second round trip; an unauthenticated
@@ -173,20 +171,19 @@ export function invalidateStarStatusCache(): void {
173171 * management API.
174172 */
175173export async function starRepository (
176- deps ? : StarDeps ,
174+ deps : StarDeps = defaultDeps ,
177175) : Promise < { ok : boolean ; status : StarStatus ; code ?: StarErrorCode } > {
178- const d = activeDeps ( deps ) ;
179- const auth = await d . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
176+ const auth = await deps . runGh ( [ "auth" , "status" , "--hostname" , GH_HOSTNAME ] , AUTH_TIMEOUT_MS ) ;
180177 if ( ! auth || auth . status !== 0 ) {
181178 generation += 1 ;
182- cached = { timestamp : d . nowMs ( ) , state : "unauthenticated" } ;
179+ cached = { timestamp : deps . nowMs ( ) , state : "unauthenticated" } ;
183180 return {
184181 ok : false ,
185182 status : { state : "unauthenticated" , repo : STAR_REPO , url : STAR_REPO_URL } ,
186183 code : "gh_unavailable" ,
187184 } ;
188185 }
189- const result = await d . runGh (
186+ const result = await deps . runGh (
190187 [ "api" , "--hostname" , GH_HOSTNAME , "-X" , "PUT" , `/user/starred/${ STAR_REPO } ` ] ,
191188 API_TIMEOUT_MS ,
192189 ) ;
@@ -201,6 +198,6 @@ export async function starRepository(
201198 // Authoritative: this call just starred the repo. Bumping the generation makes any
202199 // read that is still in flight discard its now-obsolete observation.
203200 generation += 1 ;
204- cached = { timestamp : d . nowMs ( ) , state : "starred" } ;
201+ cached = { timestamp : deps . nowMs ( ) , state : "starred" } ;
205202 return { ok : true , status : { state : "starred" , repo : STAR_REPO , url : STAR_REPO_URL } } ;
206203}
0 commit comments