Files
orca/cloud/apps/sandbox-control/test/create-git-async.test.ts
T
Neil 4bcf815aa7 fix(cloud): a boot whose member identity install fails records it too (VM git-setup.failed + gitSetup), so Restart can't hide a failure
Boot-time installs were swallowed and the boot cleared gitSetup, so a restart whose install failed again dropped the desktop strip while git still lacked identities. Every boot install now goes through settleMemberGit: failure marks the VM and the row, a later success (or a boot with nobody to install) clears them. The git shim refuses on a failed mark even with nothing pending. Restart is the retry (it re-runs the install before it answers).
2026-11-11 19:29:18 -08:00

175 lines
8.4 KiB
TypeScript

// CREATE-GIT-ASYNC: a create answers once orcad is ready and installs member git identities after
// (ctx.waitUntil). The VM is told the setup is pending so its `git` holds commits and pushes; fetches
// use the workspace credential the boot already put in the egress policy. Wake, resume and restart
// still install identities before they answer.
import { describe, expect, it } from 'vitest'
import type { RunCommandParams } from '../src/sandbox-provider'
import { GIT_SETUP_MARK_SCRIPT, MEMBERS_COMMAND, githubPlaceholderToken } from '../src/lifecycle'
import { START_COMMAND } from '../src/sandbox-vm-commands'
import { gitBasicAuthorization } from '../src/network-policy'
import { getSandbox } from '../src/store'
import { createHarness } from './harness'
const PROXY_URL = 'https://sandbox.test/v1/egress/proxy'
const REPO = 'https://github.com/acme/app'
const OP = 'o'.repeat(40)
const GIT_SETUP_AWARE = { 'x-orca-sandbox-features': 'git-setup' }
type Gate = { open: () => void; wait: Promise<void> }
function gate(): Gate {
let open = (): void => {}
const wait = new Promise<void>((resolve) => {
open = resolve
})
return { open, wait }
}
async function setup() {
const background: Promise<unknown>[] = []
const h = await createHarness(
{ EGRESS_PROXY_URL: PROXY_URL, SECRETS_KEK: Buffer.alloc(32, 7).toString('base64'), OPERATOR_TOKEN: OP },
{
waitUntil: (work: Promise<unknown>) => void background.push(work),
credentials: { authorizationFor: async () => 'Bearer ghs_app' },
github: {
tokenForRepo: async () => ({ token: 'ghs_repo_token', expiresAt: h.now() + 3_600_000 }),
installationForRepo: async () => null
} as never
}
)
// The members install waits on `members` until the test opens it, or answers with `membersExit`.
let members: Gate | null = null
let membersExit = 0
h.vercel.commandHandler = (params: RunCommandParams) => {
if (params.command !== MEMBERS_COMMAND) {
return undefined
}
const answer = { exitCode: membersExit, stdout: '{"members":1,"devices":1}\n', stderr: '' }
return members ? members.wait.then(() => answer) : Promise.resolve(answer)
}
const as = (sub: string) => h.token(sub, 'acme', 'sandbox', { org_role: 'member' })
// The creator connected GitHub, so their identity is one the VM installs.
await h.call('POST', '/v1/internal/dev/member-github', {
bearer: OP,
body: { accountId: 'org:acme', userId: 'alice', repoUrl: REPO, login: 'alice-gh', githubUserId: 11, name: 'alice name' }
})
const create = async () =>
h.call('POST', '/v1/sandboxes', {
bearer: await as('alice'),
body: { workspaceId: `ws-${Math.random()}`, machineSize: 'standard', repo: { url: REPO }, visibility: 'team' }
})
const startEnvs = () => h.vercel.commands.filter((c) => c.command === START_COMMAND).map((c) => c.env)
const marks = () =>
h.vercel.commands.filter((c) => c.command === 'bash' && c.args?.[1] === GIT_SETUP_MARK_SCRIPT).map((c) => c.args?.[3])
const membersRuns = () => h.vercel.commands.filter((c) => c.command === MEMBERS_COMMAND)
const settle = async () => {
await Promise.allSettled(background.splice(0))
}
return {
h,
as,
create,
startEnvs,
marks,
membersRuns,
settle,
hold: () => (members = gate()),
release: () => members?.open(),
failMembers: (code: number) => (membersExit = code)
}
}
describe('create answers before member git setup', () => {
it('answers while the members install is still running, and tells the VM setup is pending', async () => {
const t = await setup()
const held = t.hold()
const created = await t.create()
expect(created.status).toBe(200)
// The VM's git holds commits and pushes on this mark until the install finishes.
expect(t.startEnvs().at(-1)?.ORCA_GIT_SETUP).toBe('pending')
expect(created.body.sandbox.gitSetup).toBeUndefined()
held.open()
await t.settle()
// The owner's own device is what the VM maps to the owner (git as the sender).
const payload = JSON.parse(Buffer.from(t.membersRuns().at(-1)!.env.ORCA_MEMBERS_B64!, 'base64').toString())
expect(payload.map((m: { userId: string }) => m.userId)).toContain('alice')
})
it('an immediate fetch works before the install finishes: the boot already routed the repo credential', async () => {
const t = await setup()
t.hold()
const created = await t.create()
const row = (await getSandbox(t.h.env.SANDBOX_DB, created.body.sandbox.id))!
const box = [...t.h.vercel.boxes.values()].find((b) => b.params.name.endsWith(row.id))!
expect(JSON.stringify(box.policy.allow['github.com'])).toContain(gitBasicAuthorization(githubPlaceholderToken(row)))
t.release()
await t.settle()
})
it('a failed install marks the VM setup failed and reports gitSetup "failed" until a later sync succeeds', async () => {
const t = await setup()
t.failMembers(1)
const created = await t.create()
const id: string = created.body.sandbox.id
await t.settle()
expect(t.marks()).toContain('failed')
const view = await t.h.call('GET', `/v1/sandboxes/${id}`, { bearer: await t.as('alice'), headers: GIT_SETUP_AWARE })
expect(view.body.sandbox.gitSetup).toBe('failed')
// Desktops that do not send the feature parse the view strictly and never see the field.
const strict = await t.h.call('GET', `/v1/sandboxes/${id}`, { bearer: await t.as('alice') })
expect(strict.body.sandbox.gitSetup).toBeUndefined()
// A restart whose install fails again keeps the failure (VM mark and flag): never a silent broken state.
const marksBefore = t.marks().length
const again = await t.h.call('POST', `/v1/sandboxes/${id}/restart`, { bearer: await t.as('alice') })
expect(again.status).toBe(200)
expect(t.marks().slice(marksBefore)).toEqual(['failed'])
const still = await t.h.call('GET', `/v1/sandboxes/${id}`, { bearer: await t.as('alice'), headers: GIT_SETUP_AWARE })
expect(still.body.sandbox.gitSetup).toBe('failed')
// Restart is the retry: it installs identities before it answers, and a success clears the failure.
t.failMembers(0)
const restarted = await t.h.call('POST', `/v1/sandboxes/${id}/restart`, { bearer: await t.as('alice') })
expect(restarted.status).toBe(200)
expect(t.startEnvs().at(-1)?.ORCA_GIT_SETUP).toBeUndefined()
const after = await t.h.call('GET', `/v1/sandboxes/${id}`, { bearer: await t.as('alice'), headers: GIT_SETUP_AWARE })
expect(after.body.sandbox.gitSetup).toBeUndefined()
})
it('a member who is not the creator gets their identity when they connect, before that call answers', async () => {
const t = await setup()
const created = await t.create()
const id: string = created.body.sandbox.id
await t.settle()
const before = t.membersRuns().length
const bob = await t.as('bob')
const { generateGrantRecipientKey } = await import('../../../../src/main/cloud-workspaces/sealed-grant-node')
const issued = await t.h.call('POST', `/v1/team/workspaces/${id}/grants`, {
bearer: bob,
body: { recipientPublicKeyB64: generateGrantRecipientKey().publicKeyB64, deviceLabel: 'b', deviceKind: 'desktop' }
})
expect(issued.status).toBe(200)
// The grant path is unchanged: synchronous, nothing left in the background.
expect(t.membersRuns().length).toBe(before + 1)
})
it('sleeping before the background install finishes still ends with identities installed on wake', async () => {
const t = await setup()
const held = t.hold()
const created = await t.create()
const id: string = created.body.sandbox.id
const alice = await t.as('alice')
expect((await t.h.call('POST', `/v1/sandboxes/${id}/suspend`, { bearer: alice })).status).toBe(200)
// The VM is gone: the held install fails and is recorded, never retried against a stopped box.
held.open()
await t.settle()
t.release()
const runsBeforeWake = t.membersRuns().length
const resumed = await t.h.call('POST', `/v1/sandboxes/${id}/resume`, { bearer: alice })
expect(resumed.status).toBe(200)
// Wake installs identities before it answers (the owner's device is connected), with no pending mark.
expect(t.membersRuns().length).toBeGreaterThan(runsBeforeWake)
expect(t.startEnvs().at(-1)?.ORCA_GIT_SETUP).toBeUndefined()
const view = await t.h.call('GET', `/v1/sandboxes/${id}`, { bearer: alice, headers: GIT_SETUP_AWARE })
expect(view.body.sandbox.gitSetup).toBeUndefined()
})
})