mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(host-routing): resolve the execution host before reading a connection Three issues in one defect class: a resolver reads one spelling of one arbitrarily chosen row instead of resolving the worktree's execution host, so something local answers a question about a remote. returned that row's connectionId. With duplicate repo rows for one repo id it could pair a runtime owner with a client-owned SSH connection. It now resolves through the same ambiguity-aware index getRuntimeEnvironmentIdForWorktree uses, prefers the repo row for the host the worktree names, and derives the connection from the resolved host. Conflicting rows return `undefined` (this module's documented "cannot determine the host"), never `null`. `store.getRepo(worktree.repoId)?.connectionId ?? null`. `getRepo` is host-blind and the same repo id can exist on local, SSH and runtime hosts, so a remote worktree could spawn its PTY on the client with the remote cwd. resolveWorktreeLaunchHost picks the row for the worktree's host and reads the connection off that host; conflicting rows are unresolved, not local. session-partition owner maps that contradict each other. Both now compute through one shared function whose argument records the divergence. No behaviour change on either side: converging needs a read-both migration, since both partitions hold real data written by shipping builds. * fix(host-routing): keep nested SSH connections resolvable under a runtime host getRepoSshConnectionId read only the resolved execution host, so a repo row owned by a runtime that reaches a nested SSH target (connectionId: ssh-*, executionHostId: runtime:*) resolved to no connection — answering 'local' for a remote worktree, the same defect #17909 fixed in the other direction. * fix(host-routing): resolve both sides of the execution host through one rule The renderer resolver leaked between two different SSH hosts: a worktree on `ssh:m4air` whose only indexed repo row belonged to `openclaw` answered 'openclaw', because the host-scoped lookup missing fell through to an id-only one. Main's resolver, in the same change, answered 'm4air' — two resolvers, one right and one wrong, on identical input. Both sides now adapt one shared rule (`worktree-execution-host-resolution.ts`): the worktree's own host outranks every repo row, and a row on a different host is never evidence about this one. The renderer's WeakMap index becomes the memoizing adapter it always was; `resolveWorktreeLaunchHost` becomes main's mapping of unresolved onto its throw. Settles the rule the change previously answered two ways. `getRepoSshConnectionId` and `getSshTargetIdForExecutionHost` disagreed for a runtime host carrying a nested `connectionId`; they now compose, so the execution host is the single authority. On a `runtime:*` row that field is a paired HUB's private SSH target, spread through by `repoWithFetchedOwner` and unaddressable from this client — the project-first successor of the row nulls it for exactly that reason. That also fixes the `kind !== 'ssh'` fallback, which fired for `local`: a row declaring itself local handed out an SSH connection. * fix(ssh): resolve the execution host in the worktree scan and managed create The worktree scan and createManagedWorktree both picked remote-vs-local from repo.connectionId, so a row stamped only executionHostId: 'ssh:*' was scanned and created on the client against a remote path. The folder branch returns before the check, so its agent-trust write landed locally too. Refs #11163 * fix(ssh): stop over-rejecting and refusing SSH hosts the process owns runtimeRepoMatchesExecutionHost rejected an unstamped SSH repo against its own ssh:<connectionId>, so repo-add/clone dedupe could register a second row for a path the host already owns. assertHostIsSupported made the CLI/runtime RPC refuse --host ssh:* while the same process's IPC handler routed it correctly; setupExistingFolder now shares that registration. Clone still refuses, because nothing in this process clones onto an SSH host. Refs #11163 * test(ssh): retarget the SSH host-setup guard spec at the substitution it prevents setupProjectExistingFolder now registers the remote path through the same addRemoteRepoFromPath the desktop IPC uses, so it fails on the host's terms (connection not registered) rather than a categorical refusal. The local clone/probe side effects it exists to catch are still asserted absent. Refs #11163 * fix(cli): require an absolute path when setting a project up on an SSH host Routing --host ssh:* to the remote registration made relative paths newly reachable there, and they were resolved against the client cwd — registering a path that names the wrong machine. Refs #11163 * fix(repos): read the SSH registry directly so the runtime stays Node-bootable Routing runtime project setup through addRemoteRepoFromPath dragged ipc/ssh -- and its 25-module electron graph -- into the runtime bundle. ssh-target-registry already exists for exactly this; ipc/ssh only re-exports it. * fix(ssh): close the agent-launch and session-export host-blind twins Three sites left on the legacy spelling, all the same shape as the ones this branch already fixed: - `launchAgentTerminal` did `getRepo(worktree.repoId)` then wrote agent trust with that row's `connectionId`. Host-blind, so a repo id carried by two SSH hosts wrote a remote path into the *client's* Codex/Cursor/Copilot config and the agent on the host never saw the trust. Every sibling call site already passes the resolved `workspace.connectionId`; this was the last that did not. - `targetForWorktree` (workspace-session export) fell back to the same host-blind read, so a session could be published to a machine that never owned the worktree. Unresolvable ownership now exports to nobody. - `addRemoteRepoFromPath` minted `connectionId`-only rows while being the routing path this branch adds, so it kept creating rows in exactly the spelling the branch works around. It now stamps `toSshExecutionHostId(connectionId)` at creation; `reassignSshTargetId` already migrates both spellings, so target rename stays correct. Tests cover two *different* SSH hosts throughout — the case none of the earlier duplicate-row tests had, all of which were local-vs-ssh or runtime-vs-ssh.
816 lines
25 KiB
TypeScript
816 lines
25 KiB
TypeScript
import path from 'node:path'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
const {
|
|
callMock,
|
|
runtimeClientConstructorMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock,
|
|
addEnvironmentFromPairingCodeMock,
|
|
listEnvironmentsMock,
|
|
spawnMock
|
|
} = vi.hoisted(() => ({
|
|
callMock: vi.fn(),
|
|
runtimeClientConstructorMock: vi.fn(),
|
|
serveOrcaAppMock: vi.fn(),
|
|
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
|
|
addEnvironmentFromPairingCodeMock: vi.fn(),
|
|
listEnvironmentsMock: vi.fn(),
|
|
spawnMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./runtime-client', async () => {
|
|
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
|
|
return createRuntimeClientModuleMock({
|
|
callMock,
|
|
runtimeClientConstructorMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock
|
|
})
|
|
})
|
|
|
|
vi.mock('./runtime/environments', () => ({
|
|
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
|
|
listEnvironments: listEnvironmentsMock,
|
|
removeEnvironment: vi.fn(),
|
|
resolveEnvironment: vi.fn()
|
|
}))
|
|
|
|
vi.mock('child_process', async () => {
|
|
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
|
|
return createChildProcessModuleMock(spawnMock)
|
|
})
|
|
|
|
import { main } from './index'
|
|
import { okFixture, queueFixtures } from './test-fixtures'
|
|
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
|
|
|
|
describe('orca cli worktree awareness', () => {
|
|
useWorktreeAwarenessEnvironment({
|
|
callMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock,
|
|
addEnvironmentFromPairingCodeMock,
|
|
listEnvironmentsMock,
|
|
spawnMock
|
|
})
|
|
|
|
it('resolves repo.add paths against the invoking cli cwd', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_repo_add', {
|
|
repo: {
|
|
id: 'repo-1',
|
|
path: path.resolve('/tmp/repo/apps/web'),
|
|
displayName: 'web'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['repo', 'add', '--path', './apps/web', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('repo.add', {
|
|
path: path.resolve('/tmp/repo/apps/web')
|
|
})
|
|
})
|
|
|
|
it('lists projects through the project-first runtime API', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_list', {
|
|
projects: [
|
|
{
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
providerIdentity: {
|
|
provider: 'github',
|
|
owner: 'stablyai',
|
|
repo: 'orca'
|
|
},
|
|
sourceRepoIds: ['repo-1'],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
}
|
|
]
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['project', 'list', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('project.list')
|
|
})
|
|
|
|
it('routes a runtime host filter to that paired server and keeps its own local rows', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setups', {
|
|
setups: [
|
|
{
|
|
id: 'setup-local',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
repoId: 'repo-local',
|
|
path: '/tmp/orca',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'legacy-repo',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
{
|
|
id: 'setup-remote',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:gpu',
|
|
repoId: 'repo-remote',
|
|
path: '/srv/orca',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'legacy-repo',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
}
|
|
]
|
|
})
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['project', 'setups', '--project', 'github:stablyai/orca', '--host', 'runtime:gpu'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'gpu')
|
|
expect(callMock).toHaveBeenCalledWith('projectHostSetup.list')
|
|
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-remote')
|
|
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-local')
|
|
})
|
|
|
|
it('keeps --host local a filter on the selected environment rather than a second selector', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'prod')
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setups', {
|
|
setups: [
|
|
{
|
|
id: 'setup-on-box',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
repoId: 'repo-on-box',
|
|
path: '/srv/orca',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'legacy-repo',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
{
|
|
id: 'setup-by-client',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:prod',
|
|
repoId: 'repo-by-client',
|
|
path: '/srv/orca-2',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'legacy-repo',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
}
|
|
]
|
|
})
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['project', 'setups', '--environment', 'prod', '--host', 'local'], '/tmp/repo')
|
|
|
|
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'prod')
|
|
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-on-box')
|
|
expect(logSpy.mock.calls[0]?.[0]).not.toContain('setup-by-client')
|
|
})
|
|
|
|
// Why: --host runtime:<id> routes to a paired server, so an older one is reachable without the
|
|
// caller meaning to. A raw method_not_found reads as an Orca bug rather than a version gap.
|
|
it('names the version gap when the server predates project host setup', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'old-server')
|
|
const { RuntimeClientError } = await import('./runtime/types.js')
|
|
callMock.mockRejectedValueOnce(
|
|
new RuntimeClientError('method_not_found', 'Unknown method: projectHostSetup.list')
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(['project', 'setups', '--host', 'runtime:old-server', '--json'], '/tmp/repo')
|
|
|
|
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
|
|
expect(printed).toContain('does not support project host setup yet')
|
|
expect(printed).not.toContain('Unknown method')
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects a runtime host id that no paired server owns instead of answering empty', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(['project', 'setups', '--host', 'runtime:not-a-real-env', '--json'], '/tmp/repo')
|
|
|
|
// The command itself never reached a runtime; only the suggestion lookup did.
|
|
expect(callMock).not.toHaveBeenCalledWith('projectHostSetup.list')
|
|
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
|
|
expect(printed).toContain('no paired Orca server is named or has id not-a-real-env')
|
|
// An agent reads the code and the retry candidates, not the prose.
|
|
expect(JSON.parse(printed).error.code).toBe('invalid_argument')
|
|
expect(JSON.parse(printed).error.data.knownEnvironments).toEqual([])
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
// Why: ssh: was never validated, so an unknown target answered ok:true with an empty list —
|
|
// the same silent wrong-machine answer unknown runtime ids used to give.
|
|
it('rejects an unknown ssh host instead of answering empty', async () => {
|
|
queueFixtures(callMock, okFixture('req_ssh_targets', { targets: [] }))
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(['project', 'setups', '--host', 'ssh:openclaw', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalledWith('projectHostSetup.list')
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'no SSH target named or with id openclaw'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('tells a caller reaching for a paired server by ssh that it is an environment', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'awin')
|
|
queueFixtures(callMock, okFixture('req_ssh_targets', { targets: [] }))
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(['project', 'setups', '--host', 'ssh:awin', '--json'], '/tmp/repo')
|
|
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'--environment awin'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('resolves an ssh label to its target id before filtering', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_ssh_targets', { targets: [{ id: 'ssh-123-abc', label: 'openclaw' }] }),
|
|
okFixture('req_project_setups', {
|
|
setups: [
|
|
{
|
|
id: 'setup-openclaw',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'ssh:ssh-123-abc',
|
|
repoId: 'repo-openclaw',
|
|
path: '/home/me/orca',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'legacy-repo',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
}
|
|
]
|
|
})
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['project', 'setups', '--host', 'ssh:openclaw'], '/tmp/repo')
|
|
|
|
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-openclaw')
|
|
})
|
|
|
|
// Why: `runtime:<id>` is a persisted token — it lands in ProjectHostSetup.hostId and is
|
|
// embedded in generated setup ids. Accepting a name is only safe because it is canonicalized
|
|
// to the id before anything downstream sees it; this pins that.
|
|
it('never lets an environment name reach a persisted host id', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'env-uuid-1', 'awin')
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup_create', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: [],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-awin',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
repoId: '',
|
|
path: '',
|
|
displayName: 'awin',
|
|
setupState: 'setting-up',
|
|
setupMethod: 'provisioned',
|
|
createdAt: 1,
|
|
updatedAt: 2
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-create',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'runtime:awin',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'env-uuid-1')
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'projectHostSetup.create',
|
|
expect.objectContaining({ hostId: 'runtime:env-uuid-1' })
|
|
)
|
|
})
|
|
|
|
it('rejects a malformed --host value before contacting any runtime', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(['project', 'setups', '--host', 'runtime:', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Invalid --host value: runtime:'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('refuses a runtime host id alongside an unrelated --pairing-code connection', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
['project', 'setups', '--host', 'runtime:gpu', '--pairing-code', 'remote-runtime', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'use either --host runtime:<id> or --pairing-code, not both'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('sets up an existing project folder with a path resolved against the local cli cwd', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: ['repo-1'],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-local',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
repoId: 'repo-1',
|
|
path: path.resolve('/tmp/orca'),
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'imported-existing-folder',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
repo: {
|
|
id: 'repo-1',
|
|
path: path.resolve('/tmp/orca'),
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
addedAt: 1
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-existing-folder',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'local',
|
|
'--path',
|
|
'..',
|
|
'--kind',
|
|
'git',
|
|
'--display-name',
|
|
'Orca',
|
|
'--json'
|
|
],
|
|
'/tmp/orca/worktrees/feature'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('projectHostSetup.setupExistingFolder', {
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
path: path.resolve('/tmp/orca/worktrees'),
|
|
kind: 'git',
|
|
displayName: 'Orca'
|
|
})
|
|
})
|
|
|
|
it('rejects remote project setup relative paths instead of resolving against client cwd', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-existing-folder',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'runtime:gpu',
|
|
'--path',
|
|
'./orca',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Remote project setup requires --path to be an absolute path on the remote server.'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects SSH project setup relative paths, which name the client filesystem', async () => {
|
|
// A local CLI reaching an `ssh:*` host is still off-client: resolving `./orca` against the
|
|
// CLI cwd would register a path that exists on the wrong machine.
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-existing-folder',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'ssh:openclaw',
|
|
'--path',
|
|
'./orca',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Remote project setup requires --path to be an absolute path on the remote server.'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects remote repo.add relative paths instead of resolving against client cwd', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
['repo', 'add', '--path', './apps/web', '--pairing-code', 'remote-runtime', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Remote repo add requires --path to be an absolute path on the remote server.'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('sends remote repo.add absolute paths unchanged', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_repo_add', {
|
|
repo: {
|
|
id: 'repo-1',
|
|
path: '/srv/orca/web',
|
|
displayName: 'web'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['repo', 'add', '--path', '/srv/orca/web', '--pairing-code', 'remote-runtime', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('repo.add', {
|
|
path: '/srv/orca/web'
|
|
})
|
|
})
|
|
|
|
it.each(['C:\\repo', 'C:/repo', '\\\\server\\share\\repo', '//server/share/repo'])(
|
|
'sends remote repo.add server absolute path %s unchanged',
|
|
async (serverPath) => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_repo_add', {
|
|
repo: {
|
|
id: 'repo-1',
|
|
path: serverPath,
|
|
displayName: 'web'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['repo', 'add', '--path', serverPath, '--pairing-code', 'remote-runtime', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('repo.add', {
|
|
path: serverPath
|
|
})
|
|
}
|
|
)
|
|
|
|
// Why: STA-4792 defect 2. `--host runtime:<id>` used to leave the client local, so a Windows
|
|
// destination fell into resolve(cwd, ...) and became a literal directory next to the caller.
|
|
// Routing makes the client remote, which is what sends the path through untouched.
|
|
it('sends a windows destination to the routed host instead of joining it to the local cwd', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'awin')
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup_clone', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: [],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-awin',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'local',
|
|
repoId: 'repo-awin',
|
|
path: 'C:\\orca-probe\\orca',
|
|
displayName: 'Orca',
|
|
setupState: 'ready',
|
|
setupMethod: 'cloned',
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
repo: {
|
|
id: 'repo-awin',
|
|
path: 'C:\\orca-probe\\orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
addedAt: 1
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-clone',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'runtime:awin',
|
|
'--url',
|
|
'https://github.com/stablyai/orca.git',
|
|
'--destination',
|
|
'C:\\orca-probe',
|
|
'--json'
|
|
],
|
|
'/Users/nwparker/orca/workspaces/orca/IME-koko'
|
|
)
|
|
|
|
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'awin')
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'projectHostSetup.clone',
|
|
expect.objectContaining({ destination: 'C:\\orca-probe' })
|
|
)
|
|
})
|
|
|
|
it('updates project host setup metadata through the project-first runtime API', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup_update', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: [],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-gpu',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:gpu',
|
|
repoId: '',
|
|
path: '/srv/orca',
|
|
displayName: 'GPU VM',
|
|
setupState: 'ready',
|
|
setupMethod: 'imported-existing-folder',
|
|
createdAt: 1,
|
|
updatedAt: 2
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-update',
|
|
'--setup',
|
|
'setup-gpu',
|
|
'--display-name',
|
|
'GPU VM',
|
|
'--path',
|
|
'/srv/orca',
|
|
'--worktree-base-path',
|
|
'../worktrees',
|
|
'--state',
|
|
'ready',
|
|
'--method',
|
|
'imported-existing-folder',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('projectHostSetup.update', {
|
|
setupId: 'setup-gpu',
|
|
updates: {
|
|
displayName: 'GPU VM',
|
|
path: path.resolve('/tmp/repo', '/srv/orca'),
|
|
worktreeBasePath: '../worktrees',
|
|
gitUsername: undefined,
|
|
kind: undefined,
|
|
setupState: 'ready',
|
|
setupMethod: 'imported-existing-folder'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('creates independent project host setup metadata through the project-first runtime API', async () => {
|
|
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup_create', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: [],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-gpu',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:gpu',
|
|
repoId: '',
|
|
path: '',
|
|
displayName: 'GPU VM',
|
|
setupState: 'setting-up',
|
|
setupMethod: 'provisioned',
|
|
createdAt: 1,
|
|
updatedAt: 2
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'project',
|
|
'setup-create',
|
|
'--project',
|
|
'github:stablyai/orca',
|
|
'--host',
|
|
'runtime:gpu',
|
|
'--setup-id',
|
|
'setup-gpu',
|
|
'--display-name',
|
|
'GPU VM',
|
|
'--state',
|
|
'setting-up',
|
|
'--method',
|
|
'provisioned',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('projectHostSetup.create', {
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:gpu',
|
|
setupId: 'setup-gpu',
|
|
path: undefined,
|
|
kind: undefined,
|
|
displayName: 'GPU VM',
|
|
worktreeBasePath: undefined,
|
|
gitUsername: undefined,
|
|
setupState: 'setting-up',
|
|
setupMethod: 'provisioned'
|
|
})
|
|
})
|
|
|
|
it('deletes project host setup metadata through the project-first runtime API', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_project_setup_delete', {
|
|
result: {
|
|
project: {
|
|
id: 'github:stablyai/orca',
|
|
displayName: 'Orca',
|
|
badgeColor: '#7c3aed',
|
|
sourceRepoIds: [],
|
|
createdAt: 1,
|
|
updatedAt: 1
|
|
},
|
|
setup: {
|
|
id: 'setup-gpu',
|
|
projectId: 'github:stablyai/orca',
|
|
hostId: 'runtime:gpu',
|
|
repoId: '',
|
|
path: '/srv/orca',
|
|
displayName: 'GPU VM',
|
|
setupState: 'ready',
|
|
setupMethod: 'imported-existing-folder',
|
|
createdAt: 1,
|
|
updatedAt: 2
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['project', 'setup-delete', '--setup', 'setup-gpu', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('projectHostSetup.delete', {
|
|
setupId: 'setup-gpu'
|
|
})
|
|
})
|
|
})
|