mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 00:02:21 +00:00
feat: add first-class Qoder CLI support Integrate Qoder launch, identity, canonical hook status, trust and resume. Verify with captured Qoder 1.1.64 transcripts and hidden Electron sidebar checks. Builds on and cross-reviews #7502, #8611, #9655, #12910, #13311 and #15291. Co-authored-by: dalveytech-vincent <vincent@dalveytech.com> Co-authored-by: Eridanus117 <45489268+Eridanus117@users.noreply.github.com> Co-authored-by: xingqingzzp-gif <xingqingzzp-gif@users.noreply.github.com> Co-authored-by: jyang2004 <jyang2004@users.noreply.github.com> Co-authored-by: yunqian <yunqian@alibaba-inc.com> Co-authored-by: huzhening.hzn <huzhening.hzn@alibaba-inc.com>
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
recognizeAgentProcessFromCommandLine,
|
|
isExpectedAgentProcess
|
|
} from './agent-process-recognition'
|
|
import {
|
|
getAgentLabel,
|
|
isGeminiTerminalTitle,
|
|
normalizeTerminalTitle,
|
|
detectAgentStatusFromTitle
|
|
} from './agent-detection'
|
|
import { buildAgentStartupPlan } from './tui-agent-startup'
|
|
import { getAgentResumeArgv } from './agent-session-resume'
|
|
import { createHookListenerState } from './agent-hook-listener/listener-state'
|
|
import { normalizeAndAccept } from './agent-hook-listener-test-harness'
|
|
|
|
describe('Qoder agent identity and lifecycle', () => {
|
|
it('recognizes the installed versioned binary on native platforms and excludes headless runs', () => {
|
|
for (const command of [
|
|
'qodercli',
|
|
'/home/dev/.qoder/bin/qodercli/qodercli-1.1.64',
|
|
'C:\\Qoder\\qodercli-1.1.64.exe'
|
|
]) {
|
|
expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('qoder')
|
|
expect(isExpectedAgentProcess(command, 'qodercli')).toBe(true)
|
|
}
|
|
expect(recognizeAgentProcessFromCommandLine('qodercli --print hello')).toBeNull()
|
|
for (const args of [
|
|
'-o json',
|
|
'--input-format=stream-json',
|
|
'--remote-control session',
|
|
'--acp',
|
|
'--delete-session 1'
|
|
]) {
|
|
expect(recognizeAgentProcessFromCommandLine(`qodercli ${args}`)).toBeNull()
|
|
}
|
|
expect(recognizeAgentProcessFromCommandLine('qodercli --prompt-interactive hello')?.agent).toBe(
|
|
'qoder'
|
|
)
|
|
expect(recognizeAgentProcessFromCommandLine('qodercli-unrelated')).toBeNull()
|
|
})
|
|
|
|
it.each([
|
|
['◇ Qoder CLI | Ready', 'idle'],
|
|
['✦ Implement feature | Thinking', 'working'],
|
|
['▲ Implement feature | Confirm', 'permission']
|
|
] as const)('keeps %s attributed through title storage', (title, status) => {
|
|
const stored = normalizeTerminalTitle(title.padEnd(80))
|
|
expect(getAgentLabel(stored)).toBe('Qoder CLI')
|
|
expect(isGeminiTerminalTitle(title)).toBe(false)
|
|
expect(detectAgentStatusFromTitle(stored)).toBe(status)
|
|
expect(normalizeTerminalTitle(stored)).toBe(stored)
|
|
})
|
|
|
|
it('keeps upstream Gemini titles and arbitrary triangle text unchanged', () => {
|
|
expect(isGeminiTerminalTitle('✦ Working (project)')).toBe(true)
|
|
expect(getAgentLabel('deploy ▲ complete')).not.toBe('Qoder CLI')
|
|
})
|
|
|
|
it('tracks start, work, permission, completion and resume without becoming Claude', () => {
|
|
const state = createHookListenerState()
|
|
const session = { session_id: 'qoder-session', transcript_path: '/tmp/qoder-session.jsonl' }
|
|
const send = (hook_event_name: string, extra = {}) =>
|
|
normalizeAndAccept(state, 'qoder', { ...session, hook_event_name, ...extra })
|
|
const started = send('SessionStart', { source: 'startup' })
|
|
expect(started?.payload).toMatchObject({
|
|
agentType: 'qoder',
|
|
state: 'done',
|
|
sessionBoundary: true
|
|
})
|
|
expect(started?.providerSession).toMatchObject({ id: 'qoder-session' })
|
|
expect(send('UserPromptSubmit', { prompt: 'Fix the bug' })?.payload).toMatchObject({
|
|
state: 'working',
|
|
agentType: 'qoder',
|
|
prompt: 'Fix the bug'
|
|
})
|
|
expect(
|
|
send('PermissionRequest', { tool_name: 'Bash', tool_input: { command: 'pwd' } })?.payload
|
|
).toMatchObject({ state: 'waiting', toolName: 'Bash' })
|
|
expect(send('PostToolUse', { tool_name: 'Bash' })?.payload.state).toBe('working')
|
|
expect(send('SessionStart', { source: 'compact' })).toBeNull()
|
|
expect(send('Stop', { last_assistant_message: 'Fixed.' })?.payload).toMatchObject({
|
|
state: 'done',
|
|
lastAssistantMessage: 'Fixed.'
|
|
})
|
|
expect(send('StopFailure', { error: 'Unavailable' })?.payload.mainAgent).toMatchObject({
|
|
outcome: 'failure'
|
|
})
|
|
expect(getAgentResumeArgv('qoder', { key: 'session_id', id: 'qoder-session' })).toEqual([
|
|
'qodercli',
|
|
'--resume',
|
|
'qoder-session'
|
|
])
|
|
})
|
|
})
|
|
|
|
it('replays captured unauthenticated startup, prompt, idle and resume hooks', () => {
|
|
const state = createHookListenerState()
|
|
const lines = readFileSync(
|
|
join(__dirname, '__fixtures__', 'qoder-no-account-hooks.jsonl'),
|
|
'utf8'
|
|
)
|
|
.trim()
|
|
.split('\n')
|
|
const events = lines.map((line) => normalizeAndAccept(state, 'qoder', JSON.parse(line)))
|
|
expect(events.map((event) => event?.payload.state)).toEqual([
|
|
'done',
|
|
'working',
|
|
'done',
|
|
'done',
|
|
'done',
|
|
'done'
|
|
])
|
|
expect(events.every((event) => event?.payload.agentType === 'qoder')).toBe(true)
|
|
expect(events[4]?.payload.sessionBoundary).toBe(true)
|
|
expect(events[4]?.providerSession?.id).toBe(events[0]?.providerSession?.id)
|
|
})
|
|
|
|
it.each(['darwin', 'linux', 'win32'] as const)(
|
|
'launches a Qoder prompt safely on %s',
|
|
(platform) => {
|
|
const plan = buildAgentStartupPlan({
|
|
agent: 'qoder',
|
|
prompt: "fix Bob's branch; echo $HOME",
|
|
cmdOverrides: {},
|
|
platform
|
|
})
|
|
expect(plan?.expectedProcess).toBe('qodercli')
|
|
expect(plan?.followupPrompt).toBeNull()
|
|
expect(plan?.launchCommand).toBe(
|
|
platform === 'win32'
|
|
? "qodercli --prompt-interactive 'fix Bob''s branch; echo $HOME'"
|
|
: `qodercli --prompt-interactive 'fix Bob'"'"'s branch; echo $HOME'`
|
|
)
|
|
}
|
|
)
|
|
|
|
it('does not rewrite DSH titles containing a pipe as Qoder', () => {
|
|
const title = '✦ 🐋 refactor parser | run tests'
|
|
expect(normalizeTerminalTitle(title)).toBe(title)
|
|
expect(getAgentLabel(title)).not.toBe('Qoder CLI')
|
|
expect(detectAgentStatusFromTitle(title)).not.toBe('working')
|
|
})
|
|
|
|
it('settles manual compaction without interrupting automatic compaction', () => {
|
|
const state = createHookListenerState()
|
|
const send = (hook_event_name: string, extra = {}) =>
|
|
normalizeAndAccept(state, 'qoder', { hook_event_name, ...extra })
|
|
expect(send('UserPromptSubmit', { prompt: 'Refactor' })?.payload.state).toBe('working')
|
|
expect(send('PreCompact', { trigger: 'manual' })).toBeNull()
|
|
expect(send('PostCompact', { trigger: 'auto' })).toBeNull()
|
|
expect(send('SessionStart', { source: 'compact' })).toBeNull()
|
|
expect(send('PostCompact', { trigger: 'manual' })?.payload).toMatchObject({
|
|
state: 'done',
|
|
sessionBoundary: true,
|
|
agentType: 'qoder'
|
|
})
|
|
})
|