ci: make reference and worktree tests portable (#7381)

* fix(forge): make reference and worktree tests portable

* test(worktree): cover error and text cancellation reasons

* test(forge): brace void arrow bodies in the reference lookup spec

* test(ci): expect the joined version script path in the pre-merge check

---------

Co-authored-by: imccyu <[email protected]>
This commit is contained in:
Yichen Jiangandimccyu authored and GitHub committed 2026-10-27 02:20:02 +08:00
1 parent 9c69007eca
commit e2c4d0538f
4 files changed
+200 -5

No files matched your search

@@ -32,6 +32,7 @@ describe('commandEvidence', () => {
['git --no-pager push --force-with-lease', pushed],
['cd repo && git push && gh pr create --fill', { ...created, pushes: true }],
['gh pr view 12 --json url', { urls: 'referenced', pushes: false, named: [] }],
['gh pr view 12 --repo octo/demo --json number', { urls: 'referenced', pushes: false, named: [] }],
[`gh pr view '${url}' --json title`, { urls: 'referenced', pushes: false, named: [url] }],
[`gh pr edit ${url}/files --add-label bug`, { urls: 'referenced', pushes: false, named: [url] }],
[`gh pr checkout ${url} && git push`, { urls: 'referenced', pushes: true, named: [url] }],
@@ -0,0 +1,191 @@
/** Reference observation through Cordis services without a platform shell or CLI process. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmRuntime, { ToolCallId } from '@deepseek-ai/dsh-llm'
import { pluginRecordOf, SessionId } from '@deepseek-ai/dsh-session'
import ShellExecutor from '@deepseek-ai/dsh-shell'
import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellRunResult } from '@deepseek-ai/dsh-shell'
import * as ShellEnv from '@deepseek-ai/dsh-shell-env'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import { bench, pullUrl } from './fixtures.ts'
const contexts: Context[] = []
const releases: (() => void)[] = []
afterEach(async () => {
for (const release of releases.splice(0)) release()
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
})
function output(overrides: Partial<ShellRunResult> = {}): ShellRunResult {
return {
exitCode: 0, signal: null, timedOut: false, aborted: false, timeoutMs: 1_234,
stdout: { text: JSON.stringify({ url: pullUrl(9) }), truncated: false },
stderr: { text: '', truncated: false },
...overrides,
}
}
function execution(result: () => Promise<ShellRunResult>): ShellExecution {
const reader = { readFrom: (from: number) => ({ text: '', nextOffset: from, lossy: false }) }
return {
status: 'completed', exitCode: 0, signal: null, done: Promise.resolve(),
observed: { stdout: reader, stderr: reader },
readOutput: () => ({ delta: '', lossy: false }), kill: () => false, result,
}
}
class LookupShell extends ShellExecutor {
resolve(request: ShellExecRequest): ShellExecSpec {
return {
...request,
workdir: request.workdir ?? process.cwd(), timeoutMs: request.timeoutMs ?? 1_234,
onExpiry: 'kill', stdoutMaxBytes: 64_000, sandboxPolicy: request.sandboxPolicy,
}
}
async execute(): Promise<ShellExecution> {
return execution(async () => output())
}
}
async function fixture() {
const b = await bench({ discoveryTimeoutMs: 1_234 })
const { ctx } = b
contexts.push(ctx)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LlmRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(ShellEnv, { dshHome: process.cwd() })
await ctx.plugin(LookupShell)
const { agent } = await ctx.agents.create({ sessionId: SessionId('reference-observer') })
const controller = new AbortController()
const exec: ToolExecution = {
name: 'bash', agent,
callId: ToolCallId('reference'), rootCallId: ToolCallId('reference'), token: Symbol('reference') as ToolExecutionToken,
arguments: { command: 'gh pr view 9 -R octo/demo --json number' }, signal: controller.signal,
}
const result: ToolExecutionResult = {
isError: false, content: [{ type: 'text', text: '{"number":9}' }],
value: { kind: 'foreground', cwd: process.cwd(), exitCode: 0, signal: null, timedOut: false, aborted: false },
}
const records = () => agent.session.snapshotEvents().flatMap((event) => {
const record = pluginRecordOf(event)
return record?.type === 'plugin:forge-session/binding' ? [record.data] : []
})
const execute = vi.spyOn(ctx.shell, 'execute')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
const emit = (call: ToolExecution = exec, outcome: ToolExecutionResult = result) => { ctx.emit('tools/result', call, outcome) }
return { ...b, agent, exec, result, controller, records, execute, warn, emit }
}
describe('PR reference lookup', () => {
it('records the resolved URL with the caller cwd and environment, without restoring a dismissed binding', async () => {
const f = await fixture()
const resolve = vi.spyOn(f.ctx.shell, 'resolve')
f.emit()
await vi.waitFor(() => { expect(f.records()).toEqual([{ action: 'reference', url: pullUrl(9) }]) })
expect(resolve).toHaveBeenCalledWith(expect.objectContaining({
command: 'env gh pr view 9 --repo octo/demo --json url', workdir: process.cwd(), timeoutMs: 1_234,
dshEnv: f.ctx.shellEnv.collect(f.exec),
}))
f.ctx.forgeSession.dismiss(f.agent.session, pullUrl(9))
f.emit()
expect(f.execute).toHaveBeenCalledTimes(2)
const returned = f.execute.mock.results[1]
if (returned?.type !== 'return') throw new Error('missing second lookup')
await (await returned.value).result()
await f.service.dispose()
expect(f.records()).toEqual([{ action: 'reference', url: pullUrl(9) }, { action: 'dismiss', url: pullUrl(9) }])
expect(f.warn).not.toHaveBeenCalled()
})
it.each([
['failed exit', { exitCode: 1 }],
['signal', { signal: 'SIGTERM' }],
['timeout', { timedOut: true }],
['abort result', { aborted: true }],
['truncated output', { stdout: { text: '{', truncated: true } }],
['invalid JSON', { stdout: { text: '{', truncated: false } }],
['missing URL', { stdout: { text: '{}', truncated: false } }],
['non-PR URL', { stdout: { text: '{"url":"https://fake.test/octo/demo/issues/9"}', truncated: false } }],
] satisfies [string, Partial<ShellRunResult>][])('reports %s without changing bindings or the completed tool result', async (_label, overrides) => {
const f = await fixture()
f.execute.mockResolvedValue(execution(async () => output(overrides)))
f.emit()
await vi.waitFor(() => { expect(f.warn).toHaveBeenCalledWith(expect.stringContaining('PR reference lookup failed')) })
expect(f.records()).toEqual([])
expect(f.result.isError).toBe(false)
})
it('reports a shell preparation rejection that is not an Error', async () => {
const f = await fixture()
f.execute.mockRejectedValue('unavailable')
f.emit()
await vi.waitFor(() => { expect(f.warn).toHaveBeenCalledWith(expect.stringContaining('unavailable')) })
expect(f.records()).toEqual([])
})
it.each(['caller', 'service'] as const)('joins pending preparation after %s cancellation and ignores its late result', async (owner) => {
const f = await fixture()
const pending = Promise.withResolvers<ShellExecution>()
const started = Promise.withResolvers<ShellExecSpec>()
releases.push(() => { pending.resolve(execution(async () => output())) })
f.execute.mockImplementation((spec) => { started.resolve(spec); return pending.promise })
f.emit()
const spec = await started.promise
const aborted = Promise.withResolvers<undefined>()
spec.signal!.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true })
if (owner === 'caller') f.controller.abort()
let disposed = false
const disposal = f.service.dispose().then(() => { disposed = true })
await aborted.promise
expect(disposed).toBe(false)
pending.resolve(execution(async () => output()))
await disposal
expect(f.records()).toEqual([])
expect(f.warn).not.toHaveBeenCalled()
})
it('joins a cancelled preparation rejection without warning', async () => {
const f = await fixture()
const pending = Promise.withResolvers<ShellExecution>()
const started = Promise.withResolvers<undefined>()
releases.push(() => { pending.resolve(execution(async () => output())) })
f.execute.mockImplementation(() => { started.resolve(undefined); return pending.promise })
f.emit()
await started.promise
f.controller.abort()
pending.reject(new Error('cancelled during preparation'))
await f.service.dispose()
expect(f.records()).toEqual([])
expect(f.warn).not.toHaveBeenCalled()
})
it('skips unsupported, unsuccessful, already identified, nested, or cancelled calls', async () => {
const f = await fixture()
f.emit({ ...f.exec, name: 'pwsh' })
const { agent: _agent, ...withoutAgent } = f.exec
f.emit(withoutAgent)
f.emit({ ...f.exec, parent: f.exec.token })
f.emit(f.exec, { isError: true, error: { message: 'failed' }, content: [] })
f.emit({ ...f.exec, arguments: {} })
f.emit(f.exec, { isError: false, value: { kind: 'background' }, content: [] })
f.emit({ ...f.exec, arguments: { command: 'gh pr view 9' } }, { ...f.result, content: [{ type: 'reasoning', text: 'thinking' }] })
f.emit({ ...f.exec, arguments: { command: 'gh pr view 9 -R fake.test/octo/demo' } })
f.emit(f.exec, { ...f.result, content: [{ type: 'text', text: JSON.stringify({ url: pullUrl(9) }) }] })
f.controller.abort()
f.emit()
await f.service.dispose()
expect(f.execute).not.toHaveBeenCalled()
expect(f.records()).toEqual([])
expect(f.warn).not.toHaveBeenCalled()
})
})
@@ -454,7 +454,7 @@ describe('restoring removed worktrees', () => {
expect(ctx.workingDirectory.get(agent.session)).toBe(root)
})
it('retains a caller cancellation reason when restoration is cancelled before its allocation starts', async () => {
it.each([new Error('caller stopped'), 'caller stopped'])('retains cancellation reason %s when restoration is cancelled before its allocation starts', async (reason) => {
const root = await repo()
ctx = await harness(root)
const agent = testAgent(ctx, root)
@@ -463,7 +463,7 @@ describe('restoring removed worktrees', () => {
const controller = new AbortController()
const resolveExecutable = ctx.subprocess.resolveExecutable.bind(ctx.subprocess)
const probe = vi.spyOn(ctx.subprocess, 'resolveExecutable').mockImplementation(async (command, env, signal) => {
if (command === 'node') controller.abort('caller stopped')
if (command === 'node') controller.abort(reason)
return resolveExecutable(command, env, signal)
})
try {
@@ -559,13 +559,16 @@ it('does not remove a registration when its directory reappears during restorati
const created = await ctx.worktrees.create(agent, { name: 'topic' })
await rm(created.path, { recursive: true, force: true })
const resolve = ctx.fs.resolve.bind(ctx.fs)
const target = await resolve(created.path)
let targetReads = 0
const probe = vi.spyOn(ctx.fs, 'resolve').mockImplementation(async (path, options) => {
if (path === created.path && ++targetReads === 2) await mkdir(created.path)
return resolve(path, options)
const resolved = await resolve(path, options)
if (resolved.targetKey === target.targetKey && ++targetReads === 2) await mkdir(created.path)
return resolved
})
try {
await expect(ctx.worktrees.restore(agent, created)).rejects.toThrow('worktree path already exists')
expect(targetReads).toBe(2)
expect(await git(root, 'worktree', 'list', '--porcelain')).toContain(`worktree ${created.path.split(sep).join('/')}`)
} finally { probe.mockRestore() }
})
+1 -1
View File
@@ -45,7 +45,7 @@ describe('pre-merge check', () => {
it('checks the versions before it installs, and installs before the builds', () => {
const steps = preMergeSteps('/repo')
expect(steps.map(step => step.name)).toEqual(['versions', 'install', 'host contracts', 'client'])
expect(steps[0]).toEqual({ name: 'versions', command: process.execPath, args: ['/repo/scripts/verify-version-sync.ts', '/repo'] })
expect(steps[0]).toEqual({ name: 'versions', command: process.execPath, args: [join('/repo', 'scripts', 'verify-version-sync.ts'), '/repo'] })
expect(steps[1]).toEqual({ name: 'install', command: 'pnpm', args: ['install', '--frozen-lockfile'] })
expect(steps[3]?.args).toEqual(['exec', 'tsx', 'scripts/compile-referenced-projects.ts', 'client'])
})