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
154 lines
5.0 KiB
TypeScript
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')
|
|
})
|
|
})
|