Files
orca/src/shared/qoder-agent.test.ts
T
Neil ebeea319b2 Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback.

Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path.

Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior.

Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented.
2026-10-04 19:50:45 -07:00

267 lines
9.7 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, buildAgentResumeStartupPlan } 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 [
'qoder',
'/home/dev/.qoder/entry/qoder',
'C:\\Qoder\\qoder.cmd',
'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(isExpectedAgentProcess(command, 'qoder')).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()
expect(recognizeAgentProcessFromCommandLine('qoder-unrelated')).toBeNull()
})
it.each(['qoder', 'qodercli'])(
'excludes headless %s and preserves interactive resume',
(command) => {
for (const args of [
'--print hello',
'-p hello',
'--remote task',
'--list-sessions',
'--input-format stream-json',
'--output-format stream-json',
'--remote-control session'
]) {
expect(recognizeAgentProcessFromCommandLine(`${command} ${args}`)).toBeNull()
}
expect(
recognizeAgentProcessFromCommandLine(`${command} --resume proof --prompt-interactive hello`)
?.agent
).toBe('qoder')
}
)
it.each(['ide', 'chat', 'serve-web', 'tunnel', './project', '/home/dev/project', 'C:\\project'])(
'does not identify qoder %s as an agent',
(subcommand) => {
expect(recognizeAgentProcessFromCommandLine(`qoder ${subcommand}`)).toBeNull()
expect(
recognizeAgentProcessFromCommandLine(`qoder ${subcommand}`, {
includeHeadlessOneShot: true
})
).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.each(['darwin', 'linux', 'win32'] as const)(
'starts and resumes a configured modern Qoder entry on %s',
(platform) => {
const plan = buildAgentStartupPlan({
agent: 'qoder',
prompt: 'proof',
cmdOverrides: { qoder: 'qoder' },
platform,
isRemote: platform === 'linux'
})
expect(plan?.launchCommand).toBe("qoder --prompt-interactive 'proof'")
expect(plan?.launchConfig.agentCommand).toBe('qoder')
const resume = buildAgentResumeStartupPlan({
agent: 'qoder',
providerSession: { key: 'session_id', id: 'proof-session' },
cmdOverrides: {},
agentCommand: plan?.launchConfig.agentCommand,
platform,
isRemote: platform === 'linux'
})
expect(resume?.launchCommand).toBe("qoder '--resume' 'proof-session'")
expect(isExpectedAgentProcess('qodercli-1.1.65', resume?.expectedProcess ?? '')).toBe(true)
}
)
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'
})
})
it.each(['qoder-cn', 'qwen-code'] as const)('keeps %s lifecycle and resume attributed', (agent) => {
const state = createHookListenerState()
const send = (hook_event_name: string, extra = {}) =>
normalizeAndAccept(state, agent, {
hook_event_name,
session_id: 'provider-proof',
...extra
})
expect(send('SessionStart', { source: 'startup' })?.payload).toMatchObject({
agentType: agent,
state: 'done',
sessionBoundary: true
})
expect(send('UserPromptSubmit', { prompt: 'Create proof.txt' })?.payload).toMatchObject({
agentType: agent,
state: 'working'
})
expect(send('PermissionRequest', { tool_name: 'Write' })?.payload).toMatchObject({
agentType: agent,
state: 'waiting'
})
expect(send('Stop', { last_assistant_message: 'Done' })?.payload).toMatchObject({
agentType: agent,
state: 'done'
})
expect(send('SessionStart', { source: 'resume' })?.providerSession?.id).toBe('provider-proof')
expect(getAgentResumeArgv(agent, { key: 'session_id', id: 'provider-proof' })).toEqual([
agent === 'qoder-cn' ? 'qoderclicn' : 'qwen',
'--resume',
'provider-proof'
])
})
it.each([
'qoderclicn',
'qodercn',
'/home/test/.qoder-cn/bin/qoderclicn-1.1.65',
'C:\\Qoder\\qoderclicn.exe'
])('recognizes the China executable %s', (command) => {
expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('qoder-cn')
expect(recognizeAgentProcessFromCommandLine(`${command} --print hello`)).toBeNull()
})