77 * process, through `process.chdir` (the CLI path) and through `Instance.provide` (the server path).
88 */
99
10- import { describe , test , expect , beforeEach , afterEach , afterAll , mock } from "bun:test"
10+ import { describe , test , expect , beforeEach , afterEach , afterAll , mock , spyOn } from "bun:test"
1111import * as fs from "fs"
1212import * as os from "os"
1313import * as path from "path"
14+ import * as net from "net"
15+ import { EventEmitter } from "events"
16+ import { PassThrough } from "stream"
1417
1518// ssh2 is an optional dependency, normally not installed for tests; a fake client lets the real tunnel module run.
1619// Bun's mock.module leaks across test files, so the genuine module (or its absence) is restored in afterAll.
@@ -41,6 +44,7 @@ const sshClients: { host: string; ended: boolean }[] = []
4144const heldReady = new Map < string , ( ) => void > ( )
4245/** Simulates the SSH server closing the connection cleanly (no error event). */
4346const remoteClose = new Map < string , ( ) => void > ( )
47+ let forwardedStream : PassThrough | undefined
4448mock . module ( "ssh2" , ( ) => ( {
4549 Client : class {
4650 private handlers = new Map < string , ( ...args : unknown [ ] ) => void > ( )
@@ -57,8 +61,9 @@ mock.module("ssh2", () => ({
5761 if ( options . host . startsWith ( "held-" ) ) heldReady . set ( options . host , ready )
5862 else queueMicrotask ( ready )
5963 }
60- forwardOut ( _a : string , _b : number , _c : string , _d : number , cb : ( err ?: Error ) => void ) {
61- cb ( new Error ( "no forwarding in tests" ) )
64+ forwardOut ( _a : string , _b : number , _c : string , _d : number , cb : ( err ?: Error , stream ?: PassThrough ) => void ) {
65+ if ( forwardedStream ) cb ( undefined , forwardedStream )
66+ else cb ( new Error ( "no forwarding in tests" ) )
6267 }
6368 end ( ) {
6469 this . record . ended = true
@@ -67,7 +72,7 @@ mock.module("ssh2", () => ({
6772} ) )
6873
6974import * as Registry from "../../src/altimate/native/connections/registry"
70- import { closeAllTunnels , openTunnelCount } from "../../src/altimate/native/connections/ssh-tunnel"
75+ import { openTunnel , closeAllTunnels , openTunnelCount } from "../../src/altimate/native/connections/ssh-tunnel"
7176import { Instance } from "../../src/project/instance"
7277import { disposeInstance } from "../../src/effect/instance-registry"
7378
@@ -119,6 +124,7 @@ beforeEach(() => {
119124 process . env . OPENCODE_TEST_HOME = home
120125 projectA = fs . realpathSync ( fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "rpp-a-" ) ) )
121126 projectB = fs . realpathSync ( fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "rpp-b-" ) ) )
127+ forwardedStream = undefined
122128 sshClients . length = 0
123129 heldReady . clear ( )
124130 remoteClose . clear ( )
@@ -265,6 +271,40 @@ describe("connectors are per project", () => {
265271 } )
266272} )
267273
274+ describe ( "SSH forwarding errors" , ( ) => {
275+ for ( const side of [ "local" , "remote" ] as const ) {
276+ test ( `${ side } socket errors destroy both forwarding endpoints without throwing` , async ( ) => {
277+ const stream = new PassThrough ( )
278+ forwardedStream = stream
279+ let accept : ( ( socket : net . Socket ) => void ) | undefined
280+ const server = Object . assign ( new EventEmitter ( ) , {
281+ listen : ( _port : number , _host : string , ready : ( ) => void ) => ready ( ) ,
282+ address : ( ) => ( { port : 12345 } ) ,
283+ close : ( ) => { } ,
284+ } )
285+ const createServer = spyOn ( net , "createServer" ) . mockImplementation ( ( ( listener : ( socket : net . Socket ) => void ) => {
286+ accept = listener
287+ return server
288+ } ) as typeof net . createServer )
289+ const tunnel = openTunnel ( { ssh_host : "forwarding" , host : "db" , port : 5432 } )
290+ const local = new net . Socket ( )
291+ try {
292+ await tunnel . ready
293+ expect ( accept ) . toBeDefined ( )
294+ accept ! ( local )
295+ const endpoint = side === "local" ? local : stream
296+ expect ( ( ) => endpoint . emit ( "error" , new Error ( "ECONNRESET" ) ) ) . not . toThrow ( )
297+ expect ( local . destroyed ) . toBe ( true )
298+ expect ( stream . destroyed ) . toBe ( true )
299+ } finally {
300+ local . destroy ( )
301+ tunnel . close ( )
302+ createServer . mockRestore ( )
303+ }
304+ } )
305+ }
306+ } )
307+
268308describe ( "each connect attempt owns its SSH tunnel" , ( ) => {
269309 test ( "a stale tunnel start that finishes after a newer start never publishes or closes the newer tunnel" , async ( ) => {
270310 writeLocal ( projectA , { store : sshStore ( "held-old" ) } )
@@ -390,6 +430,29 @@ describe("remove()", () => {
390430} )
391431
392432describe ( "credentials of a removed connection" , ( ) => {
433+ test ( "are kept for an open project that has never used the warehouse registry" , async ( ) => {
434+ const { setSecretBackendForTests } = await import ( "../../src/altimate/native/connections/credential-store" )
435+ const store = new Map < string , string > ( )
436+ setSecretBackendForTests ( {
437+ name : "test" ,
438+ get : async ( k : string ) => store . get ( k ) ?? null ,
439+ set : async ( k : string , v : string ) => void store . set ( k , v ) ,
440+ delete : async ( k : string ) => store . delete ( k ) ,
441+ } as any )
442+ try {
443+ writeLocal ( projectB , { shared : { type : "postgres" , host : "b" } } )
444+ await Instance . provide ( { directory : projectB , fn : async ( ) => { } } )
445+ await inProject ( projectA , ( ) => Registry . add ( "shared" , { type : "postgres" , host : "g" , password : "pw-test" } as any ) )
446+ expect ( store . get ( "shared/password" ) ) . toBe ( "pw-test" )
447+
448+ expect ( ( await inProject ( projectA , ( ) => Registry . remove ( "shared" ) ) ) . success ) . toBe ( true )
449+ expect ( store . get ( "shared/password" ) ) . toBe ( "pw-test" )
450+ } finally {
451+ await Instance . provide ( { directory : projectB , fn : ( ) => Instance . dispose ( ) } )
452+ setSecretBackendForTests ( undefined )
453+ }
454+ } )
455+
393456 test ( "are kept while another served project's local config still names the connection" , async ( ) => {
394457 const { setSecretBackendForTests } = await import ( "../../src/altimate/native/connections/credential-store" )
395458 const store = new Map < string , string > ( )
0 commit comments