345 lines
9.0 KiB
TypeScript
345 lines
9.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type {
|
|
RemoteRuntimeBundleManifest
|
|
} from '../../shared/remote-runtime-launch-contracts'
|
|
import type {
|
|
SshConnectionPoolTarget
|
|
} from '../ssh/ssh-connection-pool'
|
|
import {
|
|
RemoteRuntimeInstallationError,
|
|
RemoteRuntimeInstallationManager
|
|
} from './remote-runtime-installation-manager'
|
|
|
|
const HOST_ID = 'host-1'
|
|
const AGENT_ID = `agent-${'a'.repeat(64)}` as never
|
|
const digestDirectory = 'a'.repeat(64)
|
|
const manifest: RemoteRuntimeBundleManifest = {
|
|
formatVersion: 2,
|
|
product: 'GoodBuddy',
|
|
runtimeId: 'opencode',
|
|
runtimeVersion: '1.18.29',
|
|
provider: 'opencode',
|
|
platform: 'linux',
|
|
architecture: 'x64',
|
|
signingKeyId: 'runtime-test',
|
|
bundleDigest: `sha256:${digestDirectory}`,
|
|
adapterDigest: `sha256:${'b'.repeat(64)}`,
|
|
sourcePackage: {
|
|
name: 'opencode-linux-x64-baseline',
|
|
integrity:
|
|
'sha512-x4KiJk9EF7ktM18Ru5Jue4kTntxMvlhWb7tHniQGGRvY2KeoK1iIkyAFd7ri5H/fSkM22hNv/Gg1Jk6/h9IlxQ=='
|
|
},
|
|
entrypoint: {
|
|
identity: 'opencode-acp',
|
|
path: 'bin/opencode',
|
|
sha256: 'c'.repeat(64),
|
|
argvPrefix: ['acp']
|
|
},
|
|
files: [
|
|
{
|
|
path: 'bin/opencode',
|
|
size: 100,
|
|
sha256: 'c'.repeat(64),
|
|
mode: '0755'
|
|
},
|
|
{
|
|
path: 'licenses/opencode.txt',
|
|
size: 10,
|
|
sha256: 'd'.repeat(64),
|
|
mode: '0644'
|
|
}
|
|
],
|
|
licenses: [
|
|
{
|
|
package: 'opencode',
|
|
version: '1.18.29',
|
|
spdx: 'MIT',
|
|
path: 'licenses/opencode.txt'
|
|
}
|
|
],
|
|
allowedEnvironmentNames: ['HOME', 'LANG'],
|
|
protocol: {
|
|
major: 2,
|
|
minor: 0
|
|
},
|
|
acpCapabilitiesDigest: `sha256:${'e'.repeat(64)}`,
|
|
limits: {
|
|
maximumPromptRuntimeMilliseconds: 60_000,
|
|
maximumPromptInputBytes: 4096,
|
|
maximumPromptOutputBytes: 1024
|
|
}
|
|
}
|
|
const entry = {
|
|
runtimeId: manifest.runtimeId,
|
|
runtimeVersion: manifest.runtimeVersion,
|
|
architecture: manifest.architecture,
|
|
bundleDigest: manifest.bundleDigest,
|
|
manifestDigest: `sha256:${'b'.repeat(64)}`,
|
|
runtimeAdapterDigest: manifest.adapterDigest,
|
|
acpCapabilitiesDigest: manifest.acpCapabilitiesDigest
|
|
}
|
|
|
|
function target(): SshConnectionPoolTarget {
|
|
return {
|
|
host: {
|
|
id: HOST_ID,
|
|
name: 'Builder',
|
|
hostname: 'builder.example',
|
|
port: 22,
|
|
username: 'goodbuddy',
|
|
authentication: 'password',
|
|
password: 'secret',
|
|
hostKey: {
|
|
algorithm: 'ssh-ed25519',
|
|
publicKeyBase64: 'a2V5',
|
|
fingerprintSha256: `SHA256:${'c'.repeat(43)}`,
|
|
generation: 2
|
|
}
|
|
},
|
|
hostRevision: 3,
|
|
hostKeyGeneration: 2
|
|
}
|
|
}
|
|
|
|
function fixture(options: {
|
|
registryEntry?: typeof entry | undefined
|
|
includeAdapter?: boolean
|
|
activate?: ReturnType<typeof vi.fn>
|
|
readRegistry?: () => Promise<Buffer>
|
|
} = {}) {
|
|
const calls: string[] = []
|
|
const registryEntry =
|
|
'registryEntry' in options
|
|
? options.registryEntry
|
|
: entry
|
|
const storedEntry = registryEntry === undefined
|
|
? undefined
|
|
: options.includeAdapter === false
|
|
? Object.fromEntries(
|
|
Object.entries(registryEntry).filter(
|
|
([key]) => key !== 'runtimeAdapterDigest'
|
|
)
|
|
)
|
|
: registryEntry
|
|
const readRegistry =
|
|
options.readRegistry ??
|
|
vi.fn(async () => Buffer.from(JSON.stringify({
|
|
formatVersion: 1,
|
|
current: storedEntry === undefined ? [] : [storedEntry]
|
|
})))
|
|
const readFile = vi.fn(async (path: string) => {
|
|
calls.push(`read:${path}`)
|
|
if (path.endsWith('registry.json')) {
|
|
return readRegistry()
|
|
}
|
|
return Buffer.from(JSON.stringify(manifest))
|
|
})
|
|
const close = vi.fn(() => calls.push('close-sftp'))
|
|
const release = vi.fn(() => calls.push('release'))
|
|
const lease = {
|
|
identity: {
|
|
hostId: HOST_ID,
|
|
hostRevision: 3,
|
|
hostKeyGeneration: 2,
|
|
authenticationIdentity: 'd'.repeat(64)
|
|
},
|
|
runAgentBootstrapProbe: vi.fn(async () => ({
|
|
ready: true,
|
|
platform: 'linux',
|
|
architecture: 'x64',
|
|
canonicalHomeDirectory: '/home/goodbuddy',
|
|
uid: 1000,
|
|
shell: '/bin/sh',
|
|
procfs: 'ready'
|
|
})),
|
|
openStagedSftp: vi.fn(async () => ({
|
|
lstat: vi.fn(async (path: string) => ({
|
|
type: 'file',
|
|
size: 512,
|
|
mode: path.endsWith('registry.json')
|
|
? 0o600
|
|
: 0o644,
|
|
uid: 1000,
|
|
gid: 1000,
|
|
atime: 0,
|
|
mtime: 0
|
|
})),
|
|
readFile,
|
|
close
|
|
})),
|
|
release,
|
|
isUsable: vi.fn(() => true)
|
|
}
|
|
const activate =
|
|
options.activate ??
|
|
vi.fn(async () => {
|
|
calls.push('activate')
|
|
})
|
|
const acquire = vi.fn(async () => lease as never)
|
|
const resolver = {
|
|
resolve: vi.fn(async () => target())
|
|
}
|
|
const manager = new RemoteRuntimeInstallationManager({
|
|
resolver,
|
|
sshPool: { acquire },
|
|
activate: activate as never
|
|
})
|
|
return {
|
|
manager,
|
|
calls,
|
|
resolver,
|
|
lease,
|
|
acquire,
|
|
activate,
|
|
readFile,
|
|
readRegistry,
|
|
close,
|
|
release
|
|
}
|
|
}
|
|
|
|
describe('RemoteRuntimeInstallationManager', () => {
|
|
it('activates a registered Runtime without reading its payload', async () => {
|
|
const value = fixture()
|
|
const phases: string[] = []
|
|
|
|
await expect(value.manager.activateInstalled(
|
|
HOST_ID,
|
|
{
|
|
agentInstallationId: AGENT_ID,
|
|
onProgress: (phase) => phases.push(phase)
|
|
}
|
|
)).resolves.toEqual({
|
|
...entry,
|
|
platform: 'linux'
|
|
})
|
|
expect(value.readFile.mock.calls.map(
|
|
(call) => call[0]
|
|
)).toEqual(['.goodbuddy/runtimes/registry.json'])
|
|
expect(value.activate).toHaveBeenCalledWith(
|
|
value.lease,
|
|
entry.runtimeId,
|
|
entry.bundleDigest,
|
|
entry.architecture,
|
|
AGENT_ID,
|
|
expect.any(AbortSignal)
|
|
)
|
|
expect(phases).toEqual([
|
|
'inspecting-host',
|
|
'verifying-bundle',
|
|
'activating-runtime',
|
|
'complete'
|
|
])
|
|
})
|
|
|
|
it('reads only the signed manifest metadata for a legacy registry entry', async () => {
|
|
const value = fixture({ includeAdapter: false })
|
|
|
|
await expect(
|
|
value.manager.activateInstalled(HOST_ID)
|
|
).resolves.toMatchObject({
|
|
runtimeAdapterDigest: manifest.adapterDigest
|
|
})
|
|
expect(value.readFile.mock.calls.map(
|
|
(call) => call[0]
|
|
)).toEqual([
|
|
'.goodbuddy/runtimes/registry.json',
|
|
`.goodbuddy/runtimes/opencode/${digestDirectory}/manifest.json`
|
|
])
|
|
})
|
|
|
|
it('caches activation until the Host is invalidated', async () => {
|
|
const value = fixture()
|
|
const first = await value.manager.activateInstalled(HOST_ID)
|
|
const second = await value.manager.activateInstalled(HOST_ID)
|
|
expect(second).toEqual(first)
|
|
expect(value.acquire).toHaveBeenCalledOnce()
|
|
|
|
value.manager.invalidateHost(HOST_ID)
|
|
await value.manager.activateInstalled(HOST_ID)
|
|
expect(value.acquire).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('deduplicates concurrent activation for one Agent identity', async () => {
|
|
let releaseRead!: () => void
|
|
const readGate = new Promise<void>((resolveRead) => {
|
|
releaseRead = resolveRead
|
|
})
|
|
const value = fixture({
|
|
readRegistry: vi.fn(async () => {
|
|
await readGate
|
|
return Buffer.from(JSON.stringify({
|
|
formatVersion: 1,
|
|
current: [entry]
|
|
}))
|
|
})
|
|
})
|
|
|
|
const first = value.manager.activateInstalled(HOST_ID, {
|
|
agentInstallationId: AGENT_ID
|
|
})
|
|
const second = value.manager.activateInstalled(HOST_ID, {
|
|
agentInstallationId: AGENT_ID
|
|
})
|
|
releaseRead()
|
|
await expect(Promise.all([first, second])).resolves.toHaveLength(2)
|
|
expect(value.acquire).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('classifies a missing Runtime registry entry as incompatible', async () => {
|
|
const value = fixture({ registryEntry: undefined })
|
|
await expect(
|
|
value.manager.activateInstalled(HOST_ID)
|
|
).rejects.toMatchObject({
|
|
reason: 'incompatible'
|
|
})
|
|
expect(value.activate).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('preserves concrete Agent activation failures', async () => {
|
|
const failure = new Error('entrypoint permission denied')
|
|
const value = fixture({
|
|
activate: vi.fn(async () => {
|
|
throw failure
|
|
})
|
|
})
|
|
await expect(
|
|
value.manager.activateInstalled(HOST_ID)
|
|
).rejects.toMatchObject({
|
|
reason: 'corrupt',
|
|
message: expect.stringContaining(
|
|
'entrypoint permission denied'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('rejects a mismatched legacy manifest before activation', async () => {
|
|
const value = fixture({ includeAdapter: false })
|
|
value.readFile.mockImplementation(async (path: string) =>
|
|
path.endsWith('registry.json')
|
|
? Buffer.from(JSON.stringify({
|
|
formatVersion: 1,
|
|
current: [
|
|
Object.fromEntries(
|
|
Object.entries(entry).filter(
|
|
([key]) =>
|
|
key !== 'runtimeAdapterDigest'
|
|
)
|
|
)
|
|
]
|
|
}))
|
|
: Buffer.from(JSON.stringify({
|
|
...manifest,
|
|
runtimeVersion: '99.0.0'
|
|
}))
|
|
)
|
|
|
|
await expect(
|
|
value.manager.activateInstalled(HOST_ID)
|
|
).rejects.toBeInstanceOf(
|
|
RemoteRuntimeInstallationError
|
|
)
|
|
expect(value.activate).not.toHaveBeenCalled()
|
|
})
|
|
})
|