Files
orca/src/shared/ephemeral-vm-runtime-store-rollback.test.ts
Jinwoo Hong 8b22f044f5 fix(vm): preserve runtime sidecar rollback compatibility (#14444)
* test(vm): reproduce runtime store rollback poisoning

* fix(vm): keep runtime sidecar rollback-readable

* fix(vm): harden rollback-compatible runtime persistence

* fix(vm): publish rollback lifecycle authority first

* test(vm): harden rollback compatibility coverage
2026-08-14 19:04:54 -04:00

289 lines
10 KiB
TypeScript

import {
existsSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
truncateSync,
utimesSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
getEphemeralVmRuntimeFeatureStorePath,
MAX_EPHEMERAL_VM_RUNTIME_FEATURE_STORE_FILE_BYTES
} from './ephemeral-vm-runtime-feature-store'
import {
EphemeralVmRuntimeStoreError,
getEphemeralVmRuntimeStorePath,
listEphemeralVmRuntimes,
updateEphemeralVmRuntimeStatus,
upsertEphemeralVmRuntime
} from './ephemeral-vm-runtime-store'
import {
EphemeralVmRuntimeStoreSchema,
RollbackEphemeralVmRuntimeStoreSchema,
type EphemeralVmRuntimeRecord
} from './ephemeral-vm-runtimes'
function runtimeRecord(
overrides: Partial<EphemeralVmRuntimeRecord> = {}
): EphemeralVmRuntimeRecord {
return {
id: 'ordinary-runtime',
recipeId: 'ordinary-recipe',
recipe: {
id: 'ordinary-recipe',
name: 'Ordinary VM',
create: './create.sh',
destroy: './destroy.sh'
},
status: 'running',
cleanupStatus: 'not_started',
createdAt: 1_000,
updatedAt: 1_000,
recipeResult: {
schemaVersion: 1,
connection: {
type: 'ssh',
projectRoot: '/workspace/ordinary',
target: {
label: 'Ordinary VM',
host: 'ordinary.example.com',
port: 22,
username: 'developer'
}
},
userData: { resourceId: 'ordinary-resource' }
},
...overrides
}
}
function provisionedRootRecord(): EphemeralVmRuntimeRecord {
return runtimeRecord({
id: 'provisioned-runtime',
recipeId: 'provisioned-recipe',
recipe: {
id: 'provisioned-recipe',
name: 'Provisioned VM',
create: './create.sh',
destroy: './destroy.sh',
checkoutMode: 'provisioned-root'
},
createdAt: 2_000,
updatedAt: 2_000,
recipeResult: {
schemaVersion: 2,
checkoutMode: 'provisioned-root',
connection: {
type: 'ssh',
projectRoot: '/workspace/provisioned',
target: {
label: 'Provisioned VM',
host: 'provisioned.example.com',
port: 22,
username: 'developer'
}
},
userData: { resourceId: 'provisioned-resource' }
}
})
}
describe('ephemeral VM runtime store rollback projection', () => {
const tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function makeUserDataPath(): string {
const path = mkdtempSync(join(tmpdir(), 'orca-vm-rollback-store-'))
tempDirs.push(path)
return path
}
it('keeps a mixed store readable by the rollback schema and restores new fields', () => {
const userDataPath = makeUserDataPath()
const ordinary = upsertEphemeralVmRuntime(userDataPath, runtimeRecord())
const provisioned = upsertEphemeralVmRuntime(userDataPath, provisionedRootRecord())
const persisted = JSON.parse(readFileSync(getEphemeralVmRuntimeStorePath(userDataPath), 'utf8'))
expect(RollbackEphemeralVmRuntimeStoreSchema.parse(persisted).runtimes).toHaveLength(2)
expect(persisted.runtimes[0].recipe).not.toHaveProperty('checkoutMode')
expect(persisted.runtimes[0].recipeResult).toMatchObject({ schemaVersion: 1 })
expect(listEphemeralVmRuntimes(userDataPath)).toEqual([provisioned, ordinary])
})
it('keeps ordinary v1 bytes and sidecar behavior unchanged', () => {
const userDataPath = makeUserDataPath()
const runtime = runtimeRecord()
const expected = JSON.stringify(
EphemeralVmRuntimeStoreSchema.parse({ version: 1, runtimes: [runtime] })
)
upsertEphemeralVmRuntime(userDataPath, runtime)
expect(readFileSync(getEphemeralVmRuntimeStorePath(userDataPath), 'utf8')).toBe(expected)
expect(existsSync(getEphemeralVmRuntimeFeatureStorePath(userDataPath))).toBe(false)
})
it('projects an explicit ordinary checkout mode without changing its current meaning', () => {
const userDataPath = makeUserDataPath()
const runtime = runtimeRecord({
recipe: { ...runtimeRecord().recipe!, checkoutMode: 'orca-worktree' }
})
upsertEphemeralVmRuntime(userDataPath, runtime)
const persisted = JSON.parse(readFileSync(getEphemeralVmRuntimeStorePath(userDataPath), 'utf8'))
expect(RollbackEphemeralVmRuntimeStoreSchema.safeParse(persisted).success).toBe(true)
expect(listEphemeralVmRuntimes(userDataPath)).toEqual([runtime])
})
it('does not rewrite unchanged features when runtime order differs from feature order', () => {
const userDataPath = makeUserDataPath()
const older = {
...provisionedRootRecord(),
id: 'a-runtime',
recipeId: 'a-recipe',
createdAt: 1_000
}
const newer = {
...provisionedRootRecord(),
id: 'z-runtime',
recipeId: 'z-recipe',
createdAt: 2_000
}
upsertEphemeralVmRuntime(userDataPath, older)
upsertEphemeralVmRuntime(userDataPath, newer)
const featurePath = getEphemeralVmRuntimeFeatureStorePath(userDataPath)
const oldTimestamp = new Date('2020-01-01T00:00:00.000Z')
utimesSync(featurePath, oldTimestamp, oldTimestamp)
const beforeBytes = readFileSync(featurePath, 'utf8')
const beforeMtime = statSync(featurePath).mtimeMs
updateEphemeralVmRuntimeStatus(userDataPath, newer.id, { status: 'suspended' })
expect(readFileSync(featurePath, 'utf8')).toBe(beforeBytes)
expect(statSync(featurePath).mtimeMs).toBe(beforeMtime)
})
it('migrates current-main poisoned bytes when they are first read', () => {
const userDataPath = makeUserDataPath()
const poisoned = {
version: 1 as const,
runtimes: [provisionedRootRecord(), runtimeRecord()]
}
writeFileSync(
getEphemeralVmRuntimeStorePath(userDataPath),
JSON.stringify(EphemeralVmRuntimeStoreSchema.parse(poisoned))
)
expect(listEphemeralVmRuntimes(userDataPath)).toEqual(poisoned.runtimes)
expect(
RollbackEphemeralVmRuntimeStoreSchema.safeParse(
JSON.parse(readFileSync(getEphemeralVmRuntimeStorePath(userDataPath), 'utf8'))
).success
).toBe(true)
})
it('carries rollback lifecycle mutations through re-upgrade', () => {
const userDataPath = makeUserDataPath()
upsertEphemeralVmRuntime(userDataPath, runtimeRecord())
upsertEphemeralVmRuntime(userDataPath, provisionedRootRecord())
const path = getEphemeralVmRuntimeStorePath(userDataPath)
const rollback = RollbackEphemeralVmRuntimeStoreSchema.parse(
JSON.parse(readFileSync(path, 'utf8'))
)
writeFileSync(
path,
JSON.stringify({
version: 1,
runtimes: rollback.runtimes.map((runtime) =>
runtime.id === 'provisioned-runtime'
? { ...runtime, status: 'cleaned', cleanupStatus: 'succeeded', updatedAt: 3_000 }
: { ...runtime, status: 'suspended', updatedAt: 3_000 }
)
})
)
expect(listEphemeralVmRuntimes(userDataPath)).toEqual([
expect.objectContaining({
id: 'provisioned-runtime',
status: 'cleaned',
cleanupStatus: 'succeeded',
recipe: expect.objectContaining({ checkoutMode: 'provisioned-root' }),
recipeResult: expect.objectContaining({
schemaVersion: 2,
checkoutMode: 'provisioned-root'
})
}),
expect.objectContaining({ id: 'ordinary-runtime', status: 'suspended' })
])
})
it('preserves unknown feature records while valid siblings remain usable', () => {
const userDataPath = makeUserDataPath()
upsertEphemeralVmRuntime(userDataPath, runtimeRecord())
upsertEphemeralVmRuntime(userDataPath, provisionedRootRecord())
const featurePath = getEphemeralVmRuntimeFeatureStorePath(userDataPath)
const featureStore = JSON.parse(readFileSync(featurePath, 'utf8'))
const futureRecord = { kind: 'future-runtime-feature', payload: { version: 3 } }
writeFileSync(
featurePath,
JSON.stringify({ ...featureStore, records: [...featureStore.records, futureRecord] })
)
expect(listEphemeralVmRuntimes(userDataPath)).toHaveLength(2)
updateEphemeralVmRuntimeStatus(userDataPath, 'ordinary-runtime', { status: 'suspended' })
expect(JSON.parse(readFileSync(featurePath, 'utf8')).records).toContainEqual(futureRecord)
})
it.each([
['malformed', '{ nope'],
['future-version', JSON.stringify({ version: 2, records: [] })]
])(
'preserves an unreadable %s feature sidecar while keeping v1 records accessible',
(_, bytes) => {
const userDataPath = makeUserDataPath()
upsertEphemeralVmRuntime(userDataPath, runtimeRecord())
upsertEphemeralVmRuntime(userDataPath, provisionedRootRecord())
const featurePath = getEphemeralVmRuntimeFeatureStorePath(userDataPath)
writeFileSync(featurePath, bytes)
expect(listEphemeralVmRuntimes(userDataPath).map((runtime) => runtime.id)).toEqual([
'provisioned-runtime',
'ordinary-runtime'
])
updateEphemeralVmRuntimeStatus(userDataPath, 'ordinary-runtime', { status: 'suspended' })
expect(readFileSync(featurePath, 'utf8')).toBe(bytes)
}
)
it('publishes lifecycle authority before an unreadable feature companion', () => {
const userDataPath = makeUserDataPath()
upsertEphemeralVmRuntime(userDataPath, runtimeRecord())
const featurePath = getEphemeralVmRuntimeFeatureStorePath(userDataPath)
writeFileSync(featurePath, '{}')
truncateSync(featurePath, MAX_EPHEMERAL_VM_RUNTIME_FEATURE_STORE_FILE_BYTES + 1)
expect(() => upsertEphemeralVmRuntime(userDataPath, provisionedRootRecord())).toThrow(
EphemeralVmRuntimeStoreError
)
const persisted = JSON.parse(readFileSync(getEphemeralVmRuntimeStorePath(userDataPath), 'utf8'))
expect(RollbackEphemeralVmRuntimeStoreSchema.parse(persisted).runtimes).toHaveLength(2)
expect(readFileSync(featurePath, 'utf8')).toHaveLength(
MAX_EPHEMERAL_VM_RUNTIME_FEATURE_STORE_FILE_BYTES + 1
)
expect(listEphemeralVmRuntimes(userDataPath).map((runtime) => runtime.id)).toEqual([
'provisioned-runtime',
'ordinary-runtime'
])
})
})