Files
craftdock/test/services/stdinService.test.js
T
Lucas RayanandClaude Sonnet 5.5 640e52c0c9 Initial commit: CraftDock as of 2026-11-01
The previous GitHub history was lost with the account (the local .git was missing
its refs and most objects), so this repository starts from the existing code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 22:44:39 -03:00

114 lines
4.1 KiB
JavaScript

import { describe, it, expect, vi } from 'vitest';
import { PassThrough } from 'node:stream';
import { createStdinService } from '../../src/services/stdinService.js';
describe('stdinService', () => {
it('writes command with newline and captures output window', async () => {
const stream = new PassThrough();
const dockerService = { attach: vi.fn().mockResolvedValue(stream) };
const svc = createStdinService(dockerService, { windowMs: 20 });
const p = svc.send('list');
// simulate server echoing output
setTimeout(() => stream.write('There are 2 players online\n'), 5);
const out = await p;
expect(out).toContain('2 players online');
});
it('appends a newline to the written command', async () => {
const stream = new PassThrough();
const written = [];
stream.write = (chunk) => { written.push(chunk.toString()); return true; };
const dockerService = { attach: vi.fn().mockResolvedValue(stream) };
const svc = createStdinService(dockerService, { windowMs: 5 });
await svc.send('op steve');
expect(written[0]).toBe('op steve\n');
});
it('serializes concurrent sends (no interleaving)', async () => {
const stream = new PassThrough();
const order = [];
// Track write order to prove serialization
const originalWrite = stream.write.bind(stream);
stream.write = (...args) => {
order.push(args[0].toString().trim());
return originalWrite(...args);
};
const dockerService = { attach: vi.fn().mockResolvedValue(stream) };
const svc = createStdinService(dockerService, { windowMs: 40 });
// Fire 3 commands concurrently — they must NOT overlap
const p1 = svc.send('cmd1');
const p2 = svc.send('cmd2');
const p3 = svc.send('cmd3');
// Simulate server responses aimed at the MIDDLE of each serialized capture
// window ([0,40) / [40,80) / [80,120)ms) so timer jitter can't flake this.
setTimeout(() => stream.push('reply1\n'), 15);
setTimeout(() => stream.push('reply2\n'), 55);
setTimeout(() => stream.push('reply3\n'), 95);
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
// Commands must have been written in order (serialized)
expect(order).toEqual(['cmd1', 'cmd2', 'cmd3']);
// Each response captured in its own window
expect(r1).toContain('reply1');
expect(r2).toContain('reply2');
expect(r3).toContain('reply3');
});
it('re-attaches after the stream ends (e.g. container stop/start on world import)', async () => {
const stream1 = new PassThrough();
const stream2 = new PassThrough();
let n = 0;
const dockerService = {
attach: vi.fn().mockImplementation(async () => (++n === 1 ? stream1 : stream2)),
};
const svc = createStdinService(dockerService, { windowMs: 5 });
await svc.send('a');
expect(dockerService.attach).toHaveBeenCalledTimes(1);
// Container stopped: the attach stream ends. A stale, non-destroyed stream
// must NOT be reused — the next command has to re-attach.
stream1.emit('end');
await new Promise((r) => setTimeout(r, 1));
setTimeout(() => stream2.push('ok\n'), 2);
const out = await svc.send('b');
expect(dockerService.attach).toHaveBeenCalledTimes(2);
expect(out).toContain('ok');
});
it('queue continues even if a command rejects (stream error)', async () => {
let callCount = 0;
const dockerService = {
attach: vi.fn().mockImplementation(async () => {
callCount++;
if (callCount === 1) {
// First call: return a stream that immediately errors on write
const bad = new PassThrough();
bad.write = () => { throw new Error('broken pipe'); };
return bad;
}
// Second call: return a healthy stream
const good = new PassThrough();
setTimeout(() => good.push('ok\n'), 3);
return good;
}),
};
const svc = createStdinService(dockerService, { windowMs: 10 });
// First send should reject
await expect(svc.send('bad')).rejects.toThrow('broken pipe');
// The service must recover (re-attach) and the queue must not be stuck
const result = await svc.send('good');
expect(result).toContain('ok');
});
});