Files
goodbuddy/src/main/remote-agent/agent-installation-manager.test.ts
T
mesalogo e5aebaa67d feat: add independent managed SSH agent packages
Managed SSH previously tied remote Agent and Runtime artifacts to desktop
packaging and Host activation. Settings now manages verified Linux x64 and
arm64 .gbagent packages explicitly, with source-aligned download and update
plus offline import and export. Each package contains the signed Agent, pinned
Node, and compatible OpenCode Runtime, and project activation never downloads
one automatically.

Dedicated native CI and a protected release workflow publish deterministic,
signed cumulative catalogs to GitHub and Beijing OSS without replacing the
desktop Latest release. Desktop packaging is metadata-only, remains independent
of Agent artifacts and signing credentials, and Linux desktop releases add RPM
installers.

The managed SSH path also keeps model credentials in Main, enforces Ask as
read-only, verifies signed Agent and Runtime inputs, preserves ACP recovery, and
reports Host update cancellation and failures accurately.

Release note: 远程项目现可在“设置”中手动下载、更新或离线导入签名 Agent 包,桌面安装包与远程组件发布相互独立;Linux 桌面版同时新增 RPM 安装包。
2026-08-25 18:28:29 +08:00

1903 lines
52 KiB
TypeScript

import {
createHash,
generateKeyPairSync,
sign
} from 'node:crypto'
import {
chmod,
mkdtemp,
mkdir,
readFile,
writeFile
} from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it, vi } from 'vitest'
import type {
AgentBundleManifest,
AgentReleaseKeyRegistry
} from '../../shared/agent-installation-contracts'
import type {
SftpEntryMetadata,
StagedSftp
} from '../ssh/bounded-sftp'
import type {
SshConnectionLease,
SshConnectionPoolTarget
} from '../ssh/ssh-connection-pool'
import {
agentManifestSignaturePayload,
type VerifiedAgentBundle
} from './agent-bundle-verifier'
import {
AgentInstallationError,
AgentInstallationManager
} from './agent-installation-manager'
type RemoteEntry = {
type: 'file' | 'directory' | 'symbolic-link'
mode: number
uid: number
contents?: Buffer
}
const home = '/home/tester'
const uid = 1000
function sha256(contents: Uint8Array): string {
return createHash('sha256').update(contents).digest('hex')
}
function missing(): Error & { code: number } {
return Object.assign(new Error('missing'), { code: 2 })
}
class MemorySftp implements StagedSftp {
readonly stagingDirectory = home
readonly operations: string[] = []
readonly entries = new Map<string, RemoteEntry>([
[home, { type: 'directory', mode: 0o700, uid }]
])
corruptReadPath?: string
abortWritePath?: string
abortController?: AbortController
closed = false
private absolute(path: string): string {
return `${home}/${path}`
}
private metadata(entry: RemoteEntry): SftpEntryMetadata {
return {
type: entry.type,
size: entry.contents?.byteLength ?? 0,
mode: entry.mode,
uid: entry.uid,
gid: entry.uid,
atime: 1,
mtime: 1
}
}
async mkdir(path: string): Promise<void> {
const absolute = this.absolute(path)
this.operations.push(`mkdir:${path}`)
if (this.entries.has(absolute)) {
throw Object.assign(new Error('exists'), { code: 4 })
}
this.entries.set(absolute, {
type: 'directory',
mode: 0o700,
uid
})
}
async writeFile(path: string, contents: Buffer): Promise<void> {
const absolute = this.absolute(path)
this.operations.push(`write:${path}`)
if (this.entries.has(absolute)) {
throw Object.assign(new Error('exists'), { code: 4 })
}
this.entries.set(absolute, {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from(contents)
})
if (this.abortWritePath === path) {
this.abortController?.abort()
throw this.abortController?.signal.reason
}
}
async readFile(path: string): Promise<Buffer> {
this.operations.push(`read:${path}`)
const contents = this.entry(path).contents ?? Buffer.alloc(0)
return path === this.corruptReadPath
? Buffer.from('corrupt')
: Buffer.from(contents)
}
async lstat(path: string): Promise<SftpEntryMetadata> {
return this.metadata(this.entry(path))
}
async stat(path: string): Promise<SftpEntryMetadata> {
const entry = this.entry(path)
if (entry.type === 'symbolic-link') {
throw new Error('symbolic link')
}
return this.metadata(entry)
}
async chmod(path: string, mode: 0o600 | 0o644 | 0o700 | 0o755): Promise<void> {
this.operations.push(`chmod:${path}:${mode.toString(8)}`)
this.entry(path).mode = mode
}
async setExecutable(path: string): Promise<void> {
await this.chmod(path, 0o700)
}
async rename(source: string, destination: string): Promise<void> {
this.operations.push(`rename:${source}:${destination}`)
if (this.entries.has(this.absolute(destination))) {
throw new Error('destination exists')
}
this.moveTree(source, destination)
}
async replaceFile(source: string, destination: string): Promise<void> {
this.operations.push(`replace:${source}:${destination}`)
const sourceEntry = this.entry(source)
const destinationEntry = this.entries.get(this.absolute(destination))
if (
sourceEntry.type !== 'file' ||
(destinationEntry && destinationEntry.type !== 'file')
) {
throw new Error('unsafe replacement')
}
this.entries.delete(this.absolute(destination))
this.entries.delete(this.absolute(source))
this.entries.set(this.absolute(destination), sourceEntry)
}
async hardLink(source: string, destination: string): Promise<void> {
this.operations.push(`link:${source}:${destination}`)
const sourceEntry = this.entry(source)
if (sourceEntry.type !== 'file') {
throw new Error('not file')
}
if (this.entries.has(this.absolute(destination))) {
throw new Error('destination exists')
}
this.entries.set(this.absolute(destination), sourceEntry)
}
async unlink(path: string): Promise<void> {
this.operations.push(`unlink:${path}`)
const entry = this.entry(path)
if (entry.type !== 'file') {
throw new Error('not file')
}
this.entries.delete(this.absolute(path))
}
async rmdir(path: string): Promise<void> {
this.operations.push(`rmdir:${path}`)
const absolute = this.absolute(path)
const entry = this.entry(path)
if (entry.type !== 'directory') {
throw new Error('not directory')
}
if (
[...this.entries.keys()].some(
(candidate) => candidate.startsWith(`${absolute}/`)
)
) {
throw new Error('not empty')
}
this.entries.delete(absolute)
}
close(): void {
this.closed = true
}
add(path: string, entry: RemoteEntry): void {
this.entries.set(this.absolute(path), entry)
}
private entry(path: string): RemoteEntry {
const entry = this.entries.get(this.absolute(path))
if (!entry) {
throw missing()
}
return entry
}
private moveTree(source: string, destination: string): void {
const sourceAbsolute = this.absolute(source)
const destinationAbsolute = this.absolute(destination)
const matches = [...this.entries.entries()].filter(
([path]) =>
path === sourceAbsolute ||
path.startsWith(`${sourceAbsolute}/`)
)
if (matches.length === 0) {
throw missing()
}
for (const [path] of matches) {
this.entries.delete(path)
}
for (const [path, entry] of matches) {
this.entries.set(
`${destinationAbsolute}${path.slice(sourceAbsolute.length)}`,
entry
)
}
}
}
const registry: AgentReleaseKeyRegistry = {
formatVersion: 1,
keys: [{
keyId: 'test-key',
publicKeySpkiBase64: 'AAAA',
environment: 'test'
}],
revocations: []
}
function target(revision = 1): SshConnectionPoolTarget {
return {
host: {
id: 'host-1',
name: 'Host',
hostname: 'host.example',
port: 22,
username: 'tester',
authentication: 'password',
password: 'secret',
hostKey: {
algorithm: 'ssh-ed25519',
publicKeyBase64: 'AAAA',
fingerprintSha256: `SHA256:${'a'.repeat(43)}`,
generation: revision
}
},
hostRevision: revision,
hostKeyGeneration: revision
}
}
async function bundleFixture(): Promise<VerifiedAgentBundle> {
const directory = await mkdtemp(join(tmpdir(), 'agent-manager-'))
const files = [
{ path: 'node', contents: Buffer.from('node'), mode: '0755' as const },
{
path: 'goodbuddy-agent',
contents: Buffer.from('agent'),
mode: '0755' as const
},
{
path: 'lib/agent.cjs',
contents: Buffer.from('script'),
mode: '0644' as const
},
{
path: 'licenses/LICENSE.txt',
contents: Buffer.from('license'),
mode: '0644' as const
}
]
const manifest: AgentBundleManifest = {
formatVersion: 1,
product: 'GoodBuddy',
agentVersion: '1.0.0',
platform: 'linux',
arch: 'x64',
protocol: { major: 1, minor: 0 },
signingKeyId: 'test-key',
entrypoint: {
path: 'goodbuddy-agent',
runtimePath: 'node',
scriptPath: 'lib/agent.cjs'
},
files: files.map((file) => ({
path: file.path,
size: file.contents.byteLength,
sha256: sha256(file.contents),
mode: file.mode
})),
licenses: [{
package: 'fixture',
version: '1.0.0',
spdx: 'MIT',
path: 'licenses/LICENSE.txt'
}]
}
for (const file of files) {
const destination = join(directory, ...file.path.split('/'))
await mkdir(join(destination, '..'), { recursive: true })
await writeFile(destination, file.contents)
}
const manifestBytes = Buffer.from(
`${JSON.stringify(manifest, null, 2)}\n`
)
await writeFile(join(directory, 'manifest.json'), manifestBytes)
await writeFile(join(directory, 'manifest.sig'), 'fixture-signature\n')
return {
bundleDirectory: directory,
manifest,
manifestSha256: sha256(manifestBytes)
}
}
function elfFixture(architecture: 'x64' | 'arm64'): Buffer {
const header = Buffer.alloc(64)
header[0] = 0x7f
header.write('ELF', 1, 'ascii')
header[4] = 2
header[5] = 1
header.writeUInt16LE(
architecture === 'x64' ? 62 : 183,
18
)
return header
}
async function harness(options: {
sftp?: MemorySftp
lifecycleFailure?: 'bootstrap' | 'health'
lifecycleCancellation?: 'health'
emulateInstallationRegistry?: boolean
resolverTargets?: SshConnectionPoolTarget[]
bootstrapGate?: Promise<void>
} = {}) {
const bundle = await bundleFixture()
const sftp = options.sftp ?? new MemorySftp()
const release = vi.fn()
const lifecycleActions: string[] = []
const installationId =
`agent-${bundle.manifestSha256}`
const lease = {
identity: {
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
authenticationIdentity: 'b'.repeat(64)
},
isUsable: () => true,
runAgentBootstrapProbe: vi.fn(async (signal?: AbortSignal) => {
if (options.bootstrapGate) {
await waitForGate(options.bootstrapGate, signal)
}
return {
ready: true,
platform: 'linux',
architecture: 'x64',
canonicalHomeDirectory: home,
uid,
shell: '/bin/bash',
procfs: 'ready'
} as const
}),
openStagedSftp: vi.fn(async () => sftp),
runAgentLifecycleAction: vi.fn(
async (
_installationId,
action: string,
signal?: AbortSignal
) => {
lifecycleActions.push(action)
if (
options.emulateInstallationRegistry &&
action === 'bootstrap'
) {
stageEmulatedCandidate(
sftp,
bundle,
installationId
)
}
if (options.lifecycleCancellation === action) {
await waitForGate(
new Promise<void>(() => undefined),
signal
)
}
const failed = options.lifecycleFailure === action
if (
options.emulateInstallationRegistry &&
action === 'health' &&
!failed
) {
promoteEmulatedCandidate(
sftp,
bundle,
installationId
)
}
return {
exitCode: failed ? 1 : 0,
stdout: '',
stderr: ''
}
}
),
release
} as unknown as SshConnectionLease
let resolveCount = 0
const targets = options.resolverTargets ?? [target()]
const resolver = {
resolve: vi.fn(async () =>
targets[Math.min(resolveCount++, targets.length - 1)]!
)
}
const sshPool = {
acquire: vi.fn(async () => lease)
}
const manager = new AgentInstallationManager({
resolver,
sshPool,
resourcePaths: {
keyRegistryPath: 'unused',
runtimeLockPath: 'unused',
bundleDirectories: { x64: 'unused', arm64: 'unused' }
},
verificationEnvironment: 'test',
loadVerifiedBundle: async () => ({ bundle, registry })
})
return {
manager,
bundle,
sftp,
release,
lifecycleActions,
resolver,
sshPool,
installationId
}
}
function stageEmulatedCandidate(
sftp: MemorySftp,
bundle: VerifiedAgentBundle,
installationId: string
): void {
const path = '.goodbuddy/agent/registry.json'
const persisted = JSON.parse(
sftp.entries.get(`${home}/${path}`)?.contents?.toString('utf8') ??
'{"formatVersion":1}'
) as {
current?: Record<string, unknown>
}
const current = persisted.current
sftp.add(path, {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from(`${JSON.stringify({
formatVersion: 1,
...(current === undefined
? {}
: {
current: {
installationId: current.installationId,
agentVersion: current.agentVersion,
manifestSha256: current.manifestSha256,
arch: current.arch
}
}),
candidate: registryEntry(bundle, installationId)
}, null, 2)}\n`)
})
}
function promoteEmulatedCandidate(
sftp: MemorySftp,
bundle: VerifiedAgentBundle,
installationId: string
): void {
sftp.add('.goodbuddy/agent/registry.json', {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from(`${JSON.stringify({
formatVersion: 1,
current: registryEntry(bundle, installationId)
}, null, 2)}\n`)
})
}
function registryEntry(
bundle: VerifiedAgentBundle,
installationId: string
): Record<string, string> {
return {
installationId,
agentVersion: bundle.manifest.agentVersion,
manifestSha256: bundle.manifestSha256,
arch: bundle.manifest.arch
}
}
function legacyRegistryBytes(): Buffer {
return Buffer.from(`{
"formatVersion": 1,
"minimumTrustedReleaseSequence": 7,
"current": {
"installationId": "old-current",
"productVersion": "0.9.0",
"agentVersion": "0.9.0",
"releaseSequence": 7,
"manifestSha256": "${'d'.repeat(64)}",
"binaryDigest": "sha256:${'d'.repeat(64)}",
"arch": "x64",
"protocol": { "major": 1, "minor": 0 },
"signingKeyId": "old-key",
"previouslyVerified": true
},
"draining": []
}
`)
}
function populateManagedRegistries(
fixture: Awaited<ReturnType<typeof harness>>,
installationRegistry: Buffer,
releaseKeyRegistry: Buffer
): void {
for (const path of [
'.goodbuddy',
'.goodbuddy/agent',
'.goodbuddy/agent/staging',
'.goodbuddy/agent/installations'
]) {
fixture.sftp.add(path, {
type: 'directory',
mode: 0o700,
uid
})
}
fixture.sftp.add('.goodbuddy/agent/registry.json', {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from(installationRegistry)
})
fixture.sftp.add('.goodbuddy/agent/release-keys.json', {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from(releaseKeyRegistry)
})
fixture.sftp.add(
'.goodbuddy/agent/installations/old-current',
{ type: 'directory', mode: 0o700, uid }
)
}
function managedBytes(
fixture: Awaited<ReturnType<typeof harness>>,
path: string
): Buffer | undefined {
return fixture.sftp.entries.get(`${home}/${path}`)?.contents
}
function bundledRegistryBytes(): Buffer {
return Buffer.from(`${JSON.stringify(registry, null, 2)}\n`)
}
async function waitForGate(
gate: Promise<void>,
signal?: AbortSignal
): Promise<void> {
if (!signal) {
await gate
return
}
signal.throwIfAborted()
await new Promise<void>((resolve, reject) => {
const abort = (): void => {
reject(
signal.reason ??
new DOMException('The operation was aborted', 'AbortError')
)
}
signal.addEventListener('abort', abort, { once: true })
gate.then(
() => {
signal.removeEventListener('abort', abort)
resolve()
},
(error: unknown) => {
signal.removeEventListener('abort', abort)
reject(error)
}
)
})
}
function populateInstallation(
fixture: Awaited<ReturnType<typeof harness>>
): void {
const root =
`.goodbuddy/agent/installations/${fixture.installationId}`
fixture.sftp.add(root, {
type: 'directory',
mode: 0o700,
uid
})
const directories = new Set<string>()
for (const file of fixture.bundle.manifest.files) {
const parts = file.path.split('/')
let directory = root
for (const part of parts.slice(0, -1)) {
directory = `${directory}/${part}`
if (!directories.has(directory)) {
fixture.sftp.add(directory, {
type: 'directory',
mode: 0o700,
uid
})
directories.add(directory)
}
}
fixture.sftp.add(`${root}/${file.path}`, {
type: 'file',
mode: file.mode === '0755' ? 0o755 : 0o644,
uid,
contents: Buffer.from(
file.path === 'node'
? 'node'
: file.path === 'goodbuddy-agent'
? 'agent'
: file.path === 'lib/agent.cjs'
? 'script'
: 'license'
)
})
}
const manifestBytes = Buffer.from(
`${JSON.stringify(fixture.bundle.manifest, null, 2)}\n`
)
fixture.sftp.add(`${root}/manifest.json`, {
type: 'file',
mode: 0o644,
uid,
contents: manifestBytes
})
fixture.sftp.add(`${root}/manifest.sig`, {
type: 'file',
mode: 0o644,
uid,
contents: Buffer.from('fixture-signature\n')
})
}
async function metadataOnlyHarness(options: {
remoteRegistryBytes?: Buffer
remoteReleaseKeyBytes?: Buffer
hostRegistrySubset?: boolean
corruptPath?: string
resolverTargets?: SshConnectionPoolTarget[]
candidateResources?: boolean
currentAgentVersion?: string
currentNodeContents?: Buffer
trustCurrentSignature?: boolean
} = {}) {
const bundle = await bundleFixture()
const keyPair = generateKeyPairSync('ed25519')
const unrelatedKeyPair = generateKeyPairSync('ed25519')
const signedRegistry: AgentReleaseKeyRegistry = {
formatVersion: 1,
keys: [
{
keyId: 'test-key',
publicKeySpkiBase64: keyPair.publicKey.export({
format: 'der',
type: 'spki'
}).toString('base64'),
environment: 'test'
},
{
keyId: 'unrelated-new-key',
publicKeySpkiBase64: unrelatedKeyPair.publicKey.export({
format: 'der',
type: 'spki'
}).toString('base64'),
environment: 'test'
}
],
revocations: []
}
const manifestBytes = await readFile(
join(bundle.bundleDirectory, 'manifest.json')
)
const candidateSignatureBytes = Buffer.from(
`${sign(
null,
agentManifestSignaturePayload(manifestBytes),
keyPair.privateKey
).toString('base64')}\n`
)
await writeFile(
join(bundle.bundleDirectory, 'manifest.sig'),
candidateSignatureBytes
)
const resourceRoot = await mkdtemp(
join(tmpdir(), 'agent-metadata-only-')
)
const registryBytes = Buffer.from(
`${JSON.stringify(signedRegistry, null, 2)}\n`
)
const rootLock = JSON.parse(
await readFile(
join(process.cwd(), 'agent-runtime-lock.json'),
'utf8'
)
) as Record<string, unknown>
const runtimeLock = {
...rootLock,
agentVersion: bundle.manifest.agentVersion,
protocol: bundle.manifest.protocol
}
const runtimeLockBytes = Buffer.from(
`${JSON.stringify(runtimeLock, null, 2)}\n`
)
const keyRegistryPath = join(
resourceRoot,
'agent-release-keys.json'
)
const runtimeLockPath = join(
resourceRoot,
'agent-runtime-lock.json'
)
await Promise.all([
writeFile(keyRegistryPath, registryBytes),
writeFile(runtimeLockPath, runtimeLockBytes)
])
const sftp = new MemorySftp()
const currentManifest: AgentBundleManifest = {
...bundle.manifest,
agentVersion:
options.currentAgentVersion ??
bundle.manifest.agentVersion,
files: bundle.manifest.files.map((file) =>
file.path === bundle.manifest.entrypoint.runtimePath &&
options.currentNodeContents !== undefined
? {
...file,
size: options.currentNodeContents.byteLength,
sha256: sha256(options.currentNodeContents)
}
: { ...file }
)
}
const currentManifestBytes = Buffer.from(
`${JSON.stringify(currentManifest, null, 2)}\n`
)
const currentManifestSha256 = sha256(currentManifestBytes)
const currentInstallationId =
`agent-${currentManifestSha256}`
const currentSigningKey = options.trustCurrentSignature === false
? generateKeyPairSync('ed25519').privateKey
: keyPair.privateKey
const currentSignatureBytes = Buffer.from(
`${sign(
null,
agentManifestSignaturePayload(currentManifestBytes),
currentSigningKey
).toString('base64')}\n`
)
const installationId = `agent-${bundle.manifestSha256}`
const installationRoot =
`.goodbuddy/agent/installations/${currentInstallationId}`
for (const path of [
'.goodbuddy',
'.goodbuddy/agent',
'.goodbuddy/agent/installations',
installationRoot,
`${installationRoot}/lib`,
`${installationRoot}/licenses`
]) {
sftp.add(path, {
type: 'directory',
mode: 0o700,
uid
})
}
const persistedRegistry =
options.remoteRegistryBytes ??
Buffer.from(`${JSON.stringify({
formatVersion: 1,
current: {
installationId: currentInstallationId,
agentVersion: currentManifest.agentVersion,
manifestSha256: currentManifestSha256,
arch: currentManifest.arch
}
}, null, 2)}\n`)
sftp.add('.goodbuddy/agent/registry.json', {
type: 'file',
mode: 0o600,
uid,
contents: persistedRegistry
})
sftp.add('.goodbuddy/agent/release-keys.json', {
type: 'file',
mode: 0o600,
uid,
contents:
options.remoteReleaseKeyBytes ??
(
options.hostRegistrySubset
? Buffer.from(`${JSON.stringify({
...signedRegistry,
keys: [signedRegistry.keys[0]]
}, null, 2)}\n`)
: registryBytes
)
})
for (const file of bundle.manifest.files) {
const contents = await readFile(
join(bundle.bundleDirectory, ...file.path.split('/'))
)
sftp.add(`${installationRoot}/${file.path}`, {
type: 'file',
mode: file.mode === '0755' ? 0o755 : 0o644,
uid,
contents:
options.corruptPath === file.path
? Buffer.alloc(contents.byteLength, 0x78)
: file.path === bundle.manifest.entrypoint.runtimePath &&
options.currentNodeContents !== undefined
? options.currentNodeContents
: contents
})
}
sftp.add(`${installationRoot}/manifest.json`, {
type: 'file',
mode: 0o644,
uid,
contents: currentManifestBytes
})
sftp.add(`${installationRoot}/manifest.sig`, {
type: 'file',
mode: 0o644,
uid,
contents:
options.corruptPath === 'manifest.sig'
? Buffer.from(`${'A'.repeat(86)}==\n`)
: currentSignatureBytes
})
const lifecycleActions: string[] = []
const release = vi.fn()
const lease = {
identity: {
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
authenticationIdentity: 'b'.repeat(64)
},
isUsable: () => true,
runAgentBootstrapProbe: vi.fn(async () => ({
ready: true,
platform: 'linux',
architecture: 'x64',
canonicalHomeDirectory: home,
uid,
shell: '/bin/bash',
procfs: 'ready'
}) as const),
openStagedSftp: vi.fn(async () => sftp),
runAgentLifecycleAction: vi.fn(
async (_installationId, action: string) => {
lifecycleActions.push(action)
return {
exitCode:
options.corruptPath === 'node' &&
!options.candidateResources &&
action === 'health'
? 1
: 0,
stdout: '',
stderr: ''
}
}
),
release
} as unknown as SshConnectionLease
let resolveCount = 0
const targets = options.resolverTargets ?? [target()]
const resolver = {
resolve: vi.fn(async () =>
targets[Math.min(resolveCount++, targets.length - 1)]!
)
}
const manager = new AgentInstallationManager({
resolver,
sshPool: { acquire: vi.fn(async () => lease) },
resourcePaths: {
keyRegistryPath,
runtimeLockPath,
bundleDirectories: {
x64: join(resourceRoot, 'missing-x64'),
arm64: join(resourceRoot, 'missing-arm64')
}
},
verificationEnvironment: 'test',
...(options.candidateResources
? {
loadVerifiedBundle: async () => ({
bundle,
registry: signedRegistry
})
}
: {})
})
return {
manager,
bundle,
installationId,
currentInstallationId,
lifecycleActions,
resolver,
sftp,
release
}
}
describe('AgentInstallationManager', () => {
it('requires the independently verified package loader in production', () => {
expect(() =>
new AgentInstallationManager({
resolver: { resolve: vi.fn() },
sshPool: { acquire: vi.fn() },
resourcePaths: {
keyRegistryPath: 'unused',
runtimeLockPath: 'unused',
bundleDirectories: {
x64: 'unused',
arm64: 'unused'
}
}
})
).toThrow(
'Production Agent installation requires a verified package loader'
)
})
it('cryptographically reuses an exact current Host Agent when bundle resources are absent', async () => {
const fixture = await metadataOnlyHarness()
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId,
agentVersion: fixture.bundle.manifest.agentVersion,
architecture: 'x64'
})
expect(fixture.lifecycleActions).toEqual(['health'])
expect(
fixture.sftp.operations.some((operation) =>
operation.startsWith('write:')
)
).toBe(false)
expect(fixture.release).toHaveBeenCalledOnce()
})
it('reuses a canonical older Host key subset containing the current signing key', async () => {
const fixture = await metadataOnlyHarness({
hostRegistrySubset: true
})
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId,
architecture: 'x64'
})
expect(fixture.lifecycleActions).toEqual(['health'])
})
it('classifies an old Agent as incompatible before reading differing Host key bytes', async () => {
const fixture = await metadataOnlyHarness({
currentAgentVersion: '0.9.0',
remoteReleaseKeyBytes: Buffer.from(
'{\n "formatVersion": 1,\n "keys": [],\n "revocations": []\n}\n'
)
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({
reason: 'incompatible',
message: expect.stringContaining(
'lacks matching Agent installation resources'
)
})
expect(fixture.sftp.operations).not.toContain(
'read:.goodbuddy/agent/release-keys.json'
)
expect(fixture.lifecycleActions).toEqual([])
})
it('rejects a lock-matching Agent when the Host registry lacks its signing key', async () => {
const fixture = await metadataOnlyHarness({
remoteReleaseKeyBytes: Buffer.from(
'{"formatVersion":1,"keys":[],"revocations":[]}\n'
)
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'corrupt' })
expect(fixture.lifecycleActions).toEqual([])
})
it('keeps a lock-matching Agent with an invalid signature classified as corrupt', async () => {
const fixture = await metadataOnlyHarness({
corruptPath: 'manifest.sig'
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'corrupt' })
expect(fixture.lifecycleActions).toEqual([])
})
it.each(['lib/agent.cjs'])(
'rejects metadata-only Agent reuse when %s is corrupt',
async (corruptPath) => {
const fixture = await metadataOnlyHarness({ corruptPath })
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'corrupt' })
expect(fixture.lifecycleActions).toEqual([])
}
)
it('delegates the large Node runtime hash check to Agent health', async () => {
const fixture = await metadataOnlyHarness({
corruptPath: 'node'
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'lifecycle' })
expect(fixture.lifecycleActions).toEqual([
'health',
'bootstrap',
'health'
])
})
it('rejects metadata-only Agent reuse after Host identity changes', async () => {
const fixture = await metadataOnlyHarness({
resolverTargets: [target(), target(2)]
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({
reason: 'host-identity-changed'
})
expect(fixture.lifecycleActions).toEqual([])
})
it('installs in manifest order with exact modes and verified readback', async () => {
const fixture = await harness()
const phases: string[] = []
const identity = await fixture.manager.ensureInstalled(
'host-1',
{ onProgress: (phase) => phases.push(phase) }
)
expect(identity).toMatchObject({
installationId: fixture.installationId,
binaryDigest: `sha256:${fixture.bundle.manifestSha256}`,
architecture: 'x64',
supervisor: 'detached-on-demand'
})
expect(fixture.lifecycleActions).toEqual(['bootstrap', 'health'])
const payloadWrites = fixture.sftp.operations
.filter((operation) => operation.startsWith('write:') &&
!operation.includes('.release-keys-'))
.map((operation) =>
operation.slice(operation.lastIndexOf('/') + 1)
)
expect(payloadWrites).toEqual([
'node',
'goodbuddy-agent',
'agent.cjs',
'LICENSE.txt',
'manifest.json',
'manifest.sig'
])
expect(
fixture.sftp.operations.some((operation) =>
/chmod:.*goodbuddy-agent:755/u.test(operation)
)
).toBe(true)
expect(phases.at(-1)).toBe('complete')
expect(fixture.release).toHaveBeenCalledOnce()
expect(fixture.sftp.closed).toBe(true)
})
it('dispatches an arm64 Host through the normal arm64 bundle-directory loader', async () => {
const resourceRoot = await mkdtemp(
join(tmpdir(), 'agent-normal-loader-')
)
const x64Directory = join(resourceRoot, 'linux-x64')
const arm64Directory = join(resourceRoot, 'linux-arm64')
await Promise.all([
mkdir(x64Directory, { recursive: true }),
mkdir(arm64Directory, { recursive: true })
])
await writeFile(
join(x64Directory, 'manifest.json'),
'not the selected architecture\n'
)
const payloads = [
{
path: 'node',
contents: elfFixture('arm64'),
mode: '0755' as const
},
{
path: 'goodbuddy-agent',
contents: Buffer.from('agent'),
mode: '0755' as const
},
{
path: 'lib/agent.cjs',
contents: Buffer.from('script'),
mode: '0644' as const
},
...[
'lib/node_modules/koffi/package.json',
'lib/node_modules/koffi/index.js',
'lib/node_modules/koffi/src/koffi/index.js',
'lib/node_modules/koffi/src/koffi/src/static.js',
'lib/node_modules/@koromix/koffi-linux-arm64/package.json',
'lib/node_modules/@koromix/koffi-linux-arm64/index.js'
].map((path) => ({
path,
contents: Buffer.from(path),
mode: '0644' as const
})),
...[
'lib/node_modules/@koromix/koffi-linux-arm64/linux_arm64/koffi.node',
'lib/node_modules/@koromix/koffi-linux-arm64/musl_arm64/koffi.node'
].map((path) => ({
path,
contents: elfFixture('arm64'),
mode: '0644' as const
})),
{
path: 'licenses/koffi-MIT.txt',
contents: Buffer.from('koffi license'),
mode: '0644' as const
},
{
path: 'licenses/koffi-native-MIT.txt',
contents: Buffer.from('native license'),
mode: '0644' as const
}
]
const manifest: AgentBundleManifest = {
formatVersion: 1,
product: 'GoodBuddy',
agentVersion: '1.0.0',
platform: 'linux',
arch: 'arm64',
protocol: { major: 1, minor: 0 },
signingKeyId: 'test-key',
entrypoint: {
path: 'goodbuddy-agent',
runtimePath: 'node',
scriptPath: 'lib/agent.cjs'
},
files: payloads.map((file) => ({
path: file.path,
size: file.contents.byteLength,
sha256: sha256(file.contents),
mode: file.mode
})),
licenses: [
{
package: 'koffi',
version: '3.1.4',
spdx: 'MIT',
path: 'licenses/koffi-MIT.txt'
},
{
package: '@koromix/koffi-linux-arm64',
version: '3.1.4',
spdx: 'MIT',
path: 'licenses/koffi-native-MIT.txt'
}
]
}
for (const payload of payloads) {
const destination = join(
arm64Directory,
...payload.path.split('/')
)
await mkdir(join(destination, '..'), { recursive: true })
await writeFile(destination, payload.contents)
await chmod(
destination,
payload.mode === '0755' ? 0o755 : 0o644
)
}
const keyPair = generateKeyPairSync('ed25519')
const signedRegistry: AgentReleaseKeyRegistry = {
formatVersion: 1,
keys: [{
keyId: 'test-key',
publicKeySpkiBase64: keyPair.publicKey.export({
format: 'der',
type: 'spki'
}).toString('base64'),
environment: 'test'
}],
revocations: []
}
const manifestBytes = Buffer.from(
`${JSON.stringify(manifest, null, 2)}\n`
)
await Promise.all([
writeFile(
join(arm64Directory, 'manifest.json'),
manifestBytes
),
writeFile(
join(arm64Directory, 'manifest.sig'),
Buffer.from(`${sign(
null,
agentManifestSignaturePayload(manifestBytes),
keyPair.privateKey
).toString('base64')}\n`)
)
])
await Promise.all([
chmod(join(arm64Directory, 'manifest.json'), 0o644),
chmod(join(arm64Directory, 'manifest.sig'), 0o644)
])
const rootLock = JSON.parse(
await readFile(
join(process.cwd(), 'agent-runtime-lock.json'),
'utf8'
)
) as Record<string, unknown>
const keyRegistryPath = join(
resourceRoot,
'agent-release-keys.json'
)
const runtimeLockPath = join(
resourceRoot,
'agent-runtime-lock.json'
)
await Promise.all([
writeFile(
keyRegistryPath,
Buffer.from(`${JSON.stringify(
signedRegistry,
null,
2
)}\n`)
),
writeFile(
runtimeLockPath,
Buffer.from(`${JSON.stringify({
...rootLock,
agentVersion: manifest.agentVersion,
protocol: manifest.protocol
}, null, 2)}\n`)
)
])
const sftp = new MemorySftp()
const release = vi.fn()
const lease = {
identity: {
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
authenticationIdentity: 'b'.repeat(64)
},
isUsable: () => true,
runAgentBootstrapProbe: vi.fn(async () => ({
ready: true,
platform: 'linux',
architecture: 'arm64',
canonicalHomeDirectory: home,
uid,
shell: '/bin/bash',
procfs: 'ready'
}) as const),
openStagedSftp: vi.fn(async () => sftp),
runAgentLifecycleAction: vi.fn(async () => ({
exitCode: 0,
stdout: '',
stderr: ''
})),
release
} as unknown as SshConnectionLease
const manager = new AgentInstallationManager({
resolver: { resolve: vi.fn(async () => target()) },
sshPool: { acquire: vi.fn(async () => lease) },
resourcePaths: {
keyRegistryPath,
runtimeLockPath,
bundleDirectories: {
x64: x64Directory,
arm64: arm64Directory
}
},
verificationEnvironment: 'test'
})
await expect(
manager.ensureInstalled('host-1')
).resolves.toMatchObject({
architecture: 'arm64',
agentVersion: manifest.agentVersion
})
expect(
sftp.operations.some((operation) =>
operation.endsWith('/node')
)
).toBe(true)
expect(release).toHaveBeenCalledOnce()
})
it('upgrades an unverifiable current Agent when candidate resources exist', async () => {
const fixture = await metadataOnlyHarness({
candidateResources: true,
currentAgentVersion: '0.9.0',
trustCurrentSignature: false
})
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId,
agentVersion: fixture.bundle.manifest.agentVersion
})
expect(fixture.lifecycleActions).toEqual([
'bootstrap',
'health'
])
expect(
fixture.sftp.operations.some((operation) =>
operation.startsWith('write:') &&
operation.endsWith('/node')
)
).toBe(true)
expect(
fixture.sftp.entries.has(
`${home}/.goodbuddy/agent/installations/${fixture.currentInstallationId}`
)
).toBe(true)
})
it('reuses an exactly verified current Node without uploading it', async () => {
const fixture = await metadataOnlyHarness({
candidateResources: true,
currentAgentVersion: '0.9.0'
})
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId
})
expect(
fixture.sftp.operations.some((operation) =>
/^link:\.goodbuddy\/agent\/installations\/agent-[a-f0-9]{64}\/node:\.goodbuddy\/agent\/staging\/op-[^/]+\/node$/u
.test(operation)
)
).toBe(true)
expect(
fixture.sftp.operations.some((operation) =>
operation.startsWith('write:') &&
operation.endsWith('/node')
)
).toBe(false)
})
it.each([
{
condition: 'untrusted current manifest',
options: { trustCurrentSignature: false }
},
{
condition: 'corrupt current Node',
options: { corruptPath: 'node' }
},
{
condition: 'different verified current Node',
options: { currentNodeContents: Buffer.from('old-node') }
}
])(
'uploads candidate Node for $condition',
async ({ options }) => {
const fixture = await metadataOnlyHarness({
candidateResources: true,
currentAgentVersion: '0.9.0',
...options
})
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId
})
expect(
fixture.sftp.operations.some((operation) =>
operation.startsWith('link:')
)
).toBe(false)
expect(
fixture.sftp.operations.some((operation) =>
operation.startsWith('write:') &&
operation.endsWith('/node')
)
).toBe(true)
}
)
it('bootstraps a stopped exact installation and reuses it without upload', async () => {
const fixture = await harness()
populateInstallation(fixture)
await fixture.manager.ensureInstalled('host-1')
expect(fixture.lifecycleActions).toEqual(['bootstrap', 'health'])
expect(
fixture.sftp.operations.some((entry) =>
entry.startsWith('rename:.goodbuddy/agent/staging/')
)
).toBe(false)
})
it('reports lifecycle recovery failure without marking a valid bundle corrupt', async () => {
const fixture = await harness({ lifecycleFailure: 'health' })
populateInstallation(fixture)
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'lifecycle' })
expect(fixture.lifecycleActions).toEqual([
'bootstrap',
'health',
'stop'
])
expect(
fixture.sftp.operations.some((entry) =>
entry.startsWith('rename:.goodbuddy/agent/staging/')
)
).toBe(false)
})
it('rejects corrupt readback and removes operation-owned staging', async () => {
const sftp = new MemorySftp()
const fixture = await harness({ sftp })
const originalRead = sftp.readFile.bind(sftp)
sftp.readFile = vi.fn(async (path: string) => {
const value = await originalRead(path)
if (path.endsWith('/node')) {
return Buffer.from('bad')
}
return value
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toThrow('readback mismatch')
expect(
[...sftp.entries.keys()].some((path) =>
path.includes('/staging/op-')
)
).toBe(false)
expect(fixture.release).toHaveBeenCalledOnce()
})
it('atomically replaces the managed release-key registry', async () => {
const fixture = await harness()
fixture.sftp.add('.goodbuddy', {
type: 'directory',
mode: 0o700,
uid
})
fixture.sftp.add('.goodbuddy/agent', {
type: 'directory',
mode: 0o700,
uid
})
fixture.sftp.add('.goodbuddy/agent/staging', {
type: 'directory',
mode: 0o700,
uid
})
fixture.sftp.add('.goodbuddy/agent/installations', {
type: 'directory',
mode: 0o700,
uid
})
fixture.sftp.add('.goodbuddy/agent/release-keys.json', {
type: 'file',
mode: 0o600,
uid,
contents: Buffer.from('old')
})
await fixture.manager.ensureInstalled('host-1')
expect(
fixture.sftp.operations.some((operation) =>
/^replace:\.goodbuddy\/agent\/\.release-keys-.*\.tmp:\.goodbuddy\/agent\/release-keys\.json$/u
.test(operation)
)
).toBe(true)
})
it('deduplicates concurrent ensures for the same full host identity', async () => {
let releaseGate!: () => void
const gate = new Promise<void>((resolve) => {
releaseGate = resolve
})
const fixture = await harness({ bootstrapGate: gate })
const first = fixture.manager.ensureInstalled('host-1')
const second = fixture.manager.ensureInstalled('host-1', {
force: true
})
await vi.waitFor(() =>
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
)
releaseGate()
const [left, right] = await Promise.all([first, second])
expect(left).toEqual(right)
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
})
it('reuses a successful installation for the same Host identity', async () => {
const fixture = await harness()
const first = await fixture.manager.ensureInstalled('host-1')
const phases: string[] = []
const second = await fixture.manager.ensureInstalled('host-1', {
onProgress: (phase) => phases.push(phase)
})
expect(second).toEqual(first)
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
expect(phases).toEqual(['inspecting-host', 'complete'])
})
it('force bypasses the settled installation cache and refreshes it after verification', async () => {
const fixture = await harness()
const first = await fixture.manager.ensureInstalled('host-1')
const refreshed = await fixture.manager.ensureInstalled(
'host-1',
{ force: true }
)
const cached = await fixture.manager.ensureInstalled('host-1')
expect(refreshed).toEqual(first)
expect(cached).toEqual(refreshed)
expect(fixture.sshPool.acquire).toHaveBeenCalledTimes(2)
expect(fixture.lifecycleActions).toEqual([
'bootstrap',
'health',
'bootstrap',
'health'
])
})
it('keeps a shared install running when the first waiter cancels', async () => {
let releaseGate!: () => void
const gate = new Promise<void>((resolve) => {
releaseGate = resolve
})
const fixture = await harness({ bootstrapGate: gate })
const controller = new AbortController()
const canceledPhases: string[] = []
const first = fixture.manager.ensureInstalled('host-1', {
signal: controller.signal,
onProgress: (phase) => canceledPhases.push(phase)
})
const survivor = fixture.manager.ensureInstalled('host-1')
await vi.waitFor(() =>
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
)
controller.abort()
await expect(first).rejects.toMatchObject({ name: 'AbortError' })
releaseGate()
await expect(survivor).resolves.toMatchObject({
installationId: fixture.installationId
})
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
expect(canceledPhases).toEqual(['inspecting-host'])
expect(fixture.release).toHaveBeenCalledOnce()
})
it('aborts a shared install after its last waiter cancels', async () => {
const gate = new Promise<void>(() => undefined)
const fixture = await harness({ bootstrapGate: gate })
const controller = new AbortController()
const installation = fixture.manager.ensureInstalled('host-1', {
signal: controller.signal
})
await vi.waitFor(() =>
expect(fixture.sshPool.acquire).toHaveBeenCalledOnce()
)
controller.abort()
await expect(installation).rejects.toMatchObject({
name: 'AbortError'
})
await vi.waitFor(() =>
expect(fixture.release).toHaveBeenCalledOnce()
)
})
it('dispose aborts active installs, cleans up, and rejects new calls', async () => {
const sftp = new MemorySftp()
let startedNodeWrite!: () => void
const nodeWriteStarted = new Promise<void>((resolve) => {
startedNodeWrite = resolve
})
const originalWrite = sftp.writeFile.bind(sftp)
sftp.writeFile = vi.fn(async (
path: string,
contents: Buffer,
signal?: AbortSignal
) => {
await originalWrite(path, contents)
if (path.endsWith('/node')) {
startedNodeWrite()
await waitForGate(
new Promise<void>(() => undefined),
signal
)
}
})
const fixture = await harness({ sftp })
const installation = fixture.manager.ensureInstalled('host-1')
await nodeWriteStarted
const firstDispose = fixture.manager.dispose()
const secondDispose = fixture.manager.dispose()
expect(secondDispose).toBe(firstDispose)
await expect(installation).rejects.toMatchObject({
name: 'AbortError'
})
await firstDispose
expect(fixture.release).toHaveBeenCalledOnce()
expect(fixture.sftp.closed).toBe(true)
expect(
[...sftp.entries.keys()].some((path) =>
path.includes('/staging/op-')
)
).toBe(false)
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toThrow('Agent installation manager is disposed')
expect(fixture.resolver.resolve).toHaveBeenCalledOnce()
})
it('cleans tracked staging and releases the lease after abort', async () => {
const controller = new AbortController()
const sftp = new MemorySftp()
sftp.abortController = controller
const fixture = await harness({ sftp })
const originalWrite = sftp.writeFile.bind(sftp)
sftp.writeFile = vi.fn(async (path: string, contents: Buffer) => {
await originalWrite(path, contents)
if (path.endsWith('/node')) {
controller.abort()
throw controller.signal.reason
}
})
await expect(
fixture.manager.ensureInstalled('host-1', {
signal: controller.signal
})
).rejects.toMatchObject({ name: 'AbortError' })
await vi.waitFor(() =>
expect(fixture.release).toHaveBeenCalledOnce()
)
expect(
[...sftp.entries.keys()].some((path) =>
path.includes('/staging/op-')
)
).toBe(false)
})
it('stops before activation when the host identity changes', async () => {
const fixture = await harness({
resolverTargets: [target(1), target(2)]
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({
reason: 'host-identity-changed'
})
expect(fixture.lifecycleActions).toEqual([])
})
it('reports an existing bad destination as corrupt without replacing it', async () => {
const fixture = await harness()
const destination =
`.goodbuddy/agent/installations/${fixture.installationId}`
fixture.sftp.add(destination, {
type: 'directory',
mode: 0o700,
uid
})
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({
reason: 'corrupt'
})
expect(fixture.sftp.entries.has(`${home}/${destination}`)).toBe(true)
expect(
fixture.sftp.operations.some((entry) =>
entry.startsWith('rename:.goodbuddy/agent/staging/')
)
).toBe(false)
})
it('leaves an old installation untouched when candidate bootstrap fails', async () => {
const fixture = await harness({ lifecycleFailure: 'bootstrap' })
fixture.sftp.add(
'.goodbuddy/agent/installations/old-current',
{ type: 'directory', mode: 0o700, uid }
)
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toBeInstanceOf(AgentInstallationError)
expect(
fixture.sftp.entries.has(
`${home}/.goodbuddy/agent/installations/old-current`
)
).toBe(true)
expect(fixture.lifecycleActions).toEqual(['bootstrap', 'stop'])
})
it('restores legacy registry and release-key bytes after upgrade health failure', async () => {
const oldRegistry = legacyRegistryBytes()
const oldReleaseKeys = Buffer.from(
'{ "legacy": true, "spacing": "preserved" }\n'
)
const fixture = await harness({
lifecycleFailure: 'health',
emulateInstallationRegistry: true
})
populateManagedRegistries(
fixture,
oldRegistry,
oldReleaseKeys
)
await expect(
fixture.manager.ensureInstalled('host-1')
).rejects.toMatchObject({ reason: 'lifecycle' })
expect(fixture.lifecycleActions).toEqual([
'bootstrap',
'health',
'stop'
])
expect(
managedBytes(fixture, '.goodbuddy/agent/registry.json')
).toEqual(oldRegistry)
expect(
managedBytes(
fixture,
'.goodbuddy/agent/release-keys.json'
)
).toEqual(oldReleaseKeys)
expect(
fixture.sftp.entries.has(
`${home}/.goodbuddy/agent/installations/old-current`
)
).toBe(true)
})
it('stops the candidate and restores exact registry bytes after cancellation', async () => {
const oldRegistry = legacyRegistryBytes()
const oldReleaseKeys = Buffer.from('old release key bytes\n')
const fixture = await harness({
lifecycleCancellation: 'health',
emulateInstallationRegistry: true
})
populateManagedRegistries(
fixture,
oldRegistry,
oldReleaseKeys
)
const controller = new AbortController()
const installation = fixture.manager.ensureInstalled('host-1', {
signal: controller.signal
})
await vi.waitFor(() =>
expect(fixture.lifecycleActions).toContain('health')
)
controller.abort()
await expect(installation).rejects.toMatchObject({
name: 'AbortError'
})
await vi.waitFor(() =>
expect(fixture.release).toHaveBeenCalledOnce()
)
expect(fixture.lifecycleActions).toContain('stop')
expect(
managedBytes(fixture, '.goodbuddy/agent/registry.json')
).toEqual(oldRegistry)
expect(
managedBytes(
fixture,
'.goodbuddy/agent/release-keys.json'
)
).toEqual(oldReleaseKeys)
})
it('keeps only the promoted current registry after successful upgrade health', async () => {
const oldRegistry = legacyRegistryBytes()
const fixture = await harness({
emulateInstallationRegistry: true
})
populateManagedRegistries(
fixture,
oldRegistry,
Buffer.from('old release keys\n')
)
await expect(
fixture.manager.ensureInstalled('host-1')
).resolves.toMatchObject({
installationId: fixture.installationId
})
expect(fixture.lifecycleActions).toEqual([
'bootstrap',
'health'
])
expect(JSON.parse(
managedBytes(
fixture,
'.goodbuddy/agent/registry.json'
)!.toString('utf8')
)).toEqual({
formatVersion: 1,
current: registryEntry(
fixture.bundle,
fixture.installationId
)
})
expect(
managedBytes(
fixture,
'.goodbuddy/agent/release-keys.json'
)
).toEqual(bundledRegistryBytes())
})
})