mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(ci): make runtime validation portable to Linux
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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-'))
|
||||
|
||||
@@ -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: [] })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user