mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
* feat(orcad): a managed orcad stops after 15 idle minutes and starts again on the next connect A client-launched orcad now exits, like the relay, once no client, terminal, working agent, staged migration or activation fence has been seen for 15 minutes. The exit is the normal graceful shutdown, which leaves the terminal daemon running; the daemon retires only if it proves itself empty. A record in the data root tells the next start (and its readiness health) that the stop was an idle one rather than a crash. On connect and after host resume, a fresh tunnel whose server does not answer starts the activated slot under the activation fence, but only on a proven exit, so a stopped server reads as not running rather than a failure. * fix(orcad): keep orcad-entry under max-lines; idle e2e connects without a relay repo * fix(orcad): deploy and rollback launches carry the managed idle-exit fence The candidate launch in activation and the rollback launch built their own launch spec without the activation root, so a freshly deployed orcad never enabled idle exit; only the wake path did. The field is now required on every launch spec, so the type system covers each launch site. * feat(ssh): start a stopped managed orcad on connect, on restore and after resume Once orcad stopped (idle, kill or host reboot), a connect still resolved managed over a forward to a dead port and every call failed. Every connect now checks the server behind its tunnel, as does a call through a restored environment; a server proven stopped is started from its activated slot under the activation fence, adopting a surviving daemon and its terminals. The status line shows the start, and a start that fails keeps the host managed with the reason and orcad.log's tail, never as a terminal verdict. * fix(ssh): reuse a serving verdict only on the same SSH transport, for 5s A reconnect right after a reboot was answered from the previous transport's cached verdict, so the stopped server was never started. * fix(ssh): key the serving verdict on the tunnel's remote port too * test(ssh): a stopped server starts before the update counts its terminals * feat(ssh): check serving at the bound port, and follow a restarted orcad to a new one The serving check uses the port the tunnel forwards to (the one orcad bound). A restart that binds a different port drops the forward and rebuilds it at the new port, within the same ensure or on the explicit connect check. The tunnel manager class moves to its own file to stay under max-lines. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
104 lines
3.6 KiB
JavaScript
104 lines
3.6 KiB
JavaScript
import { DEDICATED_E2E_SPECS } from './ci-e2e-job-selection.mjs'
|
|
import { readFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { dirname, join } from 'node:path'
|
|
import { expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
import { runProcess } from '../../src/shared/child-process/run-process'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const root = join(import.meta.dirname, '../..')
|
|
const workflow = parse(readFileSync(join(root, '.github/workflows/e2e.yml'), 'utf8'))
|
|
const job = workflow.jobs['ssh-docker-watcher-isolation']
|
|
const readRunner = (name) => readFileSync(join(root, `config/scripts/${name}.mjs`), 'utf8')
|
|
const runnerSpecs = (name) =>
|
|
[...readRunner(name).matchAll(/^ '(tests\/e2e\/[^']+\.spec\.ts)',/gm)].map((match) => match[1])
|
|
const runners = [
|
|
'run-ssh-docker-e2e',
|
|
'run-ssh-docker-watcher-isolation-e2e',
|
|
'run-ssh-docker-terminal-parking-e2e'
|
|
]
|
|
|
|
it('gives every removed SSH spec a dedicated owner even for test-only edits', () => {
|
|
const changedRun = workflow.jobs['changed-e2e'].steps.find(
|
|
(step) => step.name === 'Run changed E2E specs'
|
|
).run
|
|
expect(changedRun).toContain('node config/scripts/ci-e2e-job-selection.mjs')
|
|
const excluded = DEDICATED_E2E_SPECS
|
|
const owned = runners.flatMap(runnerSpecs)
|
|
expect(owned.length).toBeGreaterThan(25)
|
|
expect(new Set(owned).size).toBe(owned.length)
|
|
for (const spec of owned) {
|
|
expect(excluded, spec).toContain(spec)
|
|
expect(job.if, spec).toContain(`contains(inputs.test_files, '${spec}')`)
|
|
}
|
|
expect(excluded.filter((spec) => !owned.includes(spec)).sort()).toEqual([
|
|
'tests/e2e/orcad-serve-mode-switch.spec.ts',
|
|
'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts',
|
|
'tests/e2e/ssh-localhost.spec.ts',
|
|
'tests/e2e/ssh-orcad-auto-convert.spec.ts',
|
|
'tests/e2e/ssh-orcad-idle-exit.spec.ts',
|
|
'tests/e2e/terminal-ibus-hangul-native.spec.ts',
|
|
'tests/e2e/windows-missing-appdata-startup.spec.ts'
|
|
])
|
|
})
|
|
|
|
it('native Playwright shards preserve every SSH test and project exactly once', async () => {
|
|
const cli = join(dirname(require.resolve('playwright/package.json')), 'cli.js')
|
|
const env = {
|
|
...process.env,
|
|
ORCA_BACKGROUND_LAUNCH: '1',
|
|
ORCA_E2E_SSH_DOCKER: '1',
|
|
ORCA_E2E_LOCAL_SSH_BROWSER: '1',
|
|
ORCA_E2E_SSH_CLIENT_HOSTED_BROWSER: '1',
|
|
ORCA_E2E_WEB_CLIENT: '1'
|
|
}
|
|
async function discover(extra = []) {
|
|
const result = await runProcess({
|
|
program: process.execPath,
|
|
cwd: root,
|
|
args: [
|
|
cli,
|
|
'test',
|
|
...runnerSpecs(runners[0]),
|
|
'--config',
|
|
'tests/playwright.config.ts',
|
|
'--project=electron-headless',
|
|
'--project=electron-headful',
|
|
'--workers=1',
|
|
'--list',
|
|
'--reporter=json',
|
|
...extra
|
|
],
|
|
env,
|
|
timeoutMs: 30000
|
|
})
|
|
expect(result.code, result.stderr).toBe(0)
|
|
const report = JSON.parse(result.stdout)
|
|
expect(report.errors).toEqual([])
|
|
const ids = []
|
|
function visit(suite) {
|
|
for (const spec of suite.specs ?? []) {
|
|
for (const test of spec.tests) {
|
|
ids.push(`${spec.id}:${test.projectName}`)
|
|
}
|
|
}
|
|
for (const child of suite.suites ?? []) {
|
|
visit(child)
|
|
}
|
|
}
|
|
visit(report)
|
|
return ids
|
|
}
|
|
const full = await discover()
|
|
const sharded = []
|
|
for (const index of job.strategy.matrix.shard) {
|
|
const selected = await discover([`--shard=${index}/4`])
|
|
expect(selected.length).toBeGreaterThan(0)
|
|
sharded.push(...selected)
|
|
}
|
|
expect(full.length).toBeGreaterThanOrEqual(40)
|
|
expect(new Set(sharded).size).toBe(sharded.length)
|
|
expect(sharded.sort()).toEqual(full.sort())
|
|
}, 90000)
|