Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

154 lines
5.0 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockRunLocal, mockRunCloud, mockResolveKey } = vi.hoisted(() => ({
mockRunLocal: vi.fn(),
mockRunCloud: vi.fn(),
mockResolveKey: vi.fn(),
}))
vi.mock('@/executor/handlers/pi/keys', () => ({
resolvePiModelKey: mockResolveKey,
computePiCost: () => ({ input: 0, output: 0, total: 0 }),
}))
vi.mock('@/executor/handlers/pi/context', () => ({
resolvePiSkills: vi.fn().mockResolvedValue([]),
loadPiMemory: vi.fn().mockResolvedValue([]),
appendPiMemory: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/executor/handlers/pi/sim-tools', () => ({
buildSimToolSpecs: vi.fn().mockResolvedValue([]),
}))
vi.mock('@/executor/handlers/pi/local-backend', () => ({ runLocalPi: mockRunLocal }))
vi.mock('@/executor/handlers/pi/cloud-backend', () => ({ runCloudPi: mockRunCloud }))
vi.mock('@/blocks/utils', () => ({
parseOptionalNumberInput: (value: unknown) => {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : undefined
},
}))
import { PiBlockHandler } from '@/executor/handlers/pi/pi-handler'
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
const block = { id: 'blk', metadata: { id: 'pi' } } as unknown as SerializedBlock
function ctx(overrides: Partial<ExecutionContext> = {}): ExecutionContext {
return {
workflowId: 'wf',
workspaceId: 'ws',
userId: 'user',
...overrides,
} as ExecutionContext
}
function localInputs(extra: Record<string, unknown> = {}) {
return {
mode: 'local',
task: 'do the thing',
model: 'claude',
host: 'box.example.com',
username: 'deploy',
authMethod: 'password',
password: 'pw',
repoPath: '/srv/repo',
...extra,
}
}
describe('PiBlockHandler', () => {
const handler = new PiBlockHandler()
beforeEach(() => {
vi.clearAllMocks()
mockResolveKey.mockResolvedValue({ providerId: 'anthropic', apiKey: 'k', isBYOK: true })
mockRunLocal.mockResolvedValue({
totals: { finalText: 'hi', inputTokens: 1, outputTokens: 2, toolCalls: [] },
})
mockRunCloud.mockResolvedValue({
totals: { finalText: 'done', inputTokens: 0, outputTokens: 0, toolCalls: [] },
prUrl: 'https://github.com/o/r/pull/1',
branch: 'pi/abc',
changedFiles: ['a.ts'],
diff: 'diff',
})
})
it('canHandle matches the pi block type', () => {
expect(handler.canHandle(block)).toBe(true)
expect(
handler.canHandle({ id: 'x', metadata: { id: 'agent' } } as unknown as SerializedBlock)
).toBe(false)
})
it('throws when the task is missing', async () => {
await expect(handler.execute(ctx(), block, { mode: 'local', task: '' })).rejects.toThrow(/Task/)
})
it('routes local mode to the local backend with SSH params', async () => {
const output = await handler.execute(ctx(), block, localInputs())
expect(mockRunLocal).toHaveBeenCalledTimes(1)
expect(mockRunCloud).not.toHaveBeenCalled()
const params = mockRunLocal.mock.calls[0][0]
expect(params.mode).toBe('local')
expect(params.ssh.host).toBe('box.example.com')
expect(params.repoPath).toBe('/srv/repo')
expect((output as Record<string, unknown>).content).toBe('hi')
})
it('routes cloud mode to the cloud backend and surfaces PR output', async () => {
const output = (await handler.execute(ctx(), block, {
mode: 'cloud',
task: 'do it',
model: 'claude',
owner: 'o',
repo: 'r',
githubToken: 'ghp',
})) as Record<string, unknown>
expect(mockRunCloud).toHaveBeenCalledTimes(1)
expect(output.prUrl).toBe('https://github.com/o/r/pull/1')
expect(output.branch).toBe('pi/abc')
})
it('requires SSH fields in local mode', async () => {
await expect(
handler.execute(ctx(), block, { mode: 'local', task: 'x', model: 'claude', host: 'h' })
).rejects.toThrow(/Local mode requires/)
})
it('requires repo + token in cloud mode', async () => {
await expect(
handler.execute(ctx(), block, { mode: 'cloud', task: 'x', model: 'claude', owner: 'o' })
).rejects.toThrow(/Cloud mode requires/)
})
it('streams text when the block is selected for streaming output', async () => {
mockRunLocal.mockImplementation(async (_params, runCtx) => {
runCtx.onEvent({ type: 'text', text: 'streamed' })
return { totals: { finalText: 'streamed', inputTokens: 0, outputTokens: 0, toolCalls: [] } }
})
const result = (await handler.execute(
ctx({ stream: true, selectedOutputs: ['blk'] }),
block,
localInputs()
)) as StreamingExecution
expect('stream' in result).toBe(true)
const reader = result.stream.getReader()
const decoder = new TextDecoder()
let text = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
text += decoder.decode(value)
}
expect(text).toContain('streamed')
expect(result.execution.output.content).toBe('streamed')
})
})