fix(terminal): bun-safe exec attach, box self-heal, surfaced errors
The workspace terminal died everywhere: bun's node:http client never
emits the client 'upgrade' event (it surfaces the 101 as a plain
response), so docker-modem's exec.start({hijack:true}) waited forever —
a connected WebSocket with no bytes flowing (prod), or an instant close
when the socket was unreachable (dev). The pre-Phase-1 bridge avoided
dockerode attach for exactly this reason; the real-TTY-resize rewrite
reintroduced it.
- runtime/exec-stream.ts: speak the exec-start HTTP upgrade over a raw
net socket (docker and podman both answer 101 UPGRADED). Works under
bun and node; honors the modem's socketPath/host:port. The stream is
returned paused so the first screen paint can't race the caller's
data handler. Exec create/resize stay on dockerode (plain POSTs).
- user-container.ts: acquireUserBox re-ensures the container on every
acquire instead of trusting a cached "initialized" flag, so a box
removed behind the worker's back (manual rm, another instance's
reaper) is recreated instead of bricking terminals and agent turns
until restart.
- terminal.ts: bridge failures are logged and close the WebSocket with
a clamped human-readable reason instead of a silent catch;
xterm-session.tsx displays that reason instead of a mute
"Session ended". Also roomier terminal line spacing (1.2 -> 1.35).
This commit is contained in:
@@ -22,9 +22,39 @@ describe('box registry', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('serializes concurrent acquire into a single docker run', async () => {
|
||||
test('re-ensures the container on every acquire so an externally removed box self-heals', async () => {
|
||||
const module = await load();
|
||||
const docker = await import('../../src/runtime/docker');
|
||||
const first = await module.acquireUserBox({
|
||||
username: 'zoe',
|
||||
workdir: '/w',
|
||||
containerHome: '/home/zoe',
|
||||
});
|
||||
first.release();
|
||||
// Simulate `docker rm` behind the worker's back: the registry entry still
|
||||
// exists, but acquire must go back to the runtime rather than trust it.
|
||||
const second = await module.acquireUserBox({
|
||||
username: 'zoe',
|
||||
workdir: '/w',
|
||||
containerHome: '/home/zoe',
|
||||
});
|
||||
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(2);
|
||||
expect(second.boxName).toBe('spoon-box-zoe');
|
||||
});
|
||||
|
||||
test('serializes concurrent acquires so ensure calls never overlap', async () => {
|
||||
const module = await load();
|
||||
const docker = await import('../../src/runtime/docker');
|
||||
let inFlight = 0;
|
||||
vi.mocked(docker.ensureUserContainer).mockImplementation(
|
||||
async (args: { username: string }) => {
|
||||
inFlight += 1;
|
||||
expect(inFlight).toBe(1);
|
||||
await Promise.resolve();
|
||||
inFlight -= 1;
|
||||
return `spoon-box-${args.username}`;
|
||||
},
|
||||
);
|
||||
const [a, b] = await Promise.all([
|
||||
module.acquireUserBox({
|
||||
username: 'alice',
|
||||
@@ -37,7 +67,6 @@ describe('box registry', () => {
|
||||
containerHome: '/home/alice',
|
||||
}),
|
||||
]);
|
||||
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
|
||||
expect(a.boxName).toBe('spoon-box-alice');
|
||||
a.release();
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
@@ -83,7 +112,7 @@ describe('box registry', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('ensures an adopted box once before returning acquire handles', async () => {
|
||||
test('acquiring an adopted box ensures it and returns valid handles', async () => {
|
||||
const module = await load();
|
||||
const docker = await import('../../src/runtime/docker');
|
||||
vi.mocked(docker.listWorkspaceContainerNames).mockResolvedValueOnce([
|
||||
@@ -102,7 +131,7 @@ describe('box registry', () => {
|
||||
containerHome: '/home/casey',
|
||||
});
|
||||
|
||||
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
|
||||
expect(docker.ensureUserContainer).toHaveBeenCalled();
|
||||
expect(first.boxName).toBe('spoon-box-casey');
|
||||
expect(second.boxName).toBe('spoon-box-casey');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user