mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
test(skills): cover paired runtime installation
This commit is contained in:
@@ -138,4 +138,47 @@ describe('installSkillOnRemoteRuntime', () => {
|
||||
).rejects.toThrow('skill-install-remote-runtime_error')
|
||||
expect(cleanup).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('uses the client for a configured development origin rejected by the host', async () => {
|
||||
const cleanup = vi.fn(async () => undefined)
|
||||
mocks.callRuntimeEnvironment
|
||||
.mockResolvedValueOnce({
|
||||
id: 'rpc-1',
|
||||
ok: false,
|
||||
error: { code: 'runtime_error', message: 'skill-download-origin-rejected' }
|
||||
})
|
||||
.mockResolvedValueOnce(success(result))
|
||||
mocks.transferSkillPackageToRuntime.mockResolvedValue({ uploadId: 'upload-1', cleanup })
|
||||
|
||||
await expect(
|
||||
installSkillOnRemoteRuntime({
|
||||
userDataPath: '/state',
|
||||
environmentId: 'environment-1',
|
||||
request,
|
||||
capabilities: ['skills.install.v1', 'skills.upload.v1'],
|
||||
requireHttps: false
|
||||
})
|
||||
).resolves.toEqual(result)
|
||||
expect(mocks.transferSkillPackageToRuntime).toHaveBeenCalledOnce()
|
||||
expect(cleanup).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('preserves packaged-host policy rejection without attempting transfer', async () => {
|
||||
mocks.callRuntimeEnvironment.mockResolvedValue({
|
||||
id: 'rpc-1',
|
||||
ok: false,
|
||||
error: { code: 'runtime_error', message: 'skill-download-origin-rejected' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
installSkillOnRemoteRuntime({
|
||||
userDataPath: '/state',
|
||||
environmentId: 'environment-1',
|
||||
request,
|
||||
capabilities: ['skills.install.v1', 'skills.upload.v1'],
|
||||
requireHttps: true
|
||||
})
|
||||
).rejects.toThrow('skill-install-remote-runtime_error')
|
||||
expect(mocks.transferSkillPackageToRuntime).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@ import { SkillInstallFailureSchema } from '../../shared/skill-install-failure'
|
||||
import { SkillInstallOperationError } from './skill-install-operation-error'
|
||||
|
||||
const DIRECT_DOWNLOAD_FAILURE = 'skill-download-transport-failed'
|
||||
const DEVELOPMENT_DOWNLOAD_POLICY_FAILURES = new Set([
|
||||
'skill-download-url-rejected',
|
||||
'skill-download-origin-rejected'
|
||||
])
|
||||
|
||||
async function install(
|
||||
userDataPath: string,
|
||||
@@ -27,14 +31,23 @@ async function install(
|
||||
)) as RuntimeRpcResponse<unknown>
|
||||
}
|
||||
|
||||
function isDownloadTransportFailure(response: RuntimeRpcResponse<unknown>): boolean {
|
||||
function isDirectDownloadUnavailable(
|
||||
response: RuntimeRpcResponse<unknown>,
|
||||
requireHttps: boolean
|
||||
): boolean {
|
||||
const structured =
|
||||
response.ok === false ? SkillInstallFailureSchema.safeParse(response.error.data) : null
|
||||
if (response.ok === true) {
|
||||
return false
|
||||
}
|
||||
const codes = [
|
||||
response.error.code,
|
||||
response.error.message,
|
||||
...(structured?.success === true ? [structured.data.code] : [])
|
||||
]
|
||||
return (
|
||||
response.ok === false &&
|
||||
(response.error.code === DIRECT_DOWNLOAD_FAILURE ||
|
||||
response.error.message === DIRECT_DOWNLOAD_FAILURE ||
|
||||
(structured?.success === true && structured.data.code === DIRECT_DOWNLOAD_FAILURE))
|
||||
codes.includes(DIRECT_DOWNLOAD_FAILURE) ||
|
||||
(!requireHttps && codes.some((code) => DEVELOPMENT_DOWNLOAD_POLICY_FAILURES.has(code)))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +73,7 @@ export async function installSkillOnRemoteRuntime(input: {
|
||||
throw new Error('skill-install-remote-ingress-invalid')
|
||||
}
|
||||
const direct = await install(input.userDataPath, input.environmentId, input.request)
|
||||
if (!isDownloadTransportFailure(direct)) {
|
||||
if (!isDirectDownloadUnavailable(direct, input.requireHttps)) {
|
||||
if (direct.ok !== true) {
|
||||
throw remoteFailure(direct)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,8 @@ export async function launchPairedWebClient(
|
||||
export async function launchPairedElectronClient(
|
||||
offer: RuntimeDesktopPairingOffer,
|
||||
testInfo: TestInfo,
|
||||
name: string
|
||||
name: string,
|
||||
options: { extraEnv?: Record<string, string> } = {}
|
||||
): Promise<PairedElectronClient> {
|
||||
const userDataDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-paired-desktop-'))
|
||||
const directSshProbePath = path.join(userDataDir, 'forbidden-local-ssh-connects.jsonl')
|
||||
@@ -154,7 +155,7 @@ export async function launchPairedElectronClient(
|
||||
const homeIsolation = createElectronHomeIsolation({
|
||||
inheritedEnv: cleanEnv,
|
||||
launchEnv: {},
|
||||
extraEnv: {},
|
||||
extraEnv: options.extraEnv ?? {},
|
||||
userDataDir
|
||||
})
|
||||
const mainPath = path.join(process.cwd(), 'out', 'main', 'index.js')
|
||||
|
||||
+24
-24
@@ -8,13 +8,13 @@ import {
|
||||
} from '../../../src/main/skills/skill-package-creation'
|
||||
import { SKILL_PACKAGE_CONTENT_TYPE } from '../../../src/shared/skill-package-manifest'
|
||||
|
||||
export const SSH_SKILL_CLOUD_PORT = Number(process.env.ORCA_E2E_SKILL_CLOUD_PORT ?? '43961')
|
||||
export const SSH_SKILL_CLOUD_ORIGIN = `http://127.0.0.1:${SSH_SKILL_CLOUD_PORT}`
|
||||
export const SSH_SKILL_PACKAGE_ID = 'package_ssh_e2e'
|
||||
export const SSH_SKILL_VERSION_ID = 'version_ssh_e2e'
|
||||
export const SSH_SKILL_NAME = 'ssh-e2e-skill'
|
||||
export const REMOTE_SKILL_CLOUD_PORT = Number(process.env.ORCA_E2E_SKILL_CLOUD_PORT ?? '43961')
|
||||
export const REMOTE_SKILL_CLOUD_ORIGIN = `http://127.0.0.1:${REMOTE_SKILL_CLOUD_PORT}`
|
||||
export const REMOTE_SKILL_PACKAGE_ID = 'package_remote_e2e'
|
||||
export const REMOTE_SKILL_VERSION_ID = 'version_remote_e2e'
|
||||
export const REMOTE_SKILL_NAME = 'remote-e2e-skill'
|
||||
|
||||
export type SshSkillCloudFixture = {
|
||||
export type RemoteSkillCloudFixture = {
|
||||
archive: CreatedSkillPackage
|
||||
bytes: Buffer
|
||||
requests: { method: string; path: string; body: unknown }[]
|
||||
@@ -22,25 +22,25 @@ export type SshSkillCloudFixture = {
|
||||
server: Server
|
||||
}
|
||||
|
||||
export async function startSshSkillCloudFixture(): Promise<SshSkillCloudFixture> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-ssh-skill-cloud-'))
|
||||
export async function startRemoteSkillCloudFixture(): Promise<RemoteSkillCloudFixture> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-remote-skill-cloud-'))
|
||||
const source = join(root, 'source')
|
||||
await mkdir(source)
|
||||
await writeFile(
|
||||
join(source, 'SKILL.md'),
|
||||
'---\nname: ssh-e2e-skill\ndescription: SSH relay integration\n---\n\n# SSH E2E\n'
|
||||
'---\nname: remote-e2e-skill\ndescription: Remote installation integration\n---\n\n# Remote E2E\n'
|
||||
)
|
||||
const archive = await createSkillPackageArchive({
|
||||
sourceDirectory: source,
|
||||
archivePath: join(root, 'package.tar.gz'),
|
||||
packageId: SSH_SKILL_PACKAGE_ID,
|
||||
versionId: SSH_SKILL_VERSION_ID,
|
||||
packageId: REMOTE_SKILL_PACKAGE_ID,
|
||||
versionId: REMOTE_SKILL_VERSION_ID,
|
||||
createdAt: '2026-08-11T12:00:00.000Z'
|
||||
})
|
||||
const bytes = await readFile(archive.archivePath)
|
||||
const requests: SshSkillCloudFixture['requests'] = []
|
||||
const requests: RemoteSkillCloudFixture['requests'] = []
|
||||
const server = createServer((request, response) => {
|
||||
void handleSshSkillCloudRequest({ request, response, archive, bytes, requests }).catch(
|
||||
void handleRemoteSkillCloudRequest({ request, response, archive, bytes, requests }).catch(
|
||||
(error) => {
|
||||
response.writeHead(500, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({ code: 'fixture_failed', message: String(error) }))
|
||||
@@ -49,24 +49,24 @@ export async function startSshSkillCloudFixture(): Promise<SshSkillCloudFixture>
|
||||
})
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(SSH_SKILL_CLOUD_PORT, '127.0.0.1', resolve)
|
||||
server.listen(REMOTE_SKILL_CLOUD_PORT, '127.0.0.1', resolve)
|
||||
})
|
||||
return { archive, bytes, requests, root, server }
|
||||
}
|
||||
|
||||
export async function stopSshSkillCloudFixture(fixture: SshSkillCloudFixture): Promise<void> {
|
||||
export async function stopRemoteSkillCloudFixture(fixture: RemoteSkillCloudFixture): Promise<void> {
|
||||
await new Promise<void>((resolve) => fixture.server.close(() => resolve()))
|
||||
await rm(fixture.root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function handleSshSkillCloudRequest(input: {
|
||||
async function handleRemoteSkillCloudRequest(input: {
|
||||
request: IncomingMessage
|
||||
response: ServerResponse
|
||||
archive: CreatedSkillPackage
|
||||
bytes: Buffer
|
||||
requests: SshSkillCloudFixture['requests']
|
||||
requests: RemoteSkillCloudFixture['requests']
|
||||
}): Promise<void> {
|
||||
const path = new URL(input.request.url ?? '/', SSH_SKILL_CLOUD_ORIGIN).pathname
|
||||
const path = new URL(input.request.url ?? '/', REMOTE_SKILL_CLOUD_ORIGIN).pathname
|
||||
if (input.request.method === 'GET' && path === '/package.tar.gz') {
|
||||
input.requests.push({ method: 'GET', path, body: null })
|
||||
input.response.writeHead(200, {
|
||||
@@ -79,7 +79,7 @@ async function handleSshSkillCloudRequest(input: {
|
||||
if (
|
||||
input.request.method === 'POST' &&
|
||||
path ===
|
||||
`/v1/skill-packages/${SSH_SKILL_PACKAGE_ID}/versions/${SSH_SKILL_VERSION_ID}/download-grants`
|
||||
`/v1/skill-packages/${REMOTE_SKILL_PACKAGE_ID}/versions/${REMOTE_SKILL_VERSION_ID}/download-grants`
|
||||
) {
|
||||
const body = JSON.parse(await readRequestBody(input.request)) as unknown
|
||||
input.requests.push({ method: 'POST', path, body })
|
||||
@@ -94,14 +94,14 @@ async function handleSshSkillCloudRequest(input: {
|
||||
function downloadGrant(archive: CreatedSkillPackage, compressedBytes: number) {
|
||||
return {
|
||||
grant: {
|
||||
url: `${SSH_SKILL_CLOUD_ORIGIN}/package.tar.gz`,
|
||||
url: `${REMOTE_SKILL_CLOUD_ORIGIN}/package.tar.gz`,
|
||||
expiresAt: '2099-01-01T00:00:00.000Z'
|
||||
},
|
||||
version: {
|
||||
packageId: SSH_SKILL_PACKAGE_ID,
|
||||
versionId: SSH_SKILL_VERSION_ID,
|
||||
name: SSH_SKILL_NAME,
|
||||
description: 'SSH relay integration',
|
||||
packageId: REMOTE_SKILL_PACKAGE_ID,
|
||||
versionId: REMOTE_SKILL_VERSION_ID,
|
||||
name: REMOTE_SKILL_NAME,
|
||||
description: 'Remote installation integration',
|
||||
packageDigest: archive.manifest.packageDigest,
|
||||
archiveSha256: archive.archiveSha256,
|
||||
compressedBytes,
|
||||
@@ -0,0 +1,277 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import type { SkillInstallDestination } from '../../src/shared/skill-install-contract'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import {
|
||||
createRuntimeDesktopPairingOffer,
|
||||
launchPairedElectronClient,
|
||||
type PairedElectronClient
|
||||
} from './helpers/paired-electron-client'
|
||||
import {
|
||||
launchHeadlessPairedRuntimeHost,
|
||||
type HeadlessPairedRuntimeHost
|
||||
} from './helpers/headless-paired-runtime-host'
|
||||
import {
|
||||
REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
REMOTE_SKILL_NAME,
|
||||
REMOTE_SKILL_PACKAGE_ID,
|
||||
REMOTE_SKILL_VERSION_ID,
|
||||
startRemoteSkillCloudFixture,
|
||||
stopRemoteSkillCloudFixture,
|
||||
type RemoteSkillCloudFixture
|
||||
} from './helpers/remote-skill-cloud-fixture'
|
||||
|
||||
let cloud: RemoteSkillCloudFixture | null = null
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cloud = await startRemoteSkillCloudFixture()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (cloud) {
|
||||
await stopRemoteSkillCloudFixture(cloud)
|
||||
cloud = null
|
||||
}
|
||||
})
|
||||
|
||||
test('installs on a headed desktop runtime without a local fallback', async ({
|
||||
electronApp,
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
test.setTimeout(240_000)
|
||||
const fixture = requireCloudFixture()
|
||||
const requestStart = fixture.requests.length
|
||||
const folderRoot = mkdtempSync(join(tmpdir(), 'orca-paired-skill-folder-'))
|
||||
let client: PairedElectronClient | null = null
|
||||
try {
|
||||
const hostHome = await electronApp.evaluate(({ app }) => app.getPath('home'))
|
||||
const worktreeId = await activeWorktreeId(orcaPage)
|
||||
const folderWorkspaceId = await createHostFolderWorkspace(orcaPage, folderRoot)
|
||||
client = await launchPairedElectronClient(
|
||||
await createRuntimeDesktopPairingOffer(orcaPage),
|
||||
testInfo,
|
||||
'Skill installation client',
|
||||
{ extraEnv: cloudClientEnvironment() }
|
||||
)
|
||||
await connectCloud(client.page)
|
||||
const clientHome = await client.app.evaluate(({ app }) => app.getPath('home'))
|
||||
|
||||
await installPreviewRemove(client.page, client.environmentId, { scope: 'global' }, hostHome)
|
||||
await installPreviewRemove(
|
||||
client.page,
|
||||
client.environmentId,
|
||||
{ scope: 'workspace', worktreeId },
|
||||
testRepoPath
|
||||
)
|
||||
await installPreviewRemove(
|
||||
client.page,
|
||||
client.environmentId,
|
||||
{ scope: 'workspace', folderWorkspaceId },
|
||||
folderRoot
|
||||
)
|
||||
|
||||
expect(existsSync(skillPath(clientHome))).toBe(false)
|
||||
await expectManagedInstalls(client.page, client.environmentId, [])
|
||||
expectCloudRequests(fixture, requestStart, 3)
|
||||
} finally {
|
||||
await client?.dispose()
|
||||
rmSync(skillPath(testRepoPath), { recursive: true, force: true })
|
||||
rmSync(folderRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('installs on a headless serve runtime through the same contract', async ({
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
test.setTimeout(240_000)
|
||||
const fixture = requireCloudFixture()
|
||||
const requestStart = fixture.requests.length
|
||||
const host = await launchHeadlessPairedRuntimeHost()
|
||||
let client: PairedElectronClient | null = null
|
||||
try {
|
||||
client = await launchPairedElectronClient(host.offer, testInfo, 'Headless skill client', {
|
||||
extraEnv: cloudClientEnvironment()
|
||||
})
|
||||
await connectCloud(client.page)
|
||||
const hostHome = await host.app.evaluate(({ app }) => app.getPath('home'))
|
||||
const clientHome = await client.app.evaluate(({ app }) => app.getPath('home'))
|
||||
const worktreeId = await addHeadlessHostWorktree(host, testRepoPath)
|
||||
|
||||
await installPreviewRemove(client.page, client.environmentId, { scope: 'global' }, hostHome)
|
||||
await installPreviewRemove(
|
||||
client.page,
|
||||
client.environmentId,
|
||||
{ scope: 'workspace', worktreeId },
|
||||
testRepoPath
|
||||
)
|
||||
|
||||
expect(existsSync(skillPath(clientHome))).toBe(false)
|
||||
await expectManagedInstalls(client.page, client.environmentId, [])
|
||||
expectCloudRequests(fixture, requestStart, 2)
|
||||
} finally {
|
||||
await client?.dispose()
|
||||
await host.dispose()
|
||||
rmSync(skillPath(testRepoPath), { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function cloudClientEnvironment(): Record<string, string> {
|
||||
return {
|
||||
ORCA_ARTIFACTS_API_URL: REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_CLOUD_API_URL: REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_CLOUD_CLIENT_ID: 'skills-e2e-client',
|
||||
ORCA_CLOUD_DEV_AUTH: '1',
|
||||
ORCA_CLOUD_ALLOW_PLAINTEXT_SESSION: '1',
|
||||
ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS: REMOTE_SKILL_CLOUD_ORIGIN
|
||||
}
|
||||
}
|
||||
|
||||
async function connectCloud(page: Page): Promise<void> {
|
||||
const auth = await page.evaluate(() => window.api.orcaProfiles.connectCurrent())
|
||||
expect(auth.status).toBe('connected')
|
||||
}
|
||||
|
||||
function requireCloudFixture(): RemoteSkillCloudFixture {
|
||||
if (!cloud) {
|
||||
throw new Error('skill Cloud fixture unavailable')
|
||||
}
|
||||
return cloud
|
||||
}
|
||||
|
||||
function skillPath(root: string): string {
|
||||
return join(root, '.agents', 'skills', REMOTE_SKILL_NAME)
|
||||
}
|
||||
|
||||
async function activeWorktreeId(page: Page): Promise<string> {
|
||||
const worktreeId = await page.evaluate(() => window.__store?.getState().activeWorktreeId ?? null)
|
||||
if (!worktreeId) {
|
||||
throw new Error('headed host worktree unavailable')
|
||||
}
|
||||
return worktreeId
|
||||
}
|
||||
|
||||
async function createHostFolderWorkspace(page: Page, folderPath: string): Promise<string> {
|
||||
return page.evaluate(async (path) => {
|
||||
const group = await window.api.projectGroups.create({
|
||||
name: 'Paired skill folder E2E',
|
||||
parentPath: path
|
||||
})
|
||||
const workspace = await window.api.folderWorkspaces.create({
|
||||
projectGroupId: group.id,
|
||||
name: 'Paired skill folder E2E',
|
||||
folderPath: path
|
||||
})
|
||||
return workspace.id
|
||||
}, folderPath)
|
||||
}
|
||||
|
||||
async function addHeadlessHostWorktree(
|
||||
host: HeadlessPairedRuntimeHost,
|
||||
repoPath: string
|
||||
): Promise<string> {
|
||||
const added = await host.client.call<{ repo: { id: string } }>('repo.add', {
|
||||
path: repoPath,
|
||||
kind: 'git'
|
||||
})
|
||||
let worktreeId = ''
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const listed = await host.client.call<{ worktrees: { id: string }[] }>('worktree.list', {
|
||||
repo: `id:${added.result.repo.id}`
|
||||
})
|
||||
worktreeId = listed.result.worktrees[0]?.id ?? ''
|
||||
return worktreeId
|
||||
})
|
||||
.not.toBe('')
|
||||
return worktreeId
|
||||
}
|
||||
|
||||
async function installPreviewRemove(
|
||||
page: Page,
|
||||
environmentId: string,
|
||||
destination: SkillInstallDestination,
|
||||
root: string
|
||||
): Promise<void> {
|
||||
const operation = await page.evaluate(
|
||||
({ destination, environmentId, packageId, versionId }) =>
|
||||
window.api.skills.installPackageVersion({
|
||||
packageId,
|
||||
versionId,
|
||||
environmentId,
|
||||
destination
|
||||
}),
|
||||
{
|
||||
destination,
|
||||
environmentId,
|
||||
packageId: REMOTE_SKILL_PACKAGE_ID,
|
||||
versionId: REMOTE_SKILL_VERSION_ID
|
||||
}
|
||||
)
|
||||
expect(operation, JSON.stringify(operation, null, 2)).toMatchObject({
|
||||
status: 'ok',
|
||||
value: { status: 'installed', name: REMOTE_SKILL_NAME }
|
||||
})
|
||||
expect(readFileSync(join(skillPath(root), 'SKILL.md'), 'utf8')).toContain('# Remote E2E')
|
||||
|
||||
const fixture = requireCloudFixture()
|
||||
const preview = await page.evaluate(
|
||||
({ destination, environmentId, name, packageIdentity }) =>
|
||||
window.api.skills.previewInstall({
|
||||
environmentId,
|
||||
package: packageIdentity,
|
||||
name,
|
||||
destination
|
||||
}),
|
||||
{
|
||||
destination,
|
||||
environmentId,
|
||||
name: REMOTE_SKILL_NAME,
|
||||
packageIdentity: {
|
||||
packageId: fixture.archive.manifest.packageId,
|
||||
versionId: fixture.archive.manifest.versionId,
|
||||
packageDigest: fixture.archive.manifest.packageDigest,
|
||||
archiveSha256: fixture.archive.archiveSha256,
|
||||
compressedBytes: fixture.bytes.length
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(preview).toMatchObject({ status: 'ok', value: { currentState: 'unchanged' } })
|
||||
|
||||
const removed = await page.evaluate(
|
||||
({ destination, environmentId, name }) =>
|
||||
window.api.skills.removeInstall({ environmentId, name, destination }),
|
||||
{ destination, environmentId, name: REMOTE_SKILL_NAME }
|
||||
)
|
||||
expect(removed).toMatchObject({ status: 'ok', value: { status: 'removed' } })
|
||||
expect(existsSync(skillPath(root))).toBe(false)
|
||||
}
|
||||
|
||||
async function expectManagedInstalls(
|
||||
page: Page,
|
||||
environmentId: string,
|
||||
expected: unknown[]
|
||||
): Promise<void> {
|
||||
const installs = await page.evaluate(
|
||||
(id) => window.api.skills.listManagedInstalls(id),
|
||||
environmentId
|
||||
)
|
||||
expect(installs).toMatchObject({ status: 'ok', value: expected })
|
||||
}
|
||||
|
||||
function expectCloudRequests(
|
||||
fixture: RemoteSkillCloudFixture,
|
||||
start: number,
|
||||
expectedInstalls: number
|
||||
): void {
|
||||
const requests = fixture.requests.slice(start)
|
||||
expect(requests.filter((request) => request.method === 'POST')).toHaveLength(expectedInstalls)
|
||||
expect(requests.filter((request) => request.path === '/package.tar.gz')).toHaveLength(
|
||||
expectedInstalls
|
||||
)
|
||||
expect(
|
||||
requests.filter((request) => request.method === 'POST').map((request) => request.body)
|
||||
).toEqual(Array.from({ length: expectedInstalls }, () => ({ installTarget: 'remote' })))
|
||||
}
|
||||
@@ -10,40 +10,40 @@ import {
|
||||
import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection'
|
||||
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import {
|
||||
SSH_SKILL_CLOUD_ORIGIN,
|
||||
SSH_SKILL_NAME,
|
||||
SSH_SKILL_PACKAGE_ID,
|
||||
SSH_SKILL_VERSION_ID,
|
||||
startSshSkillCloudFixture,
|
||||
stopSshSkillCloudFixture,
|
||||
type SshSkillCloudFixture
|
||||
} from './helpers/ssh-skill-cloud-fixture'
|
||||
REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
REMOTE_SKILL_NAME,
|
||||
REMOTE_SKILL_PACKAGE_ID,
|
||||
REMOTE_SKILL_VERSION_ID,
|
||||
startRemoteSkillCloudFixture,
|
||||
stopRemoteSkillCloudFixture,
|
||||
type RemoteSkillCloudFixture
|
||||
} from './helpers/remote-skill-cloud-fixture'
|
||||
|
||||
const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1'
|
||||
const REMOTE_FOLDER = '/tmp/orca-skill-folder-workspace'
|
||||
|
||||
let cloud: SshSkillCloudFixture | null = null
|
||||
let cloud: RemoteSkillCloudFixture | null = null
|
||||
|
||||
test.use({
|
||||
orcaAppExtraEnv: {
|
||||
ORCA_ARTIFACTS_API_URL: SSH_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_CLOUD_API_URL: SSH_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_ARTIFACTS_API_URL: REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_CLOUD_API_URL: REMOTE_SKILL_CLOUD_ORIGIN,
|
||||
ORCA_CLOUD_CLIENT_ID: 'skills-e2e-client',
|
||||
ORCA_CLOUD_DEV_AUTH: '1',
|
||||
ORCA_CLOUD_ALLOW_PLAINTEXT_SESSION: '1',
|
||||
ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS: SSH_SKILL_CLOUD_ORIGIN
|
||||
ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS: REMOTE_SKILL_CLOUD_ORIGIN
|
||||
}
|
||||
})
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cloud = await startSshSkillCloudFixture()
|
||||
cloud = await startRemoteSkillCloudFixture()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (!cloud) {
|
||||
return
|
||||
}
|
||||
await stopSshSkillCloudFixture(cloud)
|
||||
await stopRemoteSkillCloudFixture(cloud)
|
||||
cloud = null
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('SSH skill installation', () => {
|
||||
orcaPage,
|
||||
target,
|
||||
globalDestination,
|
||||
'/root/.agents/skills/ssh-e2e-skill'
|
||||
'/root/.agents/skills/remote-e2e-skill'
|
||||
)
|
||||
const globalInstalls = await orcaPage.evaluate(
|
||||
(environmentId) => window.api.skills.listManagedInstalls(environmentId),
|
||||
@@ -84,7 +84,7 @@ test.describe('SSH skill installation', () => {
|
||||
status: 'ok',
|
||||
value: [
|
||||
{
|
||||
name: SSH_SKILL_NAME,
|
||||
name: REMOTE_SKILL_NAME,
|
||||
destination: {
|
||||
scope: 'global',
|
||||
executionTarget: { kind: 'ssh', connectionId: remote.targetId }
|
||||
@@ -97,14 +97,14 @@ test.describe('SSH skill installation', () => {
|
||||
orcaPage,
|
||||
target,
|
||||
globalDestination,
|
||||
'/root/.agents/skills/ssh-e2e-skill'
|
||||
'/root/.agents/skills/remote-e2e-skill'
|
||||
)
|
||||
|
||||
const worktreeDestination: SkillInstallDestination = {
|
||||
scope: 'workspace',
|
||||
worktreeId: remote.worktreeId
|
||||
}
|
||||
const worktreePath = '/tmp/orca-docker-relay-perf-repo/.agents/skills/ssh-e2e-skill'
|
||||
const worktreePath = '/tmp/orca-docker-relay-perf-repo/.agents/skills/remote-e2e-skill'
|
||||
await installAndVerify(orcaPage, target, worktreeDestination, worktreePath)
|
||||
await removeAndVerify(orcaPage, target, worktreeDestination, worktreePath)
|
||||
|
||||
@@ -113,7 +113,7 @@ test.describe('SSH skill installation', () => {
|
||||
scope: 'workspace',
|
||||
folderWorkspaceId
|
||||
}
|
||||
const folderPath = `${REMOTE_FOLDER}/.agents/skills/ssh-e2e-skill`
|
||||
const folderPath = `${REMOTE_FOLDER}/.agents/skills/remote-e2e-skill`
|
||||
await installAndVerify(orcaPage, target, folderDestination, folderPath)
|
||||
await removeAndVerify(orcaPage, target, folderDestination, folderPath)
|
||||
|
||||
@@ -136,7 +136,7 @@ test.describe('SSH skill installation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
function requireCloudFixture(): SshSkillCloudFixture {
|
||||
function requireCloudFixture(): RemoteSkillCloudFixture {
|
||||
if (!cloud) {
|
||||
throw new Error('skill Cloud fixture unavailable')
|
||||
}
|
||||
@@ -158,16 +158,16 @@ async function installAndVerify(
|
||||
}),
|
||||
{
|
||||
destination,
|
||||
packageId: SSH_SKILL_PACKAGE_ID,
|
||||
versionId: SSH_SKILL_VERSION_ID
|
||||
packageId: REMOTE_SKILL_PACKAGE_ID,
|
||||
versionId: REMOTE_SKILL_VERSION_ID
|
||||
}
|
||||
)
|
||||
expect(operation, JSON.stringify(operation, null, 2)).toMatchObject({
|
||||
status: 'ok',
|
||||
value: { status: 'installed', name: SSH_SKILL_NAME }
|
||||
value: { status: 'installed', name: REMOTE_SKILL_NAME }
|
||||
})
|
||||
const contents = execDockerSshRelayTargetCommand(target, `cat ${remotePath}/SKILL.md`)
|
||||
expect(contents).toContain('# SSH E2E')
|
||||
expect(contents).toContain('# Remote E2E')
|
||||
}
|
||||
|
||||
async function previewUnchanged(page: Page, destination: SkillInstallDestination): Promise<void> {
|
||||
@@ -181,7 +181,7 @@ async function previewUnchanged(page: Page, destination: SkillInstallDestination
|
||||
}),
|
||||
{
|
||||
destination,
|
||||
name: SSH_SKILL_NAME,
|
||||
name: REMOTE_SKILL_NAME,
|
||||
packageIdentity: {
|
||||
packageId: fixture.archive.manifest.packageId,
|
||||
versionId: fixture.archive.manifest.versionId,
|
||||
@@ -202,7 +202,7 @@ async function removeAndVerify(
|
||||
): Promise<void> {
|
||||
const operation = await page.evaluate(
|
||||
({ destination, name }) => window.api.skills.removeInstall({ name, destination }),
|
||||
{ destination, name: SSH_SKILL_NAME }
|
||||
{ destination, name: REMOTE_SKILL_NAME }
|
||||
)
|
||||
expect(operation).toMatchObject({ status: 'ok', value: { status: 'removed' } })
|
||||
expect(execDockerSshRelayTargetCommand(target, `test ! -e ${remotePath} && echo removed`)).toBe(
|
||||
|
||||
Reference in New Issue
Block a user