mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { uploadSshRelayRuntimeDraftAssets } from './ssh-relay-runtime-draft-upload.mjs'
|
|
|
|
const REPO = 'stablyai/orca'
|
|
const RELEASE_ID = 42
|
|
const TAG = 'v1.5.0-rc.1'
|
|
const SOURCE_COMMIT = 'a'.repeat(40)
|
|
const TOKEN = 'test-token'
|
|
const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz'
|
|
const BYTES = Buffer.from('immutable relay runtime bytes')
|
|
const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json'
|
|
const SECOND_BYTES = Buffer.from('immutable signed manifest bytes')
|
|
|
|
let root
|
|
|
|
function digest(bytes = BYTES) {
|
|
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
}
|
|
|
|
function localAsset(overrides = {}) {
|
|
return {
|
|
name: NAME,
|
|
path: join(root, NAME),
|
|
sha256: digest(),
|
|
size: BYTES.length,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function draft(assets = [], overrides = {}) {
|
|
return {
|
|
id: RELEASE_ID,
|
|
tag_name: TAG,
|
|
target_commitish: 'main',
|
|
draft: true,
|
|
assets,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function tagReference(sha = SOURCE_COMMIT, type = 'commit') {
|
|
return { object: { sha, type } }
|
|
}
|
|
|
|
function uploadedAsset(overrides = {}) {
|
|
return { id: 101, name: NAME, state: 'uploaded', size: BYTES.length, ...overrides }
|
|
}
|
|
|
|
function json(body, status = 200) {
|
|
return Response.json(body, { status })
|
|
}
|
|
|
|
async function requestBytes(options) {
|
|
const chunks = []
|
|
for await (const chunk of options.body) {
|
|
chunks.push(Buffer.from(chunk))
|
|
}
|
|
return Buffer.concat(chunks)
|
|
}
|
|
|
|
function input(fetchImpl, overrides = {}) {
|
|
return {
|
|
repo: REPO,
|
|
releaseId: RELEASE_ID,
|
|
tag: TAG,
|
|
sourceCommit: SOURCE_COMMIT,
|
|
token: TOKEN,
|
|
assets: [localAsset()],
|
|
fetchImpl,
|
|
delayImpl: vi.fn(async () => {}),
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'orca-relay-draft-upload-'))
|
|
await writeFile(join(root, NAME), BYTES)
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(root, { recursive: true, force: true })
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('SSH relay runtime draft upload', () => {
|
|
it('uploads only locally verified immutable bytes to the exact draft', async () => {
|
|
const bodies = []
|
|
const fetchImpl = vi.fn(async (url, options = {}) => {
|
|
if (options.method === 'POST') {
|
|
bodies.push(await requestBytes(options))
|
|
return json(uploadedAsset(), 201)
|
|
}
|
|
if (url.includes('/git/ref/tags/')) {
|
|
return json(tagReference())
|
|
}
|
|
return json(draft())
|
|
})
|
|
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toEqual({
|
|
releaseId: RELEASE_ID,
|
|
tag: TAG,
|
|
sourceCommit: SOURCE_COMMIT,
|
|
reusedAssets: [],
|
|
uploadedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }]
|
|
})
|
|
expect(bodies).toEqual([BYTES])
|
|
const [url, options] = fetchImpl.mock.calls.find(([, value]) => value.method === 'POST')
|
|
expect(url).toBe(
|
|
`https://uploads.github.com/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${NAME}`
|
|
)
|
|
expect(options.headers).toMatchObject({
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
'Content-Length': String(BYTES.length),
|
|
'Content-Type': 'application/octet-stream'
|
|
})
|
|
})
|
|
|
|
it('reuses only same-draft assets whose downloaded bytes match', async () => {
|
|
const location = 'https://release-assets.githubusercontent.com/example/runtime?sig=signed'
|
|
const fetchImpl = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft([uploadedAsset()])))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } }))
|
|
.mockResolvedValueOnce(new Response(BYTES))
|
|
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({
|
|
reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }],
|
|
uploadedAssets: []
|
|
})
|
|
expect(fetchImpl).toHaveBeenCalledTimes(4)
|
|
expect(fetchImpl.mock.calls.some(([, options]) => options.method === 'POST')).toBe(false)
|
|
expect(fetchImpl.mock.calls[2][1].headers.Authorization).toBe(`Bearer ${TOKEN}`)
|
|
expect(fetchImpl.mock.calls[3][0]).toBe(location)
|
|
expect(fetchImpl.mock.calls[3][1].headers).not.toHaveProperty('Authorization')
|
|
})
|
|
|
|
it('peels annotated tags to the exact source commit', async () => {
|
|
const tagSha = 'b'.repeat(40)
|
|
const fetchImpl = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft()))
|
|
.mockResolvedValueOnce(json(tagReference(tagSha, 'tag')))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(json(uploadedAsset(), 201))
|
|
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({
|
|
uploadedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }]
|
|
})
|
|
expect(fetchImpl.mock.calls[2][0]).toBe(
|
|
`https://api.github.com/repos/${REPO}/git/tags/${tagSha}`
|
|
)
|
|
})
|
|
|
|
it('fails closed on unsafe draft state or existing managed bytes', async () => {
|
|
for (const release of [
|
|
draft([], { draft: false }),
|
|
draft([], { tag_name: 'v1.5.0-rc.2' }),
|
|
draft([uploadedAsset({ name: 'orca-ssh-relay-runtime-unexpected.zip' })])
|
|
]) {
|
|
await expect(
|
|
uploadSshRelayRuntimeDraftAssets(input(vi.fn().mockResolvedValueOnce(json(release))))
|
|
).rejects.toThrow(/draft|tag|source commit|unexpected managed asset/i)
|
|
}
|
|
|
|
const wrongSourceFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft()))
|
|
.mockResolvedValueOnce(json(tagReference('b'.repeat(40))))
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(wrongSourceFetch))).rejects.toThrow(
|
|
/source commit.*tag/i
|
|
)
|
|
|
|
const changedFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft([uploadedAsset()])))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response(Buffer.from('changed bytes')))
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(changedFetch))).rejects.toThrow(
|
|
/size|sha-?256/i
|
|
)
|
|
})
|
|
|
|
it('reconciles an uncertain retry before sending the same bytes again', async () => {
|
|
const fetchImpl = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft()))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response('unavailable', { status: 503 }))
|
|
.mockResolvedValueOnce(json(draft([uploadedAsset()])))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response(BYTES))
|
|
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({
|
|
reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }],
|
|
uploadedAssets: []
|
|
})
|
|
expect(fetchImpl.mock.calls.filter(([, options]) => options.method === 'POST')).toHaveLength(1)
|
|
})
|
|
|
|
it('leaves a partial draft recoverable without replacing uploaded bytes', async () => {
|
|
await writeFile(join(root, SECOND_NAME), SECOND_BYTES)
|
|
const assets = [
|
|
localAsset(),
|
|
localAsset({
|
|
name: SECOND_NAME,
|
|
path: join(root, SECOND_NAME),
|
|
sha256: digest(SECOND_BYTES),
|
|
size: SECOND_BYTES.length
|
|
})
|
|
]
|
|
const firstAttempt = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft()))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(json(uploadedAsset(), 201))
|
|
.mockResolvedValueOnce(new Response('denied', { status: 403 }))
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(firstAttempt, { assets }))).rejects.toThrow(
|
|
/403/i
|
|
)
|
|
|
|
const recovery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft([uploadedAsset()])))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response(BYTES))
|
|
.mockResolvedValueOnce(
|
|
json(uploadedAsset({ id: 102, name: SECOND_NAME, size: SECOND_BYTES.length }), 201)
|
|
)
|
|
await expect(
|
|
uploadSshRelayRuntimeDraftAssets(input(recovery, { assets }))
|
|
).resolves.toMatchObject({
|
|
reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }],
|
|
uploadedAssets: [
|
|
{ name: SECOND_NAME, sha256: digest(SECOND_BYTES), size: SECOND_BYTES.length }
|
|
]
|
|
})
|
|
})
|
|
|
|
it('bounds retries and detects local mutation before another upload attempt', async () => {
|
|
const retryingFetch = vi.fn(async (url, options = {}) => {
|
|
if (options.method === 'POST') {
|
|
return new Response('unavailable', { status: 503 })
|
|
}
|
|
return url.includes('/git/ref/tags/') ? json(tagReference()) : json(draft())
|
|
})
|
|
const delayImpl = vi.fn(async () => {})
|
|
await expect(
|
|
uploadSshRelayRuntimeDraftAssets(input(retryingFetch, { delayImpl }))
|
|
).rejects.toThrow(/retry exhaustion/i)
|
|
expect(
|
|
retryingFetch.mock.calls.filter(([, options]) => options.method === 'POST')
|
|
).toHaveLength(3)
|
|
expect(delayImpl).toHaveBeenCalledTimes(2)
|
|
|
|
const mutationFetch = vi.fn(async (url, options = {}) => {
|
|
if (options.method === 'POST') {
|
|
return new Response('unavailable', { status: 503 })
|
|
}
|
|
return url.includes('/git/ref/tags/') ? json(tagReference()) : json(draft())
|
|
})
|
|
const mutateBeforeRetry = vi.fn(async () => {
|
|
await writeFile(join(root, NAME), Buffer.from('mutated relay runtime bytes'))
|
|
})
|
|
await expect(
|
|
uploadSshRelayRuntimeDraftAssets(input(mutationFetch, { delayImpl: mutateBeforeRetry }))
|
|
).rejects.toThrow(/local.*size|local.*sha-?256/i)
|
|
expect(
|
|
mutationFetch.mock.calls.filter(([, options]) => options.method === 'POST')
|
|
).toHaveLength(1)
|
|
})
|
|
|
|
it('does not retry authorization failures and honors cancellation before requests', async () => {
|
|
const deniedFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(json(draft()))
|
|
.mockResolvedValueOnce(json(tagReference()))
|
|
.mockResolvedValueOnce(new Response('denied', { status: 403 }))
|
|
await expect(uploadSshRelayRuntimeDraftAssets(input(deniedFetch))).rejects.toThrow(/403/i)
|
|
expect(deniedFetch.mock.calls.filter(([, options]) => options.method === 'POST')).toHaveLength(
|
|
1
|
|
)
|
|
|
|
const controller = new AbortController()
|
|
controller.abort(new Error('cancel draft upload'))
|
|
const cancelledFetch = vi.fn()
|
|
await expect(
|
|
uploadSshRelayRuntimeDraftAssets(input(cancelledFetch, { signal: controller.signal }))
|
|
).rejects.toThrow(/cancel draft upload/i)
|
|
expect(cancelledFetch).not.toHaveBeenCalled()
|
|
})
|
|
})
|