Troubleshooting test adoption
TL;DR Start with the URL, namespace, and resolved client package. Smocket keeps Socket.IO-compatible errors generic where Socket.IO does, while its origin registry and package substitution have explicit signals and cleanup rules.
The runtime examples below use one shared setup unless an entry says otherwise.
import { Server } from 'smocket';
import { connect } from 'smocket-client';
const URL = 'http://localhost:3000';
An ack is the response callback attached to an event. An origin registry is Smocket's in-process lookup from a protocol, host, and port to a server.
1. A malformed URL
- Reproduce: call
connect('http://[')or use a relative URL in Node without alocation.origin. - Signal:
connect()throws a synchronous nativeTypeError. Its message belongs to the JavaScript URL implementation and is not a stable Smocket contract. - Cause and action: the URL cannot become an absolute URL. Pass a valid absolute URL in Node, or provide the browser origin that a relative URL needs.
- Classification: Smocket exposes the native URL parser at its boundary. A real Socket.IO client need not fail at lookup construction, so do not assert parity or exact text here. See decision 0003.
2. A missing or mismatched origin
Minimal reproduction:
new Server('http://localhost:3000');
const client = connect('http://localhost:3001');
client.on('connect_error', console.error);
- Signal: on the next tick the client emits an
Errorwhose message containsno server registered for http://localhost:3001. Smocket also logs a line beginning[smocket] connect_error, does not throw fromconnect(), and does not retry. - Cause and action: protocol, hostname, or port differs, or the server was not created yet. Make both URLs name the same normalized origin and construct the server first.
- Classification: this immediate one-shot failure and console diagnostic are Smocket-specific. Real Socket.IO uses network retries. The contract is pinned in connect-url.test.ts and decision 0005.
3. An invalid namespace
Minimal reproduction:
new Server(URL);
connect(`${URL}/private`).on('connect_error', console.error);
- Signal: the client receives
Error('Invalid namespace'), stays disconnected, and gets no id. Smocket writes no extra console diagnostic. - Cause and action: the static namespace does not exist.
Call
io.of('/private')before connecting, and check the URL path spelling. - Classification: the signal is Socket.IO-compatible and intentionally generic. It is pinned in namespace.test.ts.
4. Connection middleware rejection
Minimal reproduction:
const io = new Server(URL);
io.use((_socket, next) => {
const error = Object.assign(new Error('unauthorized'), { data: { code: 401 } });
next(error);
});
connect(URL).on('connect_error', console.error);
- Signal: the client receives
connect_errorwith messageunauthorizedanderror.dataequal to{ code: 401 }. It never reachesconnection. - Cause and action: middleware called
next(error). Registerconnect_errorbefore opening the client, then inspect its auth input and the middleware-owneddatavalue. - Classification: message and data propagation are Socket.IO-compatible. The message comes from application middleware, not from a richer Smocket diagnostic. See middleware.test.ts.
5. Reserved event emission
Minimal reproduction:
new Server(URL);
const client = connect(URL);
await new Promise((done) => client.once('connect', done));
client.emit('disconnect');
- Signal: ordinary, timeout, and volatile
emit()calls throwError('"disconnect" is a reserved event name').emitWithAck()returns a rejected promise with the same error. - Cause and action: lifecycle names cannot be application events. Rename the event and
listen for
disconnectinstead of emitting it. - Classification: the error is Socket.IO-compatible on client, socket, namespace, and broadcast surfaces. See reserved-events.test.ts.
6. Acknowledgement timeout
Minimal reproduction:
const io = new Server(URL);
io.on('connection', (socket) => socket.on('save', () => {}));
const client = connect(URL);
client.timeout(20).emit('save', 'draft', (error) => {
if (error) throw error;
});
- Signal: when the peer does not call the trailing ack, the callback receives one
Error('operation has timed out').timeout(ms).emitWithAck()rejects with that error, and a late ack is ignored. - Cause and action: the handler did not ack, took longer than the chosen duration, or never ran because setup targeted the wrong client. Ack every intended path, choose a deliberate duration, and await the result or drive fake timers before teardown.
- Classification: the generic error and one-shot settlement are Socket.IO-compatible. Smocket intentionally adds no event-specific text. See timeout.test.ts.
7. Ordinary and volatile emits before connect
Minimal reproduction:
new Server(URL);
const client = connect(URL);
client.emit('ordinary', 'queued');
client.volatile.emit('volatile', 'dropped');
- Signal: no error is raised. The ordinary event is delivered after
connect, while the volatile event is absent when a later ordinary marker arrives. - Cause and action: the client is in the pre-connect window. Await
connectbefore a volatile event that must arrive, or use an ordinary event when buffering is intended. - Classification: this buffering and drop split matches Socket.IO 4.7 and 4.8. See volatile.test.ts and decision 0016.
8. Emits after disconnect
Minimal reproduction:
const io = new Server(URL);
io.on('connection', (socket) => {
socket.on('save', (_payload, ack) => ack('saved'));
});
const client = connect(URL);
await new Promise((done) => client.once('connect', done));
const disconnected = new Promise((done) => client.once('disconnect', done));
client.disconnect();
await disconnected;
const pending = client.emitWithAck('save', { id: 1 });
client.connect();
await pending;
- Signal: an ordinary emit or new
emitWithAck()made after disconnect buffers until a manualconnect(). The promise stays pending and then settles after the new peer acks. An ack already in flight when disconnect begins rejects instead. - Cause and action: application code reused a disconnected client. Stop emitting after
disposal, or reconnect explicitly and wait for
connectbefore expecting delivery. - Classification: buffering and in-flight rejection match Socket.IO. Automatic reconnection timing remains outside Smocket's scope. See ack.test.ts and decision 0012.
9. Connecting after server close
Minimal reproduction:
const io = new Server(URL);
await io.close();
connect(URL).on('connect_error', console.error);
- Signal: a later free lookup follows the missing-origin path from section 2 because
close()unregisters the server. An attempt already crossing the close boundary emitsconnect_errorwithserver is closedand never reachesconnection. - Cause and action: the test reused a closed server or began teardown before admission
completed. Construct a fresh
Serverfor the next test and await connection work before closing the current one. - Classification: rejecting an in-flight connection is Socket.IO-compatible. Registry removal and the later missing-origin diagnostic are Smocket-specific. See server-close.test.ts and decision 0020.
10. An incorrect Vitest or Jest alias
import * as mappedClient from 'socket.io-client';
import * as selectedClient from 'smocket-client';
expect(mappedClient.io).toBe(selectedClient.io);
expect(mappedClient.connect).toBe(selectedClient.connect);
- Reproduce: omit the alias, misspell either package, or load a config that does not
contain the documented mapping. In Jest, make the same identity comparison with
require(). - Signal: package loading fails, the identity assertion fails, or the later connection behaves like real Socket.IO. Runner error wording is not a Smocket contract.
- Cause and action: the config was not selected or did not map the exact
socket.io-clientspecifier tosmocket-client. Fixresolve.alias,vi.mock, ormoduleNameMapper, then keep the identity assertion while diagnosing. - Classification: this belongs to the test runner. Smocket cannot diagnose a module that never resolved to it. The executable forms live in consumers/test-adoption.
11. Accidentally running real Socket.IO
- Reproduce: import the application from
socket.io-clientwithout activating its test alias, while the test creates an in-memoryServerfromsmocket. - Signal: the client resolves from the real
socket.io-clientpackage, attempts a network connection, and may retry or leave a transport handle. There is no Smocket[smocket] connect_errorline because Smocket never received the lookup. - Cause and action: the runner used a production config, or the alias applied to a
different project or file. Run the identity assertion in section 10 and print the
installed paths with
require.resolve('smocket-client')orimport.meta.resolve('smocket-client')outside the transformed application. - Classification: this is package resolution outside the Smocket runtime. Do not add a runtime detector for it.
12. Teardown, replacement, timers, and open handles
- Reproduce: construct two servers for
URL, close the older one, or end a test while an acknowledgement timeout is armed. - Correct cleanup: retain the active client and server references and settle them in teardown.
afterEach(async () => {
client?.disconnect();
await io.close();
});
- Signal: the newest
Server(URL)owns that origin, so a second construction silently replaces the first. Closing the old server does not unregister the replacement. A server ack timeout armed beforeclose()still fires afterward withError('operation has timed out')and can keep a test process open until it settles. - Cause and action: setup reused an origin, teardown closed the wrong instance, or a
timer remained armed. Disconnect every retained client, await the current server's
close(), and settle or drive every ack timer before the test ends. - Classification: origin replacement is Smocket-specific. Disconnect order and the surviving ack timer match Socket.IO. See connect-url.test.ts, server-close.test.ts, and decision 0020.
13. ESM and CommonJS resolution
- Reproduce: load
Serverthrough ESM and the facade through CommonJS in one process, or map a default or callable client import directly to the rootsmocketpackage. - Signal: mixed formats can produce
no server registeredfor an apparently identical URL because two root module instances own separate registries. A wrong root mapping can instead fail at load time because the root has no client default or callable export. - Cause and action: the server and facade crossed module formats, or the client alias
points to the server package. Use
smocket-clientfor client imports and keep both packages in ESM or both in CommonJS. Inspect the conditional exports in the client manifest. - Classification: dual-format loading is package behavior. Mixed-format registry sharing is explicitly unsupported by decision 0023. The clean consumer runs ESM, callable CommonJS, Node16, and bundler cases separately.
14. Event-map type mismatch
- Reproduce: compile a server socket listener against the server-to-client map.
interface ClientToServer {
join(room: string): void;
}
interface ServerToClient {
ready(message: string): void;
}
const typed = new Server<ClientToServer, ServerToClient>(URL);
typed.on('connection', (socket) => socket.on('ready', () => {}));
- Signal: TypeScript rejects
readyon the server socket because server sockets listen to client-to-server events. Compiler wording can change and is not a runtime signal. - Cause and action: event maps were reversed or attached to the wrong socket side. A
server socket listens to the first map and emits the second. A client
Socketfromsmocket-clientlistens to the server map and emits the client map. - Classification: the direction matches Socket.IO's type contract and has no runtime diagnostic. See the installed invalid fixture in consumers/test-adoption/types/invalid and decision 0021.