diff --git a/packages/core/src/format.ts b/packages/core/src/format.ts index 8fbf719..c98bf6a 100644 --- a/packages/core/src/format.ts +++ b/packages/core/src/format.ts @@ -32,7 +32,10 @@ export const sliceUtf8 = (s: string, maxBytes: number): string => { } // Serialize `value` to JSON and, if it exceeds `maxBytes` (UTF-8), return a -// truncated marker object whose own serialization stays within `maxBytes`. +// truncated marker object. A fixed 256-byte margin is reserved for the marker +// metadata, so the marker's own serialization stays within `maxBytes` for any +// budget at or above a few hundred bytes (esmole uses 100 KB). For +// pathologically tiny budgets the marker may exceed `maxBytes`. export const truncateJsonBudget = ( value: unknown, maxBytes: number diff --git a/packages/core/src/net/tunnel.ts b/packages/core/src/net/tunnel.ts index de966ea..4c02d55 100644 --- a/packages/core/src/net/tunnel.ts +++ b/packages/core/src/net/tunnel.ts @@ -50,7 +50,8 @@ export const buildSshAuth = ( export const openTunnel = async ( ssh: SshConfig, dstHost: string, - dstPort: number + dstPort: number, + logLabel: string = 'ssh-tunnel' ): Promise => { const client = new Client() @@ -67,7 +68,7 @@ export const openTunnel = async ( // After 'ready' resolved, later ssh errors must not crash the process. client.on('error', (error) => { - console.error('ssh-tunnel: ssh tunnel error: ' + error.message) + console.error(logLabel + ': ssh tunnel error: ' + error.message) }) let closed = false @@ -140,7 +141,7 @@ export const openTunnel = async ( // without a persistent one, a later listener error is swallowed and the // tunnel silently serves a dead socket. Mark it closed instead. server.on('error', (error) => { - console.error('ssh-tunnel: tunnel listener error: ' + error.message) + console.error(logLabel + ': tunnel listener error: ' + error.message) markClosed() }) diff --git a/packages/core/test/unit/net/tunnel.test.ts b/packages/core/test/unit/net/tunnel.test.ts index e9e8f93..652f5d1 100644 --- a/packages/core/test/unit/net/tunnel.test.ts +++ b/packages/core/test/unit/net/tunnel.test.ts @@ -248,11 +248,34 @@ describe('openTunnel', () => { created.push(server) return server }) + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const tunnel = await openTunnel(sshConfig({ password: 'pw' }), 'db-host', 5432) // A listener error after listen() resolved must not be swallowed: it // marks the tunnel closed so the manager can rebuild it. created[0].emit('error', new Error('EADDRNOTAVAIL')) expect(tunnel.isClosed()).toBe(true) + // Default log label brands the diagnostic with 'ssh-tunnel:'. + expect(errSpy).toHaveBeenCalledWith('ssh-tunnel: tunnel listener error: EADDRNOTAVAIL') + errSpy.mockRestore() + spy.mockRestore() + await tunnel.close() + }) + + it('uses a custom logLabel prefix on diagnostics', async () => { + const created: net.Server[] = [] + const realCreateServer = net.createServer.bind(net) + const spy = vi.spyOn(net, 'createServer').mockImplementation((handler) => { + const server = realCreateServer(handler as (socket: net.Socket) => void) + created.push(server) + return server + }) + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const tunnel = await openTunnel(sshConfig({ password: 'pw' }), 'db-host', 5432, 'dbmole') + created[0].emit('error', new Error('EADDRNOTAVAIL')) + expect(tunnel.isClosed()).toBe(true) + // The injected label replaces the default prefix token. + expect(errSpy).toHaveBeenCalledWith('dbmole: tunnel listener error: EADDRNOTAVAIL') + errSpy.mockRestore() spy.mockRestore() await tunnel.close() }) diff --git a/packages/dbmole-mcp/src/db/manager.ts b/packages/dbmole-mcp/src/db/manager.ts index 2e47800..3657d51 100644 --- a/packages/dbmole-mcp/src/db/manager.ts +++ b/packages/dbmole-mcp/src/db/manager.ts @@ -1,4 +1,9 @@ -import { createManager as coreCreate, type ManagedConnection, type Manager } from '@dbmole/core' +import { + createManager as coreCreate, + type ManagedConnection, + type Manager, + openTunnel +} from '@dbmole/core' import type { DbRegistry } from '../config/registry.js' import type { ConnectionConfig } from '../schema.js' import { createBackend, resolvePort } from '../sqlBackend.js' @@ -8,4 +13,8 @@ export type DbManager = Manager export type DbManaged = ManagedConnection export const createManager = (registry: DbRegistry): DbManager => - coreCreate(registry, { createBackend, resolvePort }) + coreCreate(registry, { + createBackend, + resolvePort, + createTunnel: (ssh, host, port) => openTunnel(ssh, host, port, 'dbmole') + })