feat(cloud): desktop check for #233 run-action semantics and cloud port block (W9D_ONLY=run-ports2)

- run.nonconcurrent: echo-loop started in B stops only A's echo-loop; web keeps serving in A and B
- run.ports (cloud): ORCA_PORT in the first workspace's terminal and run action, before and after
  Sleep -> wake, with the VM's port registry; whether a second worktree on the same VM is offered
This commit is contained in:
Neil
2026-11-10 04:29:04 -08:00
parent d009ec750c
commit c6484c005b
+203 -2
View File
@@ -1029,7 +1029,7 @@ async function main() {
results.notes.push('test repo not clonable here; used a local repo with the same origin URL')
}
created.tmp = tmp
if (process.env.W9D_ONLY === 'run-actions') {
if (process.env.W9D_ONLY === 'run-actions' || process.env.W9D_ONLY === 'run-ports2') {
writeFileSync(path.join(repoDir, 'orca.yaml'), runActionsYaml(tmp))
execFileSync('git', ['-C', repoDir, 'add', 'orca.yaml'], { stdio: 'ignore' })
execFileSync('git', ['-C', repoDir, '-c', 'user.email=w9@orca.dev', '-c', 'user.name=W9', 'commit', '-qm', 'orca.yaml run actions'], { stdio: 'ignore' })
@@ -2024,7 +2024,7 @@ async function runMenuItem(step, actionName, item) {
const isItem = el.getAttribute('role') === 'menuitem'
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim()
if (!isItem && el.children.length <= 3 && text.startsWith(actionName) && !text.startsWith(`${actionName} `.repeat(2))) under = true
else if (!isItem && under && el.children.length <= 3 && /^(Web|Echo loop)\b/.test(text) && !text.startsWith(actionName)) under = false
else if (!isItem && under && el.children.length <= 3 && /^(Web|Echo loop|Env)\b/.test(text) && !text.startsWith(actionName)) under = false
if (under && isItem && text === item) {
el.setAttribute('data-w9d-run-item', '1')
return 'ok'
@@ -2036,6 +2036,203 @@ async function runMenuItem(step, actionName, item) {
await page.locator('[data-w9d-run-item="1"]').first().click()
}
/** #233: nonconcurrent stops only the same action elsewhere; cloud port block is 3000 and stable. */
async function runPorts2Flow() {
const A = `${wsName}-a`
const B = `${wsName}-b`
const wsA = {}
const wsB = {}
await runStep('run.nonconcurrent', 'echo-loop (nonconcurrent) started in B stops echo-loop in A; concurrent web keeps running and serving in A and B (#233)', async (step) => {
Object.assign(wsA, await createLocalWorktree(step, A))
await waitFor(step, async () => (await runOf(wsA.id, 'web'))?.status === 'running', 90_000)
Object.assign(wsB, await createLocalWorktree(step, B))
await waitFor(step, async () => (await runOf(wsB.id, 'web'))?.status === 'running', 90_000)
const envA = await terminalEnv(step, A)
const envB = await terminalEnv(step, B)
wsA.port = Number(envA?.ORCA_PORT)
wsB.port = Number(envB?.ORCA_PORT)
step.ports = { A: wsA.port, B: wsB.port }
step.before = { webA: (await runOf(wsA.id, 'web'))?.status, webB: (await runOf(wsB.id, 'web'))?.status, serveA: await httpOk(wsA.port), serveB: await httpOk(wsB.port) }
await (await rowOf(A, 'local')).click()
await sleep(800)
await runMenuItem(step, 'Echo loop', 'Run')
const aOn = await waitFor(step, async () => (await runOf(wsA.id, 'echo-loop'))?.status === 'running', 20_000)
await (await rowOf(B, 'local')).click()
await sleep(800)
const t0 = Date.now()
await runMenuItem(step, 'Echo loop', 'Run')
const res = await waitFor(step, async () => {
const a = await runOf(wsA.id, 'echo-loop')
const b = await runOf(wsB.id, 'echo-loop')
return b?.status === 'running' && (!a || a.status === 'stopped' || a.status === 'exited') ? true : null
}, 20_000)
step.timings.bStartToAStoppedMs = res.v ? Date.now() - t0 : null
await sleep(3000)
step.after = {
echoA: (await runOf(wsA.id, 'echo-loop'))?.status ?? 'gone',
echoB: (await runOf(wsB.id, 'echo-loop'))?.status ?? 'gone',
webA: (await runOf(wsA.id, 'web'))?.status ?? 'gone',
webB: (await runOf(wsB.id, 'web'))?.status ?? 'gone',
serveA: await httpOk(wsA.port),
serveB: await httpOk(wsB.port)
}
await runMenu(step)
await shot(step, 'b-menu')
await page.keyboard.press('Escape')
const webKept = step.after.webA === 'running' && step.after.webB === 'running' && step.after.serveA === 200 && step.after.serveB === 200
step.status = aOn.v && res.v && webKept ? 'pass' : 'fail'
step.detail = `ports A ${wsA.port}, B ${wsB.port}; before ${JSON.stringify(step.before)}; echo-loop in A running: ${Boolean(aOn.v)}; started in B -> A's echo-loop stopped in ${step.timings.bStartToAStoppedMs}ms; after ${JSON.stringify(step.after)} (web kept running and serving in both: ${webKept})`
})
// ---- cloud: ORCA_PORT=3000 for the first workspace's terminal and run action, also after Sleep -> wake.
await store(() => window.__store.getState().connectCurrentOrcaProfile())
await sleep(1500)
let cws = null
await runStep('run.ports', 'Cloud: the first workspace terminal and run action see ORCA_PORT=3000, still 3000 after Sleep -> wake; a second worktree on the same VM gets another block (if the UI allows)', async (step) => {
const sub = {}
step.substeps = sub
const w = await createExtraWorkspace(step, `${wsName}-cl`, { blankAgent: true })
const wt = await worktreeByName(w.name)
cws = { ...w, id: wt?.id }
step.sandboxId = w.sandboxId
const repoPath = await store((id) => { const s = window.__store.getState(); const r = s.repos.find((x) => (s.worktreesByRepo?.[x.id] ?? []).some((t) => t.id === id)); return r?.path ?? null }, cws.id)
step.cloudRepoPath = repoPath
// orca.yaml in the VM's primary checkout (committed there, so worktrees made from it have it too).
const yaml = [
'scripts:',
' setup: "echo SETUP P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT > /tmp/w9-setup-env-$(basename $PWD)"',
' run:',
' env:',
' command: "echo RUNENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT; sleep 600"',
' default: true',
''
].join('\n')
const b64 = Buffer.from(yaml).toString('base64')
await openNewTerminalTab()
await waitShellPrompt(90_000)
await focusTerminal()
const rp = repoPath ?? '/vercel/sandbox/project'
await page.keyboard.type(`clear; echo ${b64} | base64 -d > ${rp}/orca.yaml && git -C ${rp} add orca.yaml && git -C ${rp} -c user.email=w9@orca.dev -c user.name=W9 commit -qm w9-orca-yaml && cp ${rp}/orca.yaml ./orca.yaml && echo YAMLOK; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n`)
// The VM's port registry and orcad build, to explain which workspace holds the launcher's block.
await sleep(1500)
await page.keyboard.type('clear; echo W9REG; cat /vercel/.orcad-data/workspace-port-blocks.json; echo; echo W9REGEND\n')
const reg = await waitFor(step, async () => (await visibleTerminals()).join('\n').match(/W9REG\s*\n([\s\S]*?)\n\s*W9REGEND/)?.[1]?.replace(/\s+/g, ' ') ?? null, 15_000)
step.vmRegistry = reg.v
await focusTerminal()
await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n')
const env1 = await waitFor(step, async () => {
const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)\s*$/m)
return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null
}, 30_000)
step.terminalEnv = env1.v
sub.terminal3000 = env1.v?.ORCA_PORT === '3000' && env1.v?.ORCA_PORT_COUNT === '10' && env1.v?.CONDUCTOR_PORT === '3000'
// Run action env.
await (await rowOf(A, 'local')).click()
await sleep(800)
await (await rowOf(cws.name)).click()
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
await waitFor(step, async () => /Env/.test(await runButton().innerText().catch(() => '')), 30_000)
const readRunEnv = async () => {
const r = await waitFor(step, async () => {
const m = (await visibleTerminals()).join('\n').match(/RUNENV P=(\d*) C=(\d*) K=(\d*)/)
return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null
}, 30_000)
return r.v
}
await runButton().getByRole('button', { name: /^Run Env/ }).click()
await waitFor(step, async () => (await runOf(cws.id, 'env'))?.status === 'running', 60_000)
step.runActionEnv = await readRunEnv()
sub.runAction3000 = step.runActionEnv?.ORCA_PORT === '3000'
await shot(step, 'run-env')
step.setupEnvFirst = 'not testable: the first workspace is created from the GitHub test repo, which has no orca.yaml, so no setup runs for it'
// Sleep -> wake, then terminal + run action again.
await openRowMenu(cws.name)
await page.getByRole('menuitem', { name: /^(Suspend VM|Sleep)$/ }).first().click()
await page.getByRole('dialog').getByRole('button', { name: /^(Sleep|Suspend)/ }).click({ timeout: 1500 }).catch(() => {})
const asleep = await waitSandboxStatus(cws.sandboxId, ['suspended', 'sleeping', 'stopped'], 180_000)
step.timings.sleepMs = asleep.ms
await sleep(2000)
const tabBeforeSleep = (await runOf(cws.id, 'env'))?.tabId
const t0 = Date.now()
await (await rowOf(cws.name)).click()
// Back = relaunched in a new tab after the wake.
const back = await waitFor(step, async () => { const r = await runOf(cws.id, 'env'); return r?.status === 'running' && r.tabId !== tabBeforeSleep }, 180_000)
step.timings.wakeClickToRunActionBackMs = back.v ? Date.now() - t0 : null
await page.getByText('Run: Env', { exact: true }).first().click().catch(() => {})
await sleep(1000)
step.runActionEnvAfterWake = await readRunEnv()
await openNewTerminalTab()
await waitShellPrompt(90_000)
await focusTerminal()
await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n')
const env2 = await waitFor(step, async () => {
const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)\s*$/m)
return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null
}, 30_000)
step.terminalEnvAfterWake = env2.v
sub.stillAfterWake = env2.v?.ORCA_PORT === '3000' && step.runActionEnvAfterWake?.ORCA_PORT === '3000'
await shot(step, 'after-wake')
// A second worktree on the same VM: offered by the composer?
const d = await openComposer(step, 'second-worktree-composer')
await d.getByRole('combobox', { name: 'Run on' }).click()
const targets = page.getByRole('listbox', { name: 'Run targets' })
await targets.waitFor()
step.runTargetsWithCloud = (await targets.getByRole('option').allInnerTexts()).map((t) => t.replace(/\s+/g, ' ').trim())
await shot(step, 'run-targets')
// Only a target for the existing VM counts (not "Orca Cloud", which starts a new sandbox).
const sameVm = targets.getByRole('option').filter({ hasText: new RegExp(`${cws.name}|sandbox ·|environment`, 'i') }).filter({ hasNotText: /Orca Cloud|Local Mac|Connect/ }).first()
if (await sameVm.isVisible().catch(() => false)) {
step.secondWorktree = 'offered'
await sameVm.click()
await selectBlankAgent(d)
const n2 = `${wsName}-cl2`
await fillName(d, n2)
await d.getByRole('button', { name: /^Create/ }).click()
const w2 = await waitFor(step, () => worktreeByName(n2), 120_000)
if (w2.v) {
await openNewTerminalTab()
await waitShellPrompt(90_000)
await focusTerminal()
await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT; cat /tmp/w9-setup-env-* 2>/dev/null\n')
const env3 = await waitFor(step, async () => {
const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)/)
return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null
}, 30_000)
step.secondWorktreeEnv = env3.v
step.setupEnvLines = ((await visibleTerminals()).join('\n').match(/SETUP P=\S* C=\S* K=\S*/g) ?? [])
sub.secondWorktreeOtherBlock = Boolean(env3.v?.ORCA_PORT) && env3.v.ORCA_PORT !== '3000'
} else {
step.secondWorktree = 'offered, but no worktree appeared within 120s'
}
} else {
step.secondWorktree = 'not testable: the composer offers no target for an existing cloud VM (only a new Orca Cloud sandbox)'
await page.keyboard.press('Escape').catch(() => {})
await page.keyboard.press('Escape').catch(() => {})
}
step.status = Object.values(sub).every(Boolean) ? 'pass' : 'fail'
step.detail = `cloud first workspace: terminal ${JSON.stringify(step.terminalEnv)}, run action ${JSON.stringify(step.runActionEnv)}; Sleep -> ${asleep.status} ${asleep.ms}ms; click to wake -> run action back ${step.timings.wakeClickToRunActionBackMs}ms, then terminal ${JSON.stringify(step.terminalEnvAfterWake)}, run action ${JSON.stringify(step.runActionEnvAfterWake)}; setup: ${step.setupEnvFirst}; second worktree on the same VM: ${step.secondWorktree}${step.secondWorktreeEnv ? ` -> ${JSON.stringify(step.secondWorktreeEnv)}, setup env ${JSON.stringify(step.setupEnvLines)}` : ''}; run targets ${JSON.stringify(step.runTargetsWithCloud)}; ${JSON.stringify(sub)}`
})
if (cws) {
await runStep('life.destroy', 'Delete the cloud workspace', async (step) => {
step.sandboxId = cws.sandboxId
await page.keyboard.press('Escape').catch(() => {})
const o = await uiDelete(step, cws.name, 'delete', 120_000)
const w = await waitSandboxStatus(cws.sandboxId, ['destroyed'], 120_000)
step.status = o.kind === 'removed' && w.status === 'destroyed' ? 'pass' : 'fail'
step.detail = `UI Delete -> ${o.kind} in ${o.ms}ms; sandbox -> ${w.status}`
})
}
for (const n of [A, B]) {
if (await rowShown(n, 'local')) {
await openRowMenu(n, 'local').catch(() => {})
await page.getByRole('menuitem', { name: /^Delete/ }).click().catch(() => {})
await page.getByRole('dialog').getByRole('button', { name: /^(Force )?Delete( Workspace)?$/ }).first().click().catch(() => {})
await waitRowGone(n, 'local', 30_000).catch(() => {})
}
}
}
async function runActionsFlow() {
const A = `${wsName}-a`
const B = `${wsName}-b`
@@ -2309,6 +2506,10 @@ async function runActionsFlow() {
* and a normal Delete.
*/
async function targetedFlow() {
if (process.env.W9D_ONLY === 'run-ports2') {
await runPorts2Flow()
return
}
if (process.env.W9D_ONLY === 'run-actions') {
await runActionsFlow()
return