refactor(remote): add transport selection port
- commit
- 596dbfb8e56b833c54f6b821708fab291f34e95c
- author
- brice · · 2026-07-06 20:49 UTC
- parent
- fd55831
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +275 -46
M internal/app/run.go +64 -17
| @@ -326,26 +326,54 @@ func runAttach(ctx context.Context, intent uint8, name, remoteTarget string) err | |||
| 326 | 326 | defer func() { _ = logCloser.Close() }() | |
| 327 | 327 | ||
| 328 | 328 | return runAttachWithDeps(ctx, intent, name, remoteTarget, os.Getenv("VEV"), log, runAttachDeps{ | |
| 329 | - | localDialer: defaultLocalDialer, | |
| 330 | - | remoteDialer: defaultRemoteDialer, | |
| 331 | - | runClient: client.Run, | |
| 332 | - | createDetached: createDetachedLocalSession, | |
| 333 | - | clipboard: clipboard.New(), | |
| 329 | + | localDialer: defaultLocalDialer, | |
| 330 | + | remoteDialerFactory: defaultRemoteDialerFactory(), | |
| 331 | + | selectedRemoteTransport: os.Getenv(envRemoteTransport), | |
| 332 | + | runClient: client.Run, | |
| 333 | + | createDetached: createDetachedLocalSession, | |
| 334 | + | clipboard: clipboard.New(), | |
| 334 | 335 | }) | |
| 335 | 336 | } | |
| 336 | 337 | ||
| 338 | + | const envRemoteTransport = "VEV_REMOTE_TRANSPORT" | |
| 339 | + | ||
| 337 | 340 | func defaultLocalDialer() ports.Dialer { return localDaemonDialer{dir: ipc.SocketDir()} } | |
| 338 | 341 | ||
| 339 | - | func defaultRemoteDialer(target, session string, log *slog.Logger) ports.Dialer { | |
| 340 | - | return dgram.NewRemoteDialerWithLogger(target, session, log) | |
| 342 | + | func defaultRemoteDialerFactory() ports.RemoteDialerFactory { return remoteDialerFactory{} } | |
| 343 | + | ||
| 344 | + | type remoteDialerFactory struct{} | |
| 345 | + | ||
| 346 | + | func (remoteDialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |
| 347 | + | switch mode { | |
| 348 | + | case ports.RemoteTransportUDP: | |
| 349 | + | return dgram.NewRemoteDialerWithLogger(target, session, log), nil | |
| 350 | + | case ports.RemoteTransportStdio: | |
| 351 | + | if log != nil { | |
| 352 | + | log.Info("remote transport selected", "mode", string(ports.RemoteTransportStdio)) | |
| 353 | + | } | |
| 354 | + | return sshStdioDialer{target: target, session: session, log: log}, nil | |
| 355 | + | default: | |
| 356 | + | return nil, fmt.Errorf("vev: unsupported remote transport %q", mode) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | type sshStdioDialer struct { | |
| 361 | + | target string | |
| 362 | + | session string | |
| 363 | + | log *slog.Logger | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | func (d sshStdioDialer) Dial(ctx context.Context) (ports.Transport, error) { | |
| 367 | + | return sshstdio.DialContext(ctx, d.target, d.session, d.log) | |
| 341 | 368 | } | |
| 342 | 369 | ||
| 343 | 370 | type runAttachDeps struct { | |
| 344 | - | attachLocal func(context.Context, uint8, string, *slog.Logger) error // compatibility hook for focused tests | |
| 345 | - | localDialer func() ports.Dialer | |
| 346 | - | remoteDialer func(target, session string, log *slog.Logger) ports.Dialer | |
| 347 | - | runClient func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error | |
| 348 | - | createDetached func(context.Context, string) error | |
| 371 | + | attachLocal func(context.Context, uint8, string, *slog.Logger) error // compatibility hook for focused tests | |
| 372 | + | localDialer func() ports.Dialer | |
| 373 | + | remoteDialerFactory ports.RemoteDialerFactory | |
| 374 | + | selectedRemoteTransport string | |
| 375 | + | runClient func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error | |
| 376 | + | createDetached func(context.Context, string) error | |
| 349 | 377 | // clipboard reads a clipboard image on a remote attach's Ctrl+V (see | |
| 350 | 378 | // docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md). | |
| 351 | 379 | // Only used for the remote-dialer branch below; local attaches never | |
| @@ -353,6 +381,17 @@ type runAttachDeps struct { | |||
| 353 | 381 | clipboard ports.ClipboardReader | |
| 354 | 382 | } | |
| 355 | 383 | ||
| 384 | + | func remoteTransportModeFromEnv(value string) (ports.RemoteTransportMode, error) { | |
| 385 | + | switch value { | |
| 386 | + | case "", string(ports.RemoteTransportUDP): | |
| 387 | + | return ports.RemoteTransportUDP, nil | |
| 388 | + | case string(ports.RemoteTransportStdio): | |
| 389 | + | return ports.RemoteTransportStdio, nil | |
| 390 | + | default: | |
| 391 | + | return "", fmt.Errorf("vev: invalid remote transport %q (want %q or %q)", value, ports.RemoteTransportUDP, ports.RemoteTransportStdio) | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | ||
| 356 | 395 | func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error { | |
| 357 | 396 | if activeSession != "" { | |
| 358 | 397 | if remoteTarget == "" && intent == ports.IntentNew { | |
| @@ -366,14 +405,22 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac | |||
| 366 | 405 | runClient = client.Run | |
| 367 | 406 | } | |
| 368 | 407 | if remoteTarget != "" { | |
| 369 | - | remoteDialer := deps.remoteDialer | |
| 370 | - | if remoteDialer == nil { | |
| 371 | - | remoteDialer = defaultRemoteDialer | |
| 408 | + | mode, err := remoteTransportModeFromEnv(deps.selectedRemoteTransport) | |
| 409 | + | if err != nil { | |
| 410 | + | return err | |
| 411 | + | } | |
| 412 | + | factory := deps.remoteDialerFactory | |
| 413 | + | if factory == nil { | |
| 414 | + | factory = defaultRemoteDialerFactory() | |
| 372 | 415 | } | |
| 373 | 416 | if log != nil { | |
| 374 | - | log.Info("attaching to remote session", "target", remoteTarget, "name", name) | |
| 417 | + | log.Info("attaching to remote session", "target", remoteTarget, "name", name, "transport", string(mode)) | |
| 418 | + | } | |
| 419 | + | dialer, err := factory.DialerForRemote(remoteTarget, name, mode, log) | |
| 420 | + | if err != nil { | |
| 421 | + | return err | |
| 375 | 422 | } | |
| 376 | - | return runClient(ctx, remoteDialer(remoteTarget, name, log), term.New(), clock.New(), intent, name, true, deps.clipboard, log) | |
| 423 | + | return runClient(ctx, dialer, term.New(), clock.New(), intent, name, true, deps.clipboard, log) | |
| 377 | 424 | } | |
| 378 | 425 | ||
| 379 | 426 | attachLocal := deps.attachLocal | |
| @@ -326,26 +326,54 @@ func runAttach(ctx context.Context, intent uint8, name, remoteTarget string) err | |||||
| 326 | defer func() { _ = logCloser.Close() }() | 326 | defer func() { _ = logCloser.Close() }() | ||
| 327 | 327 | ||||
| 328 | return runAttachWithDeps(ctx, intent, name, remoteTarget, os.Getenv("VEV"), log, runAttachDeps{ | 328 | return runAttachWithDeps(ctx, intent, name, remoteTarget, os.Getenv("VEV"), log, runAttachDeps{ | ||
| 329 | - | localDialer: defaultLocalDialer, | 329 | + | localDialer: defaultLocalDialer, |
| 330 | - | remoteDialer: defaultRemoteDialer, | 330 | + | remoteDialerFactory: defaultRemoteDialerFactory(), |
| 331 | - | runClient: client.Run, | 331 | + | selectedRemoteTransport: os.Getenv(envRemoteTransport), |
| 332 | - | createDetached: createDetachedLocalSession, | 332 | + | runClient: client.Run, |
| 333 | - | clipboard: clipboard.New(), | 333 | + | createDetached: createDetachedLocalSession, |
| 334 | + | clipboard: clipboard.New(), | |||
| 334 | }) | 335 | }) | ||
| 335 | } | 336 | } | ||
| 336 | 337 | ||||
| 338 | + | const envRemoteTransport = "VEV_REMOTE_TRANSPORT" | |||
| 339 | + | ||||
| 337 | func defaultLocalDialer() ports.Dialer { return localDaemonDialer{dir: ipc.SocketDir()} } | 340 | func defaultLocalDialer() ports.Dialer { return localDaemonDialer{dir: ipc.SocketDir()} } | ||
| 338 | 341 | ||||
| 339 | - | func defaultRemoteDialer(target, session string, log *slog.Logger) ports.Dialer { | 342 | + | func defaultRemoteDialerFactory() ports.RemoteDialerFactory { return remoteDialerFactory{} } |
| 340 | - | return dgram.NewRemoteDialerWithLogger(target, session, log) | 343 | + | |
| 344 | + | type remoteDialerFactory struct{} | |||
| 345 | + | ||||
| 346 | + | func (remoteDialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |||
| 347 | + | switch mode { | |||
| 348 | + | case ports.RemoteTransportUDP: | |||
| 349 | + | return dgram.NewRemoteDialerWithLogger(target, session, log), nil | |||
| 350 | + | case ports.RemoteTransportStdio: | |||
| 351 | + | if log != nil { | |||
| 352 | + | log.Info("remote transport selected", "mode", string(ports.RemoteTransportStdio)) | |||
| 353 | + | } | |||
| 354 | + | return sshStdioDialer{target: target, session: session, log: log}, nil | |||
| 355 | + | default: | |||
| 356 | + | return nil, fmt.Errorf("vev: unsupported remote transport %q", mode) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | ||||
| 360 | + | type sshStdioDialer struct { | |||
| 361 | + | target string | |||
| 362 | + | session string | |||
| 363 | + | log *slog.Logger | |||
| 364 | + | } | |||
| 365 | + | ||||
| 366 | + | func (d sshStdioDialer) Dial(ctx context.Context) (ports.Transport, error) { | |||
| 367 | + | return sshstdio.DialContext(ctx, d.target, d.session, d.log) | |||
| 341 | } | 368 | } | ||
| 342 | 369 | ||||
| 343 | type runAttachDeps struct { | 370 | type runAttachDeps struct { | ||
| 344 | - | attachLocal func(context.Context, uint8, string, *slog.Logger) error // compatibility hook for focused tests | 371 | + | attachLocal func(context.Context, uint8, string, *slog.Logger) error // compatibility hook for focused tests |
| 345 | - | localDialer func() ports.Dialer | 372 | + | localDialer func() ports.Dialer |
| 346 | - | remoteDialer func(target, session string, log *slog.Logger) ports.Dialer | 373 | + | remoteDialerFactory ports.RemoteDialerFactory |
| 347 | - | runClient func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error | 374 | + | selectedRemoteTransport string |
| 348 | - | createDetached func(context.Context, string) error | 375 | + | runClient func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error |
| 376 | + | createDetached func(context.Context, string) error | |||
| 349 | // clipboard reads a clipboard image on a remote attach's Ctrl+V (see | 377 | // clipboard reads a clipboard image on a remote attach's Ctrl+V (see | ||
| 350 | // docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md). | 378 | // docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md). | ||
| 351 | // Only used for the remote-dialer branch below; local attaches never | 379 | // Only used for the remote-dialer branch below; local attaches never | ||
| @@ -353,6 +381,17 @@ type runAttachDeps struct { | |||||
| 353 | clipboard ports.ClipboardReader | 381 | clipboard ports.ClipboardReader | ||
| 354 | } | 382 | } | ||
| 355 | 383 | ||||
| 384 | + | func remoteTransportModeFromEnv(value string) (ports.RemoteTransportMode, error) { | |||
| 385 | + | switch value { | |||
| 386 | + | case "", string(ports.RemoteTransportUDP): | |||
| 387 | + | return ports.RemoteTransportUDP, nil | |||
| 388 | + | case string(ports.RemoteTransportStdio): | |||
| 389 | + | return ports.RemoteTransportStdio, nil | |||
| 390 | + | default: | |||
| 391 | + | return "", fmt.Errorf("vev: invalid remote transport %q (want %q or %q)", value, ports.RemoteTransportUDP, ports.RemoteTransportStdio) | |||
| 392 | + | } | |||
| 393 | + | } | |||
| 394 | + | ||||
| 356 | func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error { | 395 | func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error { | ||
| 357 | if activeSession != "" { | 396 | if activeSession != "" { | ||
| 358 | if remoteTarget == "" && intent == ports.IntentNew { | 397 | if remoteTarget == "" && intent == ports.IntentNew { | ||
| @@ -366,14 +405,22 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac | |||||
| 366 | runClient = client.Run | 405 | runClient = client.Run | ||
| 367 | } | 406 | } | ||
| 368 | if remoteTarget != "" { | 407 | if remoteTarget != "" { | ||
| 369 | - | remoteDialer := deps.remoteDialer | 408 | + | mode, err := remoteTransportModeFromEnv(deps.selectedRemoteTransport) |
| 370 | - | if remoteDialer == nil { | 409 | + | if err != nil { |
| 371 | - | remoteDialer = defaultRemoteDialer | 410 | + | return err |
| 411 | + | } | |||
| 412 | + | factory := deps.remoteDialerFactory | |||
| 413 | + | if factory == nil { | |||
| 414 | + | factory = defaultRemoteDialerFactory() | |||
| 372 | } | 415 | } | ||
| 373 | if log != nil { | 416 | if log != nil { | ||
| 374 | - | log.Info("attaching to remote session", "target", remoteTarget, "name", name) | 417 | + | log.Info("attaching to remote session", "target", remoteTarget, "name", name, "transport", string(mode)) |
| 418 | + | } | |||
| 419 | + | dialer, err := factory.DialerForRemote(remoteTarget, name, mode, log) | |||
| 420 | + | if err != nil { | |||
| 421 | + | return err | |||
| 375 | } | 422 | } | ||
| 376 | - | return runClient(ctx, remoteDialer(remoteTarget, name, log), term.New(), clock.New(), intent, name, true, deps.clipboard, log) | 423 | + | return runClient(ctx, dialer, term.New(), clock.New(), intent, name, true, deps.clipboard, log) |
| 377 | } | 424 | } | ||
| 378 | 425 | ||||
| 379 | attachLocal := deps.attachLocal | 426 | attachLocal := deps.attachLocal | ||
M internal/app/run_test.go +86 -29
| @@ -16,8 +16,10 @@ import ( | |||
| 16 | 16 | "github.com/bnema/vev/internal/domain" | |
| 17 | 17 | "github.com/bnema/vev/internal/persist" | |
| 18 | 18 | "github.com/bnema/vev/internal/ports" | |
| 19 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 19 | 20 | "github.com/bnema/vev/internal/usecase/client" | |
| 20 | 21 | "github.com/bnema/vev/internal/usecase/confirm" | |
| 22 | + | "github.com/stretchr/testify/mock" | |
| 21 | 23 | "github.com/stretchr/testify/require" | |
| 22 | 24 | ) | |
| 23 | 25 | ||
| @@ -436,40 +438,93 @@ func (d namedDialer) Dial(context.Context) (ports.Transport, error) { | |||
| 436 | 438 | // never actually invoked in these wiring tests. | |
| 437 | 439 | type fakeClipboardReader struct{ ports.ClipboardReader } | |
| 438 | 440 | ||
| 439 | - | func TestRunAttachWithDepsBuildsRemoteDialer(t *testing.T) { | |
| 440 | - | // The local client still owns terminal capability detection for remote attach; | |
| 441 | - | // sshstdio is only a framed transport to `vev _stdio` on the remote host. | |
| 442 | - | var gotTarget, gotSession, gotDialer string | |
| 443 | - | var gotRemote bool | |
| 444 | - | var gotClipboard ports.ClipboardReader | |
| 445 | - | clip := &fakeClipboardReader{} | |
| 446 | - | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 447 | - | remoteDialer: func(target, session string, _ *slog.Logger) ports.Dialer { | |
| 448 | - | gotTarget, gotSession = target, session | |
| 449 | - | return namedDialer{name: "remote"} | |
| 450 | - | }, | |
| 451 | - | clipboard: clip, | |
| 452 | - | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | |
| 453 | - | gotDialer = d.(namedDialer).name | |
| 454 | - | gotRemote = remote | |
| 455 | - | gotClipboard = clipboard | |
| 456 | - | if intent != ports.IntentAttach || name != "work" { | |
| 457 | - | t.Fatalf("intent/name = %d/%q, want attach/work", intent, name) | |
| 441 | + | func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) { | |
| 442 | + | tests := []struct { | |
| 443 | + | name string | |
| 444 | + | selectedTransport string | |
| 445 | + | wantMode ports.RemoteTransportMode | |
| 446 | + | }{ | |
| 447 | + | {name: "default remote mode is udp", selectedTransport: "", wantMode: ports.RemoteTransportUDP}, | |
| 448 | + | {name: "explicit stdio mode", selectedTransport: "stdio", wantMode: ports.RemoteTransportStdio}, | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | for _, tt := range tests { | |
| 452 | + | t.Run(tt.name, func(t *testing.T) { | |
| 453 | + | var gotDialer string | |
| 454 | + | var gotRemote bool | |
| 455 | + | var gotClipboard ports.ClipboardReader | |
| 456 | + | clip := &fakeClipboardReader{} | |
| 457 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 458 | + | factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil) | |
| 459 | + | ||
| 460 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 461 | + | remoteDialerFactory: factory, | |
| 462 | + | selectedRemoteTransport: tt.selectedTransport, | |
| 463 | + | clipboard: clip, | |
| 464 | + | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | |
| 465 | + | gotDialer = d.(namedDialer).name | |
| 466 | + | gotRemote = remote | |
| 467 | + | gotClipboard = clipboard | |
| 468 | + | if intent != ports.IntentAttach || name != "work" { | |
| 469 | + | t.Fatalf("intent/name = %d/%q, want attach/work", intent, name) | |
| 470 | + | } | |
| 471 | + | return nil | |
| 472 | + | }, | |
| 473 | + | }) | |
| 474 | + | if err != nil { | |
| 475 | + | t.Fatalf("runAttachWithDeps returned error: %v", err) | |
| 476 | + | } | |
| 477 | + | if gotDialer != "remote" { | |
| 478 | + | t.Fatalf("remote dialer = %q, want remote", gotDialer) | |
| 479 | + | } | |
| 480 | + | if !gotRemote { | |
| 481 | + | t.Fatal("remote attach must pass remote=true to runClient") | |
| 482 | + | } | |
| 483 | + | if gotClipboard != ports.ClipboardReader(clip) { | |
| 484 | + | t.Fatalf("remote attach must thread the configured ClipboardReader through, got %#v", gotClipboard) | |
| 458 | 485 | } | |
| 486 | + | }) | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing.T) { | |
| 491 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 492 | + | runClientCalled := false | |
| 493 | + | ||
| 494 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 495 | + | remoteDialerFactory: factory, | |
| 496 | + | selectedRemoteTransport: "serial", | |
| 497 | + | runClient: func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error { | |
| 498 | + | runClientCalled = true | |
| 459 | 499 | return nil | |
| 460 | 500 | }, | |
| 461 | 501 | }) | |
| 462 | - | if err != nil { | |
| 463 | - | t.Fatalf("runAttachWithDeps returned error: %v", err) | |
| 502 | + | if err == nil || err.Error() != `vev: invalid remote transport "serial" (want "udp" or "stdio")` { | |
| 503 | + | t.Fatalf("runAttachWithDeps error = %v, want invalid transport", err) | |
| 464 | 504 | } | |
| 465 | - | if gotTarget != "remote.example" || gotSession != "work" || gotDialer != "remote" { | |
| 466 | - | t.Fatalf("remote dialer wiring = target %q session %q dialer %q", gotTarget, gotSession, gotDialer) | |
| 505 | + | if runClientCalled { | |
| 506 | + | t.Fatal("runClient called after invalid remote transport") | |
| 467 | 507 | } | |
| 468 | - | if !gotRemote { | |
| 469 | - | t.Fatal("remote attach must pass remote=true to runClient") | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | func TestRunAttachWithDepsReturnsFactoryErrorBeforeRunClient(t *testing.T) { | |
| 511 | + | factoryErr := errors.New("factory failed") | |
| 512 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 513 | + | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr) | |
| 514 | + | runClientCalled := false | |
| 515 | + | ||
| 516 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 517 | + | remoteDialerFactory: factory, | |
| 518 | + | runClient: func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error { | |
| 519 | + | runClientCalled = true | |
| 520 | + | return nil | |
| 521 | + | }, | |
| 522 | + | }) | |
| 523 | + | if !errors.Is(err, factoryErr) { | |
| 524 | + | t.Fatalf("runAttachWithDeps error = %v, want %v", err, factoryErr) | |
| 470 | 525 | } | |
| 471 | - | if gotClipboard != ports.ClipboardReader(clip) { | |
| 472 | - | t.Fatalf("remote attach must thread the configured ClipboardReader through, got %#v", gotClipboard) | |
| 526 | + | if runClientCalled { | |
| 527 | + | t.Fatal("runClient called after remote factory error") | |
| 473 | 528 | } | |
| 474 | 529 | } | |
| 475 | 530 | ||
| @@ -477,9 +532,11 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) { | |||
| 477 | 532 | var gotDialer string | |
| 478 | 533 | gotRemote := true | |
| 479 | 534 | gotClipboard := ports.ClipboardReader(&fakeClipboardReader{}) | |
| 535 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 480 | 536 | err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "", nil, runAttachDeps{ | |
| 481 | - | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, | |
| 482 | - | clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach | |
| 537 | + | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, | |
| 538 | + | remoteDialerFactory: factory, | |
| 539 | + | clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach | |
| 483 | 540 | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | |
| 484 | 541 | gotDialer = d.(namedDialer).name | |
| 485 | 542 | gotRemote = remote | |
| @@ -16,8 +16,10 @@ import ( | |||||
| 16 | "github.com/bnema/vev/internal/domain" | 16 | "github.com/bnema/vev/internal/domain" | ||
| 17 | "github.com/bnema/vev/internal/persist" | 17 | "github.com/bnema/vev/internal/persist" | ||
| 18 | "github.com/bnema/vev/internal/ports" | 18 | "github.com/bnema/vev/internal/ports" | ||
| 19 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |||
| 19 | "github.com/bnema/vev/internal/usecase/client" | 20 | "github.com/bnema/vev/internal/usecase/client" | ||
| 20 | "github.com/bnema/vev/internal/usecase/confirm" | 21 | "github.com/bnema/vev/internal/usecase/confirm" | ||
| 22 | + | "github.com/stretchr/testify/mock" | |||
| 21 | "github.com/stretchr/testify/require" | 23 | "github.com/stretchr/testify/require" | ||
| 22 | ) | 24 | ) | ||
| 23 | 25 | ||||
| @@ -436,40 +438,93 @@ func (d namedDialer) Dial(context.Context) (ports.Transport, error) { | |||||
| 436 | // never actually invoked in these wiring tests. | 438 | // never actually invoked in these wiring tests. | ||
| 437 | type fakeClipboardReader struct{ ports.ClipboardReader } | 439 | type fakeClipboardReader struct{ ports.ClipboardReader } | ||
| 438 | 440 | ||||
| 439 | - | func TestRunAttachWithDepsBuildsRemoteDialer(t *testing.T) { | 441 | + | func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) { |
| 440 | - | // The local client still owns terminal capability detection for remote attach; | 442 | + | tests := []struct { |
| 441 | - | // sshstdio is only a framed transport to `vev _stdio` on the remote host. | 443 | + | name string |
| 442 | - | var gotTarget, gotSession, gotDialer string | 444 | + | selectedTransport string |
| 443 | - | var gotRemote bool | 445 | + | wantMode ports.RemoteTransportMode |
| 444 | - | var gotClipboard ports.ClipboardReader | 446 | + | }{ |
| 445 | - | clip := &fakeClipboardReader{} | 447 | + | {name: "default remote mode is udp", selectedTransport: "", wantMode: ports.RemoteTransportUDP}, |
| 446 | - | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | 448 | + | {name: "explicit stdio mode", selectedTransport: "stdio", wantMode: ports.RemoteTransportStdio}, |
| 447 | - | remoteDialer: func(target, session string, _ *slog.Logger) ports.Dialer { | 449 | + | } |
| 448 | - | gotTarget, gotSession = target, session | 450 | + | |
| 449 | - | return namedDialer{name: "remote"} | 451 | + | for _, tt := range tests { |
| 450 | - | }, | 452 | + | t.Run(tt.name, func(t *testing.T) { |
| 451 | - | clipboard: clip, | 453 | + | var gotDialer string |
| 452 | - | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | 454 | + | var gotRemote bool |
| 453 | - | gotDialer = d.(namedDialer).name | 455 | + | var gotClipboard ports.ClipboardReader |
| 454 | - | gotRemote = remote | 456 | + | clip := &fakeClipboardReader{} |
| 455 | - | gotClipboard = clipboard | 457 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) |
| 456 | - | if intent != ports.IntentAttach || name != "work" { | 458 | + | factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil) |
| 457 | - | t.Fatalf("intent/name = %d/%q, want attach/work", intent, name) | 459 | + | |
| 460 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |||
| 461 | + | remoteDialerFactory: factory, | |||
| 462 | + | selectedRemoteTransport: tt.selectedTransport, | |||
| 463 | + | clipboard: clip, | |||
| 464 | + | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | |||
| 465 | + | gotDialer = d.(namedDialer).name | |||
| 466 | + | gotRemote = remote | |||
| 467 | + | gotClipboard = clipboard | |||
| 468 | + | if intent != ports.IntentAttach || name != "work" { | |||
| 469 | + | t.Fatalf("intent/name = %d/%q, want attach/work", intent, name) | |||
| 470 | + | } | |||
| 471 | + | return nil | |||
| 472 | + | }, | |||
| 473 | + | }) | |||
| 474 | + | if err != nil { | |||
| 475 | + | t.Fatalf("runAttachWithDeps returned error: %v", err) | |||
| 476 | + | } | |||
| 477 | + | if gotDialer != "remote" { | |||
| 478 | + | t.Fatalf("remote dialer = %q, want remote", gotDialer) | |||
| 479 | + | } | |||
| 480 | + | if !gotRemote { | |||
| 481 | + | t.Fatal("remote attach must pass remote=true to runClient") | |||
| 482 | + | } | |||
| 483 | + | if gotClipboard != ports.ClipboardReader(clip) { | |||
| 484 | + | t.Fatalf("remote attach must thread the configured ClipboardReader through, got %#v", gotClipboard) | |||
| 458 | } | 485 | } | ||
| 486 | + | }) | |||
| 487 | + | } | |||
| 488 | + | } | |||
| 489 | + | ||||
| 490 | + | func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing.T) { | |||
| 491 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |||
| 492 | + | runClientCalled := false | |||
| 493 | + | ||||
| 494 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |||
| 495 | + | remoteDialerFactory: factory, | |||
| 496 | + | selectedRemoteTransport: "serial", | |||
| 497 | + | runClient: func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error { | |||
| 498 | + | runClientCalled = true | |||
| 459 | return nil | 499 | return nil | ||
| 460 | }, | 500 | }, | ||
| 461 | }) | 501 | }) | ||
| 462 | - | if err != nil { | 502 | + | if err == nil || err.Error() != `vev: invalid remote transport "serial" (want "udp" or "stdio")` { |
| 463 | - | t.Fatalf("runAttachWithDeps returned error: %v", err) | 503 | + | t.Fatalf("runAttachWithDeps error = %v, want invalid transport", err) |
| 464 | } | 504 | } | ||
| 465 | - | if gotTarget != "remote.example" || gotSession != "work" || gotDialer != "remote" { | 505 | + | if runClientCalled { |
| 466 | - | t.Fatalf("remote dialer wiring = target %q session %q dialer %q", gotTarget, gotSession, gotDialer) | 506 | + | t.Fatal("runClient called after invalid remote transport") |
| 467 | } | 507 | } | ||
| 468 | - | if !gotRemote { | 508 | + | } |
| 469 | - | t.Fatal("remote attach must pass remote=true to runClient") | 509 | + | |
| 510 | + | func TestRunAttachWithDepsReturnsFactoryErrorBeforeRunClient(t *testing.T) { | |||
| 511 | + | factoryErr := errors.New("factory failed") | |||
| 512 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |||
| 513 | + | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr) | |||
| 514 | + | runClientCalled := false | |||
| 515 | + | ||||
| 516 | + | err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |||
| 517 | + | remoteDialerFactory: factory, | |||
| 518 | + | runClient: func(context.Context, ports.Dialer, ports.Terminal, ports.Clock, uint8, string, bool, ports.ClipboardReader, *slog.Logger) error { | |||
| 519 | + | runClientCalled = true | |||
| 520 | + | return nil | |||
| 521 | + | }, | |||
| 522 | + | }) | |||
| 523 | + | if !errors.Is(err, factoryErr) { | |||
| 524 | + | t.Fatalf("runAttachWithDeps error = %v, want %v", err, factoryErr) | |||
| 470 | } | 525 | } | ||
| 471 | - | if gotClipboard != ports.ClipboardReader(clip) { | 526 | + | if runClientCalled { |
| 472 | - | t.Fatalf("remote attach must thread the configured ClipboardReader through, got %#v", gotClipboard) | 527 | + | t.Fatal("runClient called after remote factory error") |
| 473 | } | 528 | } | ||
| 474 | } | 529 | } | ||
| 475 | 530 | ||||
| @@ -477,9 +532,11 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) { | |||||
| 477 | var gotDialer string | 532 | var gotDialer string | ||
| 478 | gotRemote := true | 533 | gotRemote := true | ||
| 479 | gotClipboard := ports.ClipboardReader(&fakeClipboardReader{}) | 534 | gotClipboard := ports.ClipboardReader(&fakeClipboardReader{}) | ||
| 535 | + | factory := portsmocks.NewMockRemoteDialerFactory(t) | |||
| 480 | err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "", nil, runAttachDeps{ | 536 | err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "", nil, runAttachDeps{ | ||
| 481 | - | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, | 537 | + | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, |
| 482 | - | clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach | 538 | + | remoteDialerFactory: factory, |
| 539 | + | clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach | |||
| 483 | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | 540 | runClient: func(_ context.Context, d ports.Dialer, _ ports.Terminal, _ ports.Clock, intent uint8, name string, remote bool, clipboard ports.ClipboardReader, _ *slog.Logger) error { | ||
| 484 | gotDialer = d.(namedDialer).name | 541 | gotDialer = d.(namedDialer).name | ||
| 485 | gotRemote = remote | 542 | gotRemote = remote | ||
M internal/ports/mocks/mocks.go +108 -0
| @@ -7,6 +7,7 @@ package portsmocks | |||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | 9 | "io" | |
| 10 | + | "log/slog" | |
| 10 | 11 | "time" | |
| 11 | 12 | ||
| 12 | 13 | "github.com/bnema/vev/internal/domain" | |
| @@ -2534,3 +2535,110 @@ func (_c *MockClipboardReader_ReadImage_Call) RunAndReturn(run func(ctx context. | |||
| 2534 | 2535 | _c.Call.Return(run) | |
| 2535 | 2536 | return _c | |
| 2536 | 2537 | } | |
| 2538 | + | ||
| 2539 | + | // NewMockRemoteDialerFactory creates a new instance of MockRemoteDialerFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 2540 | + | // The first argument is typically a *testing.T value. | |
| 2541 | + | func NewMockRemoteDialerFactory(t interface { | |
| 2542 | + | mock.TestingT | |
| 2543 | + | Cleanup(func()) | |
| 2544 | + | }) *MockRemoteDialerFactory { | |
| 2545 | + | mock := &MockRemoteDialerFactory{} | |
| 2546 | + | mock.Mock.Test(t) | |
| 2547 | + | ||
| 2548 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 2549 | + | ||
| 2550 | + | return mock | |
| 2551 | + | } | |
| 2552 | + | ||
| 2553 | + | // MockRemoteDialerFactory is an autogenerated mock type for the RemoteDialerFactory type | |
| 2554 | + | type MockRemoteDialerFactory struct { | |
| 2555 | + | mock.Mock | |
| 2556 | + | } | |
| 2557 | + | ||
| 2558 | + | type MockRemoteDialerFactory_Expecter struct { | |
| 2559 | + | mock *mock.Mock | |
| 2560 | + | } | |
| 2561 | + | ||
| 2562 | + | func (_m *MockRemoteDialerFactory) EXPECT() *MockRemoteDialerFactory_Expecter { | |
| 2563 | + | return &MockRemoteDialerFactory_Expecter{mock: &_m.Mock} | |
| 2564 | + | } | |
| 2565 | + | ||
| 2566 | + | // DialerForRemote provides a mock function for the type MockRemoteDialerFactory | |
| 2567 | + | func (_mock *MockRemoteDialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |
| 2568 | + | ret := _mock.Called(target, session, mode, log) | |
| 2569 | + | ||
| 2570 | + | if len(ret) == 0 { | |
| 2571 | + | panic("no return value specified for DialerForRemote") | |
| 2572 | + | } | |
| 2573 | + | ||
| 2574 | + | var r0 ports.Dialer | |
| 2575 | + | var r1 error | |
| 2576 | + | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) (ports.Dialer, error)); ok { | |
| 2577 | + | return returnFunc(target, session, mode, log) | |
| 2578 | + | } | |
| 2579 | + | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) ports.Dialer); ok { | |
| 2580 | + | r0 = returnFunc(target, session, mode, log) | |
| 2581 | + | } else { | |
| 2582 | + | if ret.Get(0) != nil { | |
| 2583 | + | r0 = ret.Get(0).(ports.Dialer) | |
| 2584 | + | } | |
| 2585 | + | } | |
| 2586 | + | if returnFunc, ok := ret.Get(1).(func(string, string, ports.RemoteTransportMode, *slog.Logger) error); ok { | |
| 2587 | + | r1 = returnFunc(target, session, mode, log) | |
| 2588 | + | } else { | |
| 2589 | + | r1 = ret.Error(1) | |
| 2590 | + | } | |
| 2591 | + | return r0, r1 | |
| 2592 | + | } | |
| 2593 | + | ||
| 2594 | + | // MockRemoteDialerFactory_DialerForRemote_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DialerForRemote' | |
| 2595 | + | type MockRemoteDialerFactory_DialerForRemote_Call struct { | |
| 2596 | + | *mock.Call | |
| 2597 | + | } | |
| 2598 | + | ||
| 2599 | + | // DialerForRemote is a helper method to define mock.On call | |
| 2600 | + | // - target string | |
| 2601 | + | // - session string | |
| 2602 | + | // - mode ports.RemoteTransportMode | |
| 2603 | + | // - log *slog.Logger | |
| 2604 | + | func (_e *MockRemoteDialerFactory_Expecter) DialerForRemote(target any, session any, mode any, log any) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 2605 | + | return &MockRemoteDialerFactory_DialerForRemote_Call{Call: _e.mock.On("DialerForRemote", target, session, mode, log)} | |
| 2606 | + | } | |
| 2607 | + | ||
| 2608 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Run(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger)) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 2609 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2610 | + | var arg0 string | |
| 2611 | + | if args[0] != nil { | |
| 2612 | + | arg0 = args[0].(string) | |
| 2613 | + | } | |
| 2614 | + | var arg1 string | |
| 2615 | + | if args[1] != nil { | |
| 2616 | + | arg1 = args[1].(string) | |
| 2617 | + | } | |
| 2618 | + | var arg2 ports.RemoteTransportMode | |
| 2619 | + | if args[2] != nil { | |
| 2620 | + | arg2 = args[2].(ports.RemoteTransportMode) | |
| 2621 | + | } | |
| 2622 | + | var arg3 *slog.Logger | |
| 2623 | + | if args[3] != nil { | |
| 2624 | + | arg3 = args[3].(*slog.Logger) | |
| 2625 | + | } | |
| 2626 | + | run( | |
| 2627 | + | arg0, | |
| 2628 | + | arg1, | |
| 2629 | + | arg2, | |
| 2630 | + | arg3, | |
| 2631 | + | ) | |
| 2632 | + | }) | |
| 2633 | + | return _c | |
| 2634 | + | } | |
| 2635 | + | ||
| 2636 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Return(dialer ports.Dialer, err error) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 2637 | + | _c.Call.Return(dialer, err) | |
| 2638 | + | return _c | |
| 2639 | + | } | |
| 2640 | + | ||
| 2641 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) RunAndReturn(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error)) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 2642 | + | _c.Call.Return(run) | |
| 2643 | + | return _c | |
| 2644 | + | } | |
| @@ -7,6 +7,7 @@ package portsmocks | |||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | "io" | 9 | "io" | ||
| 10 | + | "log/slog" | |||
| 10 | "time" | 11 | "time" | ||
| 11 | 12 | ||||
| 12 | "github.com/bnema/vev/internal/domain" | 13 | "github.com/bnema/vev/internal/domain" | ||
| @@ -2534,3 +2535,110 @@ func (_c *MockClipboardReader_ReadImage_Call) RunAndReturn(run func(ctx context. | |||||
| 2534 | _c.Call.Return(run) | 2535 | _c.Call.Return(run) | ||
| 2535 | return _c | 2536 | return _c | ||
| 2536 | } | 2537 | } | ||
| 2538 | + | ||||
| 2539 | + | // NewMockRemoteDialerFactory creates a new instance of MockRemoteDialerFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 2540 | + | // The first argument is typically a *testing.T value. | |||
| 2541 | + | func NewMockRemoteDialerFactory(t interface { | |||
| 2542 | + | mock.TestingT | |||
| 2543 | + | Cleanup(func()) | |||
| 2544 | + | }) *MockRemoteDialerFactory { | |||
| 2545 | + | mock := &MockRemoteDialerFactory{} | |||
| 2546 | + | mock.Mock.Test(t) | |||
| 2547 | + | ||||
| 2548 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |||
| 2549 | + | ||||
| 2550 | + | return mock | |||
| 2551 | + | } | |||
| 2552 | + | ||||
| 2553 | + | // MockRemoteDialerFactory is an autogenerated mock type for the RemoteDialerFactory type | |||
| 2554 | + | type MockRemoteDialerFactory struct { | |||
| 2555 | + | mock.Mock | |||
| 2556 | + | } | |||
| 2557 | + | ||||
| 2558 | + | type MockRemoteDialerFactory_Expecter struct { | |||
| 2559 | + | mock *mock.Mock | |||
| 2560 | + | } | |||
| 2561 | + | ||||
| 2562 | + | func (_m *MockRemoteDialerFactory) EXPECT() *MockRemoteDialerFactory_Expecter { | |||
| 2563 | + | return &MockRemoteDialerFactory_Expecter{mock: &_m.Mock} | |||
| 2564 | + | } | |||
| 2565 | + | ||||
| 2566 | + | // DialerForRemote provides a mock function for the type MockRemoteDialerFactory | |||
| 2567 | + | func (_mock *MockRemoteDialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |||
| 2568 | + | ret := _mock.Called(target, session, mode, log) | |||
| 2569 | + | ||||
| 2570 | + | if len(ret) == 0 { | |||
| 2571 | + | panic("no return value specified for DialerForRemote") | |||
| 2572 | + | } | |||
| 2573 | + | ||||
| 2574 | + | var r0 ports.Dialer | |||
| 2575 | + | var r1 error | |||
| 2576 | + | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) (ports.Dialer, error)); ok { | |||
| 2577 | + | return returnFunc(target, session, mode, log) | |||
| 2578 | + | } | |||
| 2579 | + | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) ports.Dialer); ok { | |||
| 2580 | + | r0 = returnFunc(target, session, mode, log) | |||
| 2581 | + | } else { | |||
| 2582 | + | if ret.Get(0) != nil { | |||
| 2583 | + | r0 = ret.Get(0).(ports.Dialer) | |||
| 2584 | + | } | |||
| 2585 | + | } | |||
| 2586 | + | if returnFunc, ok := ret.Get(1).(func(string, string, ports.RemoteTransportMode, *slog.Logger) error); ok { | |||
| 2587 | + | r1 = returnFunc(target, session, mode, log) | |||
| 2588 | + | } else { | |||
| 2589 | + | r1 = ret.Error(1) | |||
| 2590 | + | } | |||
| 2591 | + | return r0, r1 | |||
| 2592 | + | } | |||
| 2593 | + | ||||
| 2594 | + | // MockRemoteDialerFactory_DialerForRemote_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DialerForRemote' | |||
| 2595 | + | type MockRemoteDialerFactory_DialerForRemote_Call struct { | |||
| 2596 | + | *mock.Call | |||
| 2597 | + | } | |||
| 2598 | + | ||||
| 2599 | + | // DialerForRemote is a helper method to define mock.On call | |||
| 2600 | + | // - target string | |||
| 2601 | + | // - session string | |||
| 2602 | + | // - mode ports.RemoteTransportMode | |||
| 2603 | + | // - log *slog.Logger | |||
| 2604 | + | func (_e *MockRemoteDialerFactory_Expecter) DialerForRemote(target any, session any, mode any, log any) *MockRemoteDialerFactory_DialerForRemote_Call { | |||
| 2605 | + | return &MockRemoteDialerFactory_DialerForRemote_Call{Call: _e.mock.On("DialerForRemote", target, session, mode, log)} | |||
| 2606 | + | } | |||
| 2607 | + | ||||
| 2608 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Run(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger)) *MockRemoteDialerFactory_DialerForRemote_Call { | |||
| 2609 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2610 | + | var arg0 string | |||
| 2611 | + | if args[0] != nil { | |||
| 2612 | + | arg0 = args[0].(string) | |||
| 2613 | + | } | |||
| 2614 | + | var arg1 string | |||
| 2615 | + | if args[1] != nil { | |||
| 2616 | + | arg1 = args[1].(string) | |||
| 2617 | + | } | |||
| 2618 | + | var arg2 ports.RemoteTransportMode | |||
| 2619 | + | if args[2] != nil { | |||
| 2620 | + | arg2 = args[2].(ports.RemoteTransportMode) | |||
| 2621 | + | } | |||
| 2622 | + | var arg3 *slog.Logger | |||
| 2623 | + | if args[3] != nil { | |||
| 2624 | + | arg3 = args[3].(*slog.Logger) | |||
| 2625 | + | } | |||
| 2626 | + | run( | |||
| 2627 | + | arg0, | |||
| 2628 | + | arg1, | |||
| 2629 | + | arg2, | |||
| 2630 | + | arg3, | |||
| 2631 | + | ) | |||
| 2632 | + | }) | |||
| 2633 | + | return _c | |||
| 2634 | + | } | |||
| 2635 | + | ||||
| 2636 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Return(dialer ports.Dialer, err error) *MockRemoteDialerFactory_DialerForRemote_Call { | |||
| 2637 | + | _c.Call.Return(dialer, err) | |||
| 2638 | + | return _c | |||
| 2639 | + | } | |||
| 2640 | + | ||||
| 2641 | + | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) RunAndReturn(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error)) *MockRemoteDialerFactory_DialerForRemote_Call { | |||
| 2642 | + | _c.Call.Return(run) | |||
| 2643 | + | return _c | |||
| 2644 | + | } | |||
A internal/ports/remote.go +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | package ports | |
| 2 | + | ||
| 3 | + | import "log/slog" | |
| 4 | + | ||
| 5 | + | // RemoteTransportMode selects the transport used for remote attach after CLI parsing. | |
| 6 | + | type RemoteTransportMode string | |
| 7 | + | ||
| 8 | + | const ( | |
| 9 | + | RemoteTransportUDP RemoteTransportMode = "udp" | |
| 10 | + | RemoteTransportStdio RemoteTransportMode = "stdio" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // RemoteDialerFactory builds the transport-specific dialer for a remote target. | |
| 14 | + | // Implementations live in adapters; app wiring consumes this interface. | |
| 15 | + | type RemoteDialerFactory interface { | |
| 16 | + | DialerForRemote(target string, session string, mode RemoteTransportMode, log *slog.Logger) (Dialer, error) | |
| 17 | + | } | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | import "log/slog" | |||
| 4 | + | ||||
| 5 | + | // RemoteTransportMode selects the transport used for remote attach after CLI parsing. | |||
| 6 | + | type RemoteTransportMode string | |||
| 7 | + | ||||
| 8 | + | const ( | |||
| 9 | + | RemoteTransportUDP RemoteTransportMode = "udp" | |||
| 10 | + | RemoteTransportStdio RemoteTransportMode = "stdio" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // RemoteDialerFactory builds the transport-specific dialer for a remote target. | |||
| 14 | + | // Implementations live in adapters; app wiring consumes this interface. | |||
| 15 | + | type RemoteDialerFactory interface { | |||
| 16 | + | DialerForRemote(target string, session string, mode RemoteTransportMode, log *slog.Logger) (Dialer, error) | |||
| 17 | + | } | |||