Files
orca/tests/e2e/helpers/remote-pane-launch-transcript.ts
T
Neil 0ea900aba1 merge: origin/main (STA-3077 SSH reconnect) into the daemon-hold branch
Three conflicts, all in the ownership-critical files, and the two changes turn out
to be making the same argument from opposite ends.

STA-3077 made hasPty three-valued: null now means "this provider cannot answer",
where before the only answers were yes and no. Its reasoning — an owner that cannot
answer must stay unknown rather than become a liveness proof — is the same rule this
branch applies to daemon occupancy. Both sides kept: the new signature, and the
owner gate that sits above it.

The gate needed no change, because `hasPty?.(id) !== true` already reads a null as
"not proven" and refuses. But it needed a test. Mutating `!== true` to `=== false`
left all 1525 daemon tests green: every double in the suite answers false, which
makes the two predicates indistinguishable, while in production the difference is
now the whole point — a null read as ownership lets the in-process fallback answer
for a daemon-owned session, closing the pane while the agent runs on as an orphan.
Two tests added at that boundary; the mutation is caught.

pty:signal keeps both additions in the order that makes sense: their superseded-id
fence first, then the routing call whose rejection is already handled.
2026-08-13 02:05:28 -07:00

66 lines
2.8 KiB
TypeScript

/**
* A stand-in for an agent's transcript, recorded by the host rather than by the app.
*
* The STA-3077 report's second failure is "a coding agent was resumed twice into
* one transcript". Driving a real coding agent inside the Docker OpenSSH fixture
* is not possible — no agent binary ships in the image and no provider session
* exists to resume — so the observable is moved to the thing that actually goes
* wrong: a second shell launching for a pane that already has one. Every shell
* the relay starts for a pane appends one line here, keyed by pane, so "resumed
* twice" reads as two lines for one pane key and needs no app-side reporting to
* be believed.
*
* The hook is installed before the first pane exists, and each test asserts the
* first launch was recorded before asserting no second one was — a hook that
* silently failed to fire would otherwise make every clause pass vacuously.
*/
import {
execDockerSshRelayTargetCommand,
type DockerSshRelayTarget
} from './docker-ssh-relay-target'
const TRANSCRIPT_PATH = '/tmp/orca-pane-launch-transcript.log'
// Prepended, not appended: Debian's stock /root/.bashrc returns early for a
// non-interactive shell, and a hook placed after that guard would only fire
// sometimes. `ORCA_PANE_KEY` is injected by the pane launch path alone, so a
// `docker exec` probe shell adds nothing.
const INSTALL_TRANSCRIPT_HOOK_COMMAND = `
set -e
hook=/root/.orca-pane-launch-transcript.sh
cat > "$hook" <<'HOOK'
if [ -n "$ORCA_PANE_KEY" ]; then
printf '%s\\t%s\\n' "$ORCA_PANE_KEY" "$$" >> ${TRANSCRIPT_PATH}
fi
HOOK
touch /root/.bashrc ${TRANSCRIPT_PATH}
if ! grep -q orca-pane-launch-transcript /root/.bashrc; then
printf '. %s\\n' "$hook" | cat - /root/.bashrc > /root/.bashrc.next
mv /root/.bashrc.next /root/.bashrc
fi
grep -c orca-pane-launch-transcript /root/.bashrc
`
export function installRemotePaneLaunchTranscript(target: DockerSshRelayTarget): void {
const installed = Number(
execDockerSshRelayTargetCommand(target, INSTALL_TRANSCRIPT_HOOK_COMMAND).split('\n').pop()
)
if (installed !== 1) {
throw new Error(`The pane launch transcript hook was not installed exactly once: ${installed}`)
}
}
/** Every shell launch the host recorded, oldest first, as `paneKey\tpid`. */
export function readRemotePaneLaunchTranscript(target: DockerSshRelayTarget): string[] {
return execDockerSshRelayTargetCommand(target, `cat ${TRANSCRIPT_PATH} 2>/dev/null || true`)
.split('\n')
.filter((line) => line.includes('\t'))
}
/** The pids the host launched a shell for under one pane key. */
export function readRemotePaneLaunchPids(target: DockerSshRelayTarget, paneKey: string): number[] {
return readRemotePaneLaunchTranscript(target)
.filter((line) => line.split('\t')[0] === paneKey)
.map((line) => Number(line.split('\t')[1]))
}