fix(ci): make runtime validation portable to Linux

This commit is contained in:
程序员阿江(Relakkes)
2026-09-07 21:56:11 +08:00
parent 9e4f83bbe3
commit 8a4be0dee7
8 changed files with 77 additions and 28 deletions
+7
View File
@@ -126,8 +126,13 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Install ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Install adapter dependencies
working-directory: adapters
run: bun install --frozen-lockfile
@@ -290,6 +295,8 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Install ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
+19 -8
View File
@@ -70,15 +70,26 @@ describe('PR quality workflow', () => {
}
})
test('installs adapter dependencies before root server tests that import adapters', () => {
test('installs imported workspace dependencies and ripgrep before runtime and coverage tests', () => {
const jobs = workflowJobs(readFileSync('.github/workflows/pr-quality.yml', 'utf8'))
const steps = jobs['server-checks'].steps ?? []
const adapterInstall = steps.findIndex(step =>
step['working-directory'] === 'adapters' && step.run === 'bun install --frozen-lockfile',
)
const serverCheck = steps.findIndex(step => step.run === 'bun run check:server')
expect(adapterInstall).toBeGreaterThanOrEqual(0)
expect(adapterInstall).toBeLessThan(serverCheck)
for (const [job, command] of [
['server-checks', 'bun run check:server'],
['coverage-checks', 'bun run check:coverage'],
]) {
const steps = jobs[job].steps ?? []
const check = steps.findIndex(step => step.run === command)
expect(check).toBeGreaterThanOrEqual(0)
for (const workspace of ['desktop', 'adapters']) {
const install = steps.findIndex(step =>
step['working-directory'] === workspace && step.run === 'bun install --frozen-lockfile',
)
expect(install).toBeGreaterThanOrEqual(0)
expect(install).toBeLessThan(check)
}
const ripgrep = steps.findIndex(step => step.run?.includes('apt-get install -y ripgrep'))
expect(ripgrep).toBeGreaterThanOrEqual(0)
expect(ripgrep).toBeLessThan(check)
}
})
test('keeps coverage artifacts observable in CI', () => {
+7 -5
View File
@@ -2250,12 +2250,14 @@ describe('TeamService', () => {
blocks: [],
blockedBy: [],
})
expect(await readTaskListSnapshot(teamName)).toMatchObject({
const activeSnapshot = await readTaskListSnapshot(teamName)
expect(activeSnapshot.tasks).toHaveLength(2)
expect(activeSnapshot).toMatchObject({
revision: 2,
tasks: [
{ id: '1', subject: 'Only generation-two task' },
{ id: '2', subject: 'Active generation-two writer' },
],
tasks: expect.arrayContaining([
expect.objectContaining({ id: '1', subject: 'Only generation-two task' }),
expect.objectContaining({ id: '2', subject: 'Active generation-two writer' }),
]),
})
} finally {
writerResource.emitDestroy()
+9 -6
View File
@@ -478,27 +478,30 @@ describe('Task tool execution ordering', () => {
expect(listed.data.taskListSnapshotRevision).toBe(2)
expect(updated.data.taskListMutationRevision).toBe(3)
expect(afterUpdate.data.taskListSnapshotRevision).toBe(3)
expect(listed.data.tasks).toEqual([
expect(listed.data.tasks).toHaveLength(2)
expect(listed.data.tasks).toEqual(expect.arrayContaining([
expect.objectContaining({ id: taskId, status: 'pending' }),
expect.objectContaining({ id: dependent.data.task.id, status: 'pending' }),
])
expect(afterUpdate.data.tasks).toEqual([
]))
expect(afterUpdate.data.tasks).toHaveLength(2)
expect(afterUpdate.data.tasks).toEqual(expect.arrayContaining([
expect.objectContaining({ id: taskId, status: 'in_progress' }),
expect.objectContaining({
id: dependent.data.task.id,
blockedBy: [taskId],
}),
])
]))
appState = {
...appState,
teamContext: { teamName: taskListId },
}
const deleted = await TeamDeleteTool.call({}, context)
expect(deleted.data.finalTasks).toHaveLength(2)
expect(deleted.data).toMatchObject({
success: true,
team_name: taskListId,
finalTasks: [
finalTasks: expect.arrayContaining([
expect.objectContaining({
id: taskId,
status: 'in_progress',
@@ -508,7 +511,7 @@ describe('Task tool execution ordering', () => {
id: dependent.data.task.id,
blockedBy: [taskId],
}),
],
]),
})
expect(Number.isFinite(Date.parse(deleted.data.taskListSnapshotAt ?? ''))).toBe(true)
expect(deleted.data.taskListSnapshotRevision).toBe(3)
@@ -34,6 +34,11 @@ const originalSessionId = getSessionId()
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0)
// A Linux container's init may defer reaping an exited orphan. A zombie
// cannot hold the socket open and is no longer a running daemon.
if (process.platform === 'linux') {
return !/^\d+ \(.*\) Z /.test(fs.readFileSync(`/proc/${pid}/stat`, 'utf8'))
}
return true
} catch {
return false
@@ -75,7 +80,14 @@ async function launchDetachedDaemon(socketPath: string): Promise<number> {
encoding: 'utf8',
})
expect(parent.status).toBe(0)
expect(Number.parseInt(parent.stdout.trim(), 10)).toBe(1)
const parentPid = Number.parseInt(parent.stdout.trim(), 10)
if (process.platform === 'darwin') {
expect(parentPid).toBe(1)
} else {
// Linux can reparent to a container subreaper rather than PID 1.
expect(parentPid).toBeGreaterThan(0)
expect(parentPid).not.toBe(result.pid)
}
return pid
}
@@ -143,8 +155,10 @@ afterEach(async () => {
runtimeRoot = undefined
})
describe.skipIf(process.platform !== 'darwin')(
'cu-helper launchd-owned daemon lifecycle',
// The fixture models launchd ownership with a detached POSIX process; it does
// not invoke launchctl or native Computer Use, so Linux exercises it too.
describe.skipIf(process.platform === 'win32')(
'cu-helper detached daemon lifecycle',
() => {
test('startup enumeration cannot poison the first resumed turn across shutdown and restart', async () => {
runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-haha-cu-process-'))
+13 -2
View File
@@ -17,7 +17,8 @@ describe('setupComputerUseMCP runtime capability', () => {
})
expect(Object.keys(result.mcpConfig)).toEqual(['computer-use'])
expect(result.allowedTools.length).toBeGreaterThan(0)
expect(result.allowedTools).toContain('mcp__computer-use__get_app_state')
expect(result.allowedTools).not.toContain('mcp__computer-use__screenshot')
})
test('keeps the Windows compatibility engine available without a macOS helper', () => {
@@ -29,6 +30,16 @@ describe('setupComputerUseMCP runtime capability', () => {
})
expect(Object.keys(result.mcpConfig)).toEqual(['computer-use'])
expect(result.allowedTools.length).toBeGreaterThan(0)
expect(result.allowedTools).toContain('mcp__computer-use__screenshot')
expect(result.allowedTools).not.toContain('mcp__computer-use__get_app_state')
})
test('does not resolve a helper or advertise tools on unsupported platforms', () => {
expect(setupComputerUseMCP({
platform: 'linux',
resolveMacosNativeBinary: () => {
throw new Error('must not resolve a macOS helper on Linux')
},
})).toEqual({ mcpConfig: {}, allowedTools: [] })
})
})
+1 -1
View File
@@ -41,7 +41,7 @@ export function setupComputerUseMCP(deps: SetupComputerUseDeps = {}): {
}
const allowedTools = buildPlatformComputerUseTools(
getCliComputerUseCapabilities(),
getCliComputerUseCapabilities(platform),
getChicagoCoordinateMode(),
).map(t => buildMcpToolName(COMPUTER_USE_MCP_SERVER_NAME, t.name))
+4 -3
View File
@@ -692,7 +692,7 @@ describe('in-process teammate task claiming', () => {
status: 'in_progress',
})
await updateTask(taskListId, explicitAssignment, { status: 'completed' })
await createTask(taskListId, {
const followUpTaskId = await createTask(taskListId, {
subject: 'Audit workflow',
description: 'Audit workflow changes',
status: 'pending',
@@ -711,8 +711,9 @@ describe('in-process teammate task claiming', () => {
expect(prompt).toContain('Audit workflow')
const claimedTasks = await listTasks(taskListId)
expect(claimedTasks[1]?.owner).toBe(agentName)
expect(claimedTasks[1]?.status).toBe('in_progress')
const claimedTask = claimedTasks.find(task => task.id === followUpTaskId)
expect(claimedTask?.owner).toBe(agentName)
expect(claimedTask?.status).toBe('in_progress')
const unrelatedTasks = await listTasks(parentSessionId)
expect(unrelatedTasks).toHaveLength(1)