Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ describe('SandboxAcquireBackendHandler', () => {

expect(registry.get).toHaveBeenCalledTimes(1)
expect(provider.create).toHaveBeenCalledTimes(1)
expect(first).toEqual({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/a',
backend
})
expect(first).toEqual(
expect.objectContaining({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/a',
backend
})
)
expect(second).toBe(first)
})

Expand Down Expand Up @@ -90,16 +92,102 @@ describe('SandboxAcquireBackendHandler', () => {
)

expect(provider.create).toHaveBeenCalledTimes(2)
expect(first).toEqual({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/a',
backend: backendA
})
expect(second).toEqual({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/b',
backend: backendB
})
expect(first).toEqual(
expect.objectContaining({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/a',
backend: backendA
})
)
expect(second).toEqual(
expect.objectContaining({
provider: 'local-shell-sandbox',
workingDirectory: '/workspace/b',
backend: backendB
})
)
})

it('creates distinct backends for different workspace bindings in the same scope and working directory', async () => {
const backendA = {
id: 'sandbox-a',
execute: jest.fn()
}
const backendB = {
id: 'sandbox-b',
execute: jest.fn()
}
provider.create.mockResolvedValueOnce(backendA).mockResolvedValueOnce(backendB)

const first = await handler.execute(
new SandboxAcquireBackendCommand({
tenantId: 'tenant-1',
provider: 'local-shell-sandbox',
workingDirectory: '/workspace',
workspaceBinding: {
volumeRoot: '/tmp/workspace-a',
workspaceRoot: '/workspace',
workspacePath: '/workspace'
},
workFor: {
type: 'user',
id: 'user-1'
}
})
)
const second = await handler.execute(
new SandboxAcquireBackendCommand({
tenantId: 'tenant-1',
provider: 'local-shell-sandbox',
workingDirectory: '/workspace',
workspaceBinding: {
volumeRoot: '/tmp/workspace-b',
workspaceRoot: '/workspace',
workspacePath: '/workspace'
},
workFor: {
type: 'user',
id: 'user-1'
}
})
)

expect(provider.create).toHaveBeenCalledTimes(2)
expect(first.backend).toBe(backendA)
expect(second.backend).toBe(backendB)
})

it('returns workspace binding with the sandbox context', async () => {
const backend = {
id: 'sandbox-a',
execute: jest.fn()
}
const workspaceBinding = {
volumeRoot: '/tmp/xpert-workspace',
workspaceRoot: '/tmp/xpert-workspace',
workspacePath: '/tmp/xpert-workspace'
}
provider.create.mockResolvedValue(backend)

const result = await handler.execute(
new SandboxAcquireBackendCommand({
tenantId: 'tenant-1',
provider: 'local-shell-sandbox',
workingDirectory: '/tmp/xpert-workspace',
workspaceBinding,
workFor: {
type: 'user',
id: 'user-1'
}
})
)

expect(provider.create).toHaveBeenCalledWith(
expect.objectContaining({
workspaceBinding
})
)
expect((result as { workspaceBinding?: unknown }).workspaceBinding).toBe(workspaceBinding)
})

it('throws when provider is omitted', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { TSandboxConfigurable } from '@xpert-ai/contracts'
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { SandboxProviderRegistry } from '@xpert-ai/plugin-sdk'
import { SandboxProviderCreateOptions, SandboxProviderRegistry } from '@xpert-ai/plugin-sdk'
import { SandboxAcquireBackendCommand } from '../acquire-backend.command'

type SandboxInstance = {
configurable: TSandboxConfigurable
}

type SandboxRuntimeConfigurable = TSandboxConfigurable & {
workspaceBinding?: SandboxProviderCreateOptions['workspaceBinding']
}

@CommandHandler(SandboxAcquireBackendCommand)
export class SandboxAcquireBackendHandler
implements ICommandHandler<SandboxAcquireBackendCommand, TSandboxConfigurable>
Expand All @@ -25,7 +29,7 @@ export class SandboxAcquireBackendHandler
}

const sessionKey = this.getSessionKey(workFor.type, workFor.id)
const instanceKey = this.getInstanceKey(provider, workingDirectory)
const instanceKey = this.getInstanceKey(provider, workingDirectory, workspaceBinding)
const sessionMap = this.instances.get(sessionKey) ?? new Map<string, SandboxInstance>()
const existing = sessionMap.get(instanceKey)
if (existing) {
Expand All @@ -40,12 +44,15 @@ export class SandboxAcquireBackendHandler
workingDirectory,
workspaceBinding
})
const configurable: TSandboxConfigurable = {
const configurable: SandboxRuntimeConfigurable = {
environmentId: environmentId ?? null,
provider,
workingDirectory,
backend
}
if (workspaceBinding) {
configurable.workspaceBinding = workspaceBinding
}
sessionMap.set(instanceKey, { configurable })
this.instances.set(sessionKey, sessionMap)
return configurable
Expand All @@ -55,7 +62,12 @@ export class SandboxAcquireBackendHandler
return `${workForType}:${workForId}`
}

private getInstanceKey(provider?: string | null, workingDirectory?: string | null) {
return `${provider ?? '__default__'}:${workingDirectory ?? '__default__'}`
private getInstanceKey(
provider?: string | null,
workingDirectory?: string | null,
workspaceBinding?: SandboxProviderCreateOptions['workspaceBinding']
) {
const workspaceIdentity = workspaceBinding?.volumeRoot ?? workspaceBinding?.bindSource ?? ''
return `${provider ?? '__default__'}:${workingDirectory ?? '__default__'}:${workspaceIdentity}`
}
}
2 changes: 2 additions & 0 deletions packages/server-ai/src/sandbox/commands/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SandboxGetManagedServiceLogsHandler } from './get-managed-service-logs.
import { SandboxListManagedServicesHandler } from './list-managed-services.handler'
import { SandboxRestartManagedServiceHandler } from './restart-managed-service.handler'
import { SandboxCopyFileHandler } from './sandbox.copy-file.handler'
import { SandboxCopyTreeHandler } from './sandbox.copy-tree.handler'
import { SandboxStartManagedServiceHandler } from './start-managed-service.handler'
import { SandboxStopManagedServiceHandler } from './stop-managed-service.handler'
import { SandboxVMHandler } from './vm.handler'
Expand All @@ -11,6 +12,7 @@ export const CommandHandlers = [
SandboxVMHandler,
SandboxAcquireBackendHandler,
SandboxCopyFileHandler,
SandboxCopyTreeHandler,
SandboxStartManagedServiceHandler,
SandboxListManagedServicesHandler,
SandboxGetManagedServiceLogsHandler,
Expand Down
Loading