Skip to content

Commit 8bcd1f3

Browse files
authored
fix(runtime): handle RTK missing hook (#1539)
1 parent cc22371 commit 8bcd1f3

2 files changed

Lines changed: 106 additions & 26 deletions

File tree

src/main/lib/agentRuntime/rtkRuntimeService.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ interface PrepareShellCommandResult {
5858
rtkFallbackReason?: string
5959
}
6060

61+
type RtkRewriteResult =
62+
| { status: 'rewritten'; command: string }
63+
| { status: 'bypass'; message: string }
64+
| { status: 'failure'; message: string }
65+
6166
interface RtkRuntimeServiceDeps {
6267
runtimeHelper?: Pick<
6368
RuntimeHelper,
@@ -112,6 +117,37 @@ function getErrorMessage(error: unknown): string {
112117
return String(error)
113118
}
114119

120+
function classifyRtkRewriteResult(result: CommandResult): RtkRewriteResult {
121+
const stdout = result.stdout.trim()
122+
const stderr = result.stderr.trim()
123+
124+
if (result.code === 0 && stdout) {
125+
return {
126+
status: 'rewritten',
127+
command: stdout
128+
}
129+
}
130+
131+
if (result.code === 3 && stdout && /No hook installed/i.test(stderr)) {
132+
return {
133+
status: 'rewritten',
134+
command: stdout
135+
}
136+
}
137+
138+
if (result.code === 1) {
139+
return {
140+
status: 'bypass',
141+
message: 'RTK rewrite did not match this command'
142+
}
143+
}
144+
145+
return {
146+
status: 'failure',
147+
message: stderr || stdout || 'rtk rewrite failed'
148+
}
149+
}
150+
115151
async function defaultRunCommand(
116152
command: string,
117153
args: string[],
@@ -371,24 +407,22 @@ export class RtkRuntimeService {
371407
env: preparedEnv,
372408
timeoutMs: RTK_REWRITE_TIMEOUT_MS
373409
})
410+
const rewrite = classifyRtkRewriteResult(rewriteResult)
374411

375-
if (rewriteResult.code === 0) {
376-
const rewritten = rewriteResult.stdout.trim()
377-
if (rewritten) {
378-
this.recordRuntimeSuccess()
379-
return {
380-
originalCommand: rawCommand,
381-
command: rewritten,
382-
env: preparedEnv,
383-
rewritten: true,
384-
usedRtk: true,
385-
rtkApplied: true,
386-
rtkMode: 'rewrite'
387-
}
412+
if (rewrite.status === 'rewritten') {
413+
this.recordRuntimeSuccess()
414+
return {
415+
originalCommand: rawCommand,
416+
command: rewrite.command,
417+
env: preparedEnv,
418+
rewritten: true,
419+
usedRtk: true,
420+
rtkApplied: true,
421+
rtkMode: 'rewrite'
388422
}
389423
}
390424

391-
if (rewriteResult.code === 1) {
425+
if (rewrite.status === 'bypass') {
392426
this.recordRuntimeSuccess()
393427
return {
394428
originalCommand: rawCommand,
@@ -398,13 +432,11 @@ export class RtkRuntimeService {
398432
usedRtk: false,
399433
rtkApplied: false,
400434
rtkMode: 'bypass',
401-
rtkFallbackReason: 'RTK rewrite did not match this command'
435+
rtkFallbackReason: rewrite.message
402436
}
403437
}
404438

405-
const failureMessage =
406-
rewriteResult.stderr.trim() || rewriteResult.stdout.trim() || 'rtk rewrite failed'
407-
this.recordRuntimeFailure('rewrite', failureMessage)
439+
this.recordRuntimeFailure('rewrite', rewrite.message)
408440
return {
409441
originalCommand: rawCommand,
410442
command: rawCommand,
@@ -413,7 +445,7 @@ export class RtkRuntimeService {
413445
usedRtk: false,
414446
rtkApplied: false,
415447
rtkMode: 'bypass',
416-
rtkFallbackReason: failureMessage
448+
rtkFallbackReason: rewrite.message
417449
}
418450
} catch (error) {
419451
const failureMessage = getErrorMessage(error)
@@ -569,11 +601,9 @@ export class RtkRuntimeService {
569601
env: baseEnv,
570602
timeoutMs: RTK_HEALTH_TIMEOUT_MS
571603
})
572-
if (rewrite.code !== 0 || !rewrite.stdout.trim()) {
573-
throw new RtkHealthCheckError(
574-
'rewrite',
575-
rewrite.stderr.trim() || rewrite.stdout.trim() || 'rtk rewrite failed'
576-
)
604+
const rewriteCheck = classifyRtkRewriteResult(rewrite)
605+
if (rewriteCheck.status !== 'rewritten') {
606+
throw new RtkHealthCheckError('rewrite', rewriteCheck.message)
577607
}
578608

579609
const resolvedRtk = await this.runCommandImpl('rtk', ['--version'], {

test/main/lib/agentRuntime/rtkRuntimeService.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function createHealthCheckService(tempRoot: string, runCommand = vi.fn()) {
9999
})
100100
}
101101

102-
function createHealthCheckRunCommand() {
102+
function createHealthCheckRunCommand(rewriteResult = createCommandResult(0, 'rtk git status\n')) {
103103
return vi.fn(async (command: string, args: string[]) => {
104104
if (args[0] === 'ls') {
105105
return createCommandResult(
@@ -114,7 +114,7 @@ function createHealthCheckRunCommand() {
114114
}
115115

116116
if (command === '/runtime/rtk/rtk.exe' && args[0] === 'rewrite' && args[1] === 'git status') {
117-
return createCommandResult(0, 'rtk git status\n')
117+
return rewriteResult
118118
}
119119

120120
if (command === 'rtk' && args[0] === '--version') {
@@ -175,6 +175,29 @@ describe('RtkRuntimeService', () => {
175175
expect(result.rtkMode).toBe('rewrite')
176176
})
177177

178+
it('uses rewrite output when RTK reports a missing global hook', async () => {
179+
const runCommand = vi.fn().mockResolvedValue({
180+
code: 3,
181+
stdout: 'rtk git status\n',
182+
stderr: 'No hook installed\n',
183+
signal: null,
184+
timedOut: false
185+
})
186+
const service = createService(runCommand)
187+
188+
const result = await service.prepareShellCommand(
189+
'git status',
190+
{},
191+
{ getSetting: vi.fn().mockReturnValue(true) }
192+
)
193+
194+
expect(result.originalCommand).toBe('git status')
195+
expect(result.command).toBe('rtk git status')
196+
expect(result.rewritten).toBe(true)
197+
expect(result.rtkApplied).toBe(true)
198+
expect(result.rtkMode).toBe('rewrite')
199+
})
200+
178201
it.each([
179202
'find . -type f -name "*.ts" -o -name "*.vue"',
180203
'find . -type f ! -name "*.test.ts"',
@@ -240,4 +263,31 @@ describe('RtkRuntimeService', () => {
240263
fs.rmSync(tempRoot, { recursive: true, force: true })
241264
}
242265
})
266+
267+
it('keeps bundled RTK healthy when rewrite reports a missing global hook', async () => {
268+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'deepchat-rtk-health-test-'))
269+
const runCommand = createHealthCheckRunCommand(
270+
createCommandResult(3, 'rtk git status\n', 'No hook installed\n')
271+
)
272+
const service = createHealthCheckService(tempRoot, runCommand)
273+
274+
try {
275+
const state = await service.startHealthCheck()
276+
277+
expect(state).toMatchObject({
278+
health: 'healthy',
279+
source: 'bundled',
280+
failureStage: null,
281+
failureMessage: null
282+
})
283+
expect(
284+
runCommand.mock.calls.some(
285+
([command, args]) =>
286+
command === '/runtime/rtk/rtk.exe' && args[0] === 'rewrite' && args[1] === 'git status'
287+
)
288+
).toBe(true)
289+
} finally {
290+
fs.rmSync(tempRoot, { recursive: true, force: true })
291+
}
292+
})
243293
})

0 commit comments

Comments
 (0)