refactor(architecture): enforce application port boundaries
- commit
- edcc67a2aca6b372b5afcb867aa81d27a94ea55d
- author
- brice · · 2026-08-28 08:12 UTC
- committer
- brice · · 2026-08-28 14:56 UTC
- parent
- 9af94c6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
82 files · +1778 -1655
M .mockery.yml +12 -0
| @@ -28,6 +28,18 @@ packages: | |||
| 28 | 28 | dir: internal/ports/mocks | |
| 29 | 29 | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | |
| 30 | 30 | pkgname: portsmocks | |
| 31 | + | github.com/bnema/vev/internal/protocol/wire: | |
| 32 | + | config: | |
| 33 | + | dir: internal/protocol/wire/mocks | |
| 34 | + | filename: transport_mock.go | |
| 35 | + | pkgname: wiremocks | |
| 36 | + | interfaces: | |
| 37 | + | Transport: | |
| 38 | + | DatagramTransport: | |
| 39 | + | AsyncTransport: | |
| 40 | + | OwnedSynchronousTransport: | |
| 41 | + | Dialer: | |
| 42 | + | Listener: | |
| 31 | 43 | github.com/bnema/vev/internal/usecase/command: | |
| 32 | 44 | interfaces: | |
| 33 | 45 | Context: | |
M AGENTS.md +16 -14
| @@ -28,28 +28,30 @@ go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc ./internal/usecase/daemo | |||
| 28 | 28 | ||
| 29 | 29 | ## Architecture rules | |
| 30 | 30 | ||
| 31 | - | Hexagonal boundaries are enforced by `boundary_test.go`. | |
| 31 | + | The exhaustive production matrix and separate test-import policy are enforced by `boundary_test.go`; package ownership is documented in `docs/architecture.md`. | |
| 32 | 32 | ||
| 33 | - | - `pkg/` is reusable and must not import `internal/`. | |
| 34 | - | - `internal/usecase/` must not import `internal/adapters/`. | |
| 35 | - | - Usecases depend on `internal/ports` and `internal/domain`. | |
| 33 | + | - `pkg/` is reusable and never imports `internal/`. | |
| 34 | + | - Production use cases import only `internal/ports`, semantic `internal/protocol` packages, `internal/domain`, and approved sibling use cases. | |
| 35 | + | - Production use cases never import `internal/protocol/wire`, concrete adapters, `internal/app`, `internal/persist`, or `internal/platform`. | |
| 36 | + | - `internal/ports` owns application seams, not codecs, raw frames, environment policy, or worker implementations. | |
| 36 | 37 | ||
| 37 | 38 | Layer map: | |
| 38 | 39 | ||
| 39 | - | - `main.go` → `internal/app`: CLI parsing, wiring, daemon startup, hidden subcommands. | |
| 40 | - | - `internal/ports`: application and raw carriage interfaces. | |
| 41 | - | - `internal/protocol`: typed transport-neutral messages and negotiated session policy. | |
| 42 | - | - `internal/protocol/wire`: message IDs, frames, strict binary codecs, compression, and encoded bounds. | |
| 43 | - | - `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, daemon features. | |
| 40 | + | - `main.go` → `internal/app`: CLI parsing, composition, daemon startup, and hidden subcommands. | |
| 41 | + | - `internal/domain`: pure shared values and terminal capability policy. | |
| 42 | + | - `internal/protocol`: typed session messages; `protocol/catalogue` owns remote JSON schema; `protocol/wire` owns binary encoding and raw carriage contracts. | |
| 43 | + | - `internal/ports`: typed application connections and infrastructure-facing use-case seams. | |
| 44 | + | - `internal/adapters/sessionwire`: typed message ↔ raw frame adaptation. | |
| 45 | + | - `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, and daemon features. | |
| 44 | 46 | - `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim and interprets nothing. | |
| 45 | - | - `internal/adapters`: IPC, SSH stdio, PTY, terminal, and clock implementations. | |
| 47 | + | - `internal/adapters`: IPC, UDP, SSH stdio, PTY, terminal, clock, and observability implementations. | |
| 46 | 48 | ||
| 47 | 49 | Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`. | |
| 48 | 50 | ||
| 49 | 51 | ## Ports and mocks | |
| 50 | 52 | ||
| 51 | - | - Define cross-layer interfaces only in `internal/ports`. | |
| 52 | - | - Adapters implement ports; usecases and `internal/app` consume ports. | |
| 53 | + | - Define application cross-layer interfaces in `internal/ports`; raw frame carriage interfaces live in `internal/protocol/wire`. | |
| 54 | + | - Adapters implement ports and raw carriage contracts; use cases consume only application ports. | |
| 53 | 55 | - Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`. | |
| 54 | 56 | - Hand-written fakes are okay only when mocks make the test harder to understand. | |
| 55 | 57 | - After changing a port, run `make mocks` and commit the generated updates. | |
| @@ -63,7 +65,7 @@ Before touching daemon teardown paths, read the lock-ordering notes at the top o | |||
| 63 | 65 | ||
| 64 | 66 | ## Wire protocol | |
| 65 | 67 | ||
| 66 | - | Typed messages and negotiated version live in `internal/protocol`. Message IDs, frames, strict codecs, compression, and encoded bounds live in `internal/protocol/wire`; raw carriage interfaces remain in `internal/ports`. Connection framing lives in concrete carriage adapters such as `internal/adapters/ipc`. | |
| 68 | + | Typed messages and negotiated version live in `internal/protocol`. Remote discovery JSON lives in `internal/protocol/catalogue`. Message IDs, frames, strict codecs, compression, encoded bounds, and raw carriage interfaces live in `internal/protocol/wire`. Connection framing lives in concrete carriage adapters such as `internal/adapters/ipc`. | |
| 67 | 69 | ||
| 68 | 70 | - IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload. | |
| 69 | 71 | - Client message types occupy `1–13`, `15`, `32–33`, and `35` (`MsgParkedRouteRequest`); server types occupy `16–23`, `25–31`, `34`, and `36` (`MsgParkedRouteResponse`). Types `14` and `24` remain reserved. | |
| @@ -77,7 +79,7 @@ Typed messages and negotiated version live in `internal/protocol`. Message IDs, | |||
| 77 | 79 | - Named sessions survive headless and persist across daemon restarts. | |
| 78 | 80 | - The daemon starts on first use and exits when the last session ends. | |
| 79 | 81 | - Each connection has a 15-second handshake budget from connect through the initial committed publication. | |
| 80 | - | - Local and remote attach use the same `Hello`/`Welcome` session protocol, then pump `Input`/`Resize` and `Output` frames over a byte-only Transport. | |
| 82 | + | - Local and remote attach use the same typed `Hello`/`Welcome` session protocol. `sessionwire` translates typed traffic to raw frames carried by IPC, UDP, or SSH stdio. | |
| 81 | 83 | - Remote attach opens a direct connection to the selected daemon over UDP by default; `VEV_REMOTE_TRANSPORT=stdio` explicitly selects an SSH-only carriage. | |
| 82 | 84 | - A session owns shared PTYs, VT state, tabs, panes, PTY content geometry selected by the latest valid attachment claim, and ordered mutations. Each attachment owns its window/view, copy and overlay state, rendering/output state, and reconnect lifecycle; when the latest claimant detaches, the most recently claimed remaining attachment becomes authoritative. | |
| 83 | 85 | - Command requests have a 10-second result deadline and are tracked per connection. | |
M boundary_test.go +189 -46
| @@ -1,84 +1,227 @@ | |||
| 1 | 1 | package main_test | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "go/parser" | |
| 5 | 6 | "go/token" | |
| 6 | 7 | "io/fs" | |
| 7 | 8 | "path/filepath" | |
| 9 | + | "sort" | |
| 10 | + | "strconv" | |
| 8 | 11 | "strings" | |
| 9 | 12 | "testing" | |
| 10 | 13 | ) | |
| 11 | 14 | ||
| 12 | - | // boundaryRule expresses a single import-boundary constraint: no .go file | |
| 13 | - | // under root may import a package whose path has disallowedPrefix. | |
| 14 | - | type boundaryRule struct { | |
| 15 | - | name string | |
| 16 | - | root string | |
| 17 | - | disallowedPrefix string | |
| 15 | + | const modulePath = "github.com/bnema/vev" | |
| 16 | + | ||
| 17 | + | type packageLayer string | |
| 18 | + | ||
| 19 | + | const ( | |
| 20 | + | layerRoot packageLayer = "root" | |
| 21 | + | layerCommand packageLayer = "command" | |
| 22 | + | layerScript packageLayer = "script" | |
| 23 | + | layerApp packageLayer = "app" | |
| 24 | + | layerAdapter packageLayer = "adapter" | |
| 25 | + | layerUsecase packageLayer = "usecase" | |
| 26 | + | layerPorts packageLayer = "ports" | |
| 27 | + | layerProtocol packageLayer = "protocol" | |
| 28 | + | layerCatalogue packageLayer = "catalogue" | |
| 29 | + | layerWire packageLayer = "wire" | |
| 30 | + | layerDomain packageLayer = "domain" | |
| 31 | + | layerPersist packageLayer = "persist" | |
| 32 | + | layerPlatform packageLayer = "platform" | |
| 33 | + | layerLogging packageLayer = "logging" | |
| 34 | + | layerPkg packageLayer = "pkg" | |
| 35 | + | layerTestSupport packageLayer = "test-support" | |
| 36 | + | ) | |
| 37 | + | ||
| 38 | + | var productionDependencies = map[packageLayer]map[packageLayer]bool{ | |
| 39 | + | layerRoot: {layerApp: true}, | |
| 40 | + | layerCommand: {layerPkg: true}, | |
| 41 | + | layerScript: {layerAdapter: true, layerDomain: true, layerPorts: true, layerProtocol: true, layerCatalogue: true, layerWire: true, layerPkg: true}, | |
| 42 | + | layerApp: {layerAdapter: true, layerDomain: true, layerPorts: true, layerProtocol: true, layerCatalogue: true, layerWire: true, layerUsecase: true, layerPersist: true, layerPlatform: true, layerLogging: true, layerPkg: true}, | |
| 43 | + | layerAdapter: {layerAdapter: true, layerDomain: true, layerPorts: true, layerProtocol: true, layerCatalogue: true, layerWire: true, layerPlatform: true, layerPkg: true}, | |
| 44 | + | layerUsecase: {layerUsecase: true, layerDomain: true, layerPorts: true, layerProtocol: true, layerCatalogue: true}, | |
| 45 | + | layerPorts: {layerDomain: true, layerProtocol: true, layerCatalogue: true}, | |
| 46 | + | layerProtocol: {layerDomain: true}, | |
| 47 | + | layerCatalogue: {layerDomain: true, layerProtocol: true}, | |
| 48 | + | layerWire: {layerDomain: true, layerProtocol: true}, | |
| 49 | + | layerDomain: {layerDomain: true}, | |
| 50 | + | layerPersist: {layerDomain: true, layerPorts: true, layerProtocol: true, layerPkg: true}, | |
| 51 | + | layerPlatform: {}, | |
| 52 | + | layerLogging: {layerPkg: true}, | |
| 53 | + | layerPkg: {layerPkg: true}, | |
| 18 | 54 | } | |
| 19 | 55 | ||
| 20 | 56 | func TestImportBoundaries(t *testing.T) { | |
| 21 | - | rules := []boundaryRule{ | |
| 22 | - | { | |
| 23 | - | name: "pkg must not import internal", | |
| 24 | - | root: "pkg", | |
| 25 | - | disallowedPrefix: "github.com/bnema/vev/internal", | |
| 26 | - | }, | |
| 27 | - | { | |
| 28 | - | name: "usecase must not import adapters", | |
| 29 | - | root: "internal/usecase", | |
| 30 | - | disallowedPrefix: "github.com/bnema/vev/internal/adapters", | |
| 31 | - | }, | |
| 32 | - | { | |
| 33 | - | name: "usecase must not import platform", | |
| 34 | - | root: "internal/usecase", | |
| 35 | - | disallowedPrefix: "github.com/bnema/vev/internal/platform", | |
| 36 | - | }, | |
| 57 | + | violations, packages := inspectRepositoryDependencies(t) | |
| 58 | + | if len(violations) != 0 { | |
| 59 | + | t.Fatalf("invalid package dependencies:\n%s", strings.Join(violations, "\n")) | |
| 60 | + | } | |
| 61 | + | if len(packages) == 0 { | |
| 62 | + | t.Fatal("package inventory is empty") | |
| 37 | 63 | } | |
| 64 | + | } | |
| 38 | 65 | ||
| 39 | - | for _, rule := range rules { | |
| 40 | - | t.Run(rule.name, func(t *testing.T) { | |
| 41 | - | violations := findImportViolations(t, rule.root, rule.disallowedPrefix) | |
| 42 | - | if len(violations) > 0 { | |
| 43 | - | t.Errorf("found disallowed imports of %q under %q:\n%s", rule.disallowedPrefix, rule.root, strings.Join(violations, "\n")) | |
| 66 | + | func TestImportBoundaryNegativeFixtures(t *testing.T) { | |
| 67 | + | tests := []struct { | |
| 68 | + | name, source, target string | |
| 69 | + | testFile bool | |
| 70 | + | want bool | |
| 71 | + | }{ | |
| 72 | + | {"usecase accepts semantic protocol", modulePath + "/internal/usecase/client", modulePath + "/internal/protocol", false, true}, | |
| 73 | + | {"usecase rejects wire", modulePath + "/internal/usecase/daemon", modulePath + "/internal/protocol/wire", false, false}, | |
| 74 | + | {"usecase rejects adapter", modulePath + "/internal/usecase/client", modulePath + "/internal/adapters/ipc", false, false}, | |
| 75 | + | {"domain rejects ports", modulePath + "/internal/domain", modulePath + "/internal/ports", false, false}, | |
| 76 | + | {"pkg rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", false, false}, | |
| 77 | + | {"snapshot adapter exception", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/usecase/snapshot", false, true}, | |
| 78 | + | {"other adapter rejects usecase", modulePath + "/internal/adapters/ipc", modulePath + "/internal/usecase/client", false, false}, | |
| 79 | + | {"usecase test may use wire fixture", modulePath + "/internal/usecase/client", modulePath + "/internal/protocol/wire", true, true}, | |
| 80 | + | {"pkg test still rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", true, false}, | |
| 81 | + | } | |
| 82 | + | for _, tt := range tests { | |
| 83 | + | t.Run(tt.name, func(t *testing.T) { | |
| 84 | + | got, err := dependencyAllowed(tt.source, tt.target, tt.testFile) | |
| 85 | + | if err != nil { | |
| 86 | + | t.Fatalf("dependencyAllowed: %v", err) | |
| 87 | + | } | |
| 88 | + | if got != tt.want { | |
| 89 | + | t.Fatalf("dependencyAllowed(%q, %q, test=%t) = %t, want %t", tt.source, tt.target, tt.testFile, got, tt.want) | |
| 44 | 90 | } | |
| 45 | 91 | }) | |
| 46 | 92 | } | |
| 47 | 93 | } | |
| 48 | 94 | ||
| 49 | - | // findImportViolations walks root, parses each .go file's imports, and | |
| 50 | - | // returns a description for every import whose path starts with | |
| 51 | - | // disallowedPrefix. | |
| 52 | - | func findImportViolations(t *testing.T, root, disallowedPrefix string) []string { | |
| 95 | + | func inspectRepositoryDependencies(t *testing.T) ([]string, map[string]struct{}) { | |
| 53 | 96 | t.Helper() | |
| 54 | - | ||
| 55 | 97 | var violations []string | |
| 98 | + | packages := make(map[string]struct{}) | |
| 56 | 99 | fset := token.NewFileSet() | |
| 57 | - | ||
| 58 | - | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { | |
| 59 | - | if err != nil { | |
| 60 | - | return err | |
| 100 | + | err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, walkErr error) error { | |
| 101 | + | if walkErr != nil { | |
| 102 | + | return walkErr | |
| 61 | 103 | } | |
| 62 | - | if d.IsDir() || !strings.HasSuffix(path, ".go") { | |
| 104 | + | if entry.IsDir() { | |
| 105 | + | name := entry.Name() | |
| 106 | + | if name == ".git" || name == ".worktrees" || name == "vendor" { | |
| 107 | + | return filepath.SkipDir | |
| 108 | + | } | |
| 63 | 109 | return nil | |
| 64 | 110 | } | |
| 65 | - | ||
| 66 | - | f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) | |
| 111 | + | if !strings.HasSuffix(path, ".go") { | |
| 112 | + | return nil | |
| 113 | + | } | |
| 114 | + | source := importPathForFile(path) | |
| 115 | + | packages[source] = struct{}{} | |
| 116 | + | if _, err := classifyPackage(source); err != nil { | |
| 117 | + | violations = append(violations, path+": "+err.Error()) | |
| 118 | + | return nil | |
| 119 | + | } | |
| 120 | + | file, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) | |
| 67 | 121 | if err != nil { | |
| 68 | 122 | return err | |
| 69 | 123 | } | |
| 70 | - | ||
| 71 | - | for _, imp := range f.Imports { | |
| 72 | - | importPath := strings.Trim(imp.Path.Value, `"`) | |
| 73 | - | if strings.HasPrefix(importPath, disallowedPrefix) { | |
| 74 | - | violations = append(violations, path+": imports "+importPath) | |
| 124 | + | testFile := strings.HasSuffix(path, "_test.go") | |
| 125 | + | for _, spec := range file.Imports { | |
| 126 | + | target, err := strconv.Unquote(spec.Path.Value) | |
| 127 | + | if err != nil || (target != modulePath && !strings.HasPrefix(target, modulePath+"/")) { | |
| 128 | + | continue | |
| 129 | + | } | |
| 130 | + | allowed, err := dependencyAllowed(source, target, testFile) | |
| 131 | + | if err != nil { | |
| 132 | + | violations = append(violations, fmt.Sprintf("%s: %v", path, err)) | |
| 133 | + | continue | |
| 134 | + | } | |
| 135 | + | if !allowed { | |
| 136 | + | violations = append(violations, fmt.Sprintf("%s: %s may not import %s", path, source, target)) | |
| 75 | 137 | } | |
| 76 | 138 | } | |
| 77 | 139 | return nil | |
| 78 | 140 | }) | |
| 79 | 141 | if err != nil { | |
| 80 | - | t.Fatalf("walking %s: %v", root, err) | |
| 142 | + | t.Fatalf("walking repository: %v", err) | |
| 81 | 143 | } | |
| 144 | + | sort.Strings(violations) | |
| 145 | + | return violations, packages | |
| 146 | + | } | |
| 82 | 147 | ||
| 83 | - | return violations | |
| 148 | + | func dependencyAllowed(source, target string, testFile bool) (bool, error) { | |
| 149 | + | sourceLayer, err := classifyPackage(source) | |
| 150 | + | if err != nil { | |
| 151 | + | return false, err | |
| 152 | + | } | |
| 153 | + | targetLayer, err := classifyPackage(target) | |
| 154 | + | if err != nil { | |
| 155 | + | return false, err | |
| 156 | + | } | |
| 157 | + | if testFile { | |
| 158 | + | return sourceLayer != layerPkg, nil | |
| 159 | + | } | |
| 160 | + | if sourceLayer == layerTestSupport { | |
| 161 | + | return testSupportDependencyAllowed(source, targetLayer), nil | |
| 162 | + | } | |
| 163 | + | if source == modulePath+"/internal/adapters/snapshot" && target == modulePath+"/internal/usecase/snapshot" { | |
| 164 | + | return true, nil | |
| 165 | + | } | |
| 166 | + | return productionDependencies[sourceLayer][targetLayer], nil | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func testSupportDependencyAllowed(source string, target packageLayer) bool { | |
| 170 | + | switch { | |
| 171 | + | case source == modulePath+"/internal/ports/mocks": | |
| 172 | + | return target == layerPorts || target == layerDomain || target == layerProtocol || target == layerCatalogue | |
| 173 | + | case source == modulePath+"/internal/protocol/wire/mocks": | |
| 174 | + | return target == layerWire | |
| 175 | + | case strings.HasPrefix(source, modulePath+"/internal/testutil"): | |
| 176 | + | return target == layerDomain || target == layerProtocol || target == layerWire | |
| 177 | + | default: | |
| 178 | + | return false | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func classifyPackage(path string) (packageLayer, error) { | |
| 183 | + | switch { | |
| 184 | + | case path == modulePath: | |
| 185 | + | return layerRoot, nil | |
| 186 | + | case strings.HasPrefix(path, modulePath+"/cmd/"): | |
| 187 | + | return layerCommand, nil | |
| 188 | + | case strings.HasPrefix(path, modulePath+"/scripts/"): | |
| 189 | + | return layerScript, nil | |
| 190 | + | case path == modulePath+"/internal/app": | |
| 191 | + | return layerApp, nil | |
| 192 | + | case strings.HasPrefix(path, modulePath+"/internal/adapters/"): | |
| 193 | + | return layerAdapter, nil | |
| 194 | + | case path == modulePath+"/internal/ports/mocks", path == modulePath+"/internal/protocol/wire/mocks", strings.HasPrefix(path, modulePath+"/internal/testutil"): | |
| 195 | + | return layerTestSupport, nil | |
| 196 | + | case path == modulePath+"/internal/ports": | |
| 197 | + | return layerPorts, nil | |
| 198 | + | case path == modulePath+"/internal/protocol/wire": | |
| 199 | + | return layerWire, nil | |
| 200 | + | case path == modulePath+"/internal/protocol/catalogue": | |
| 201 | + | return layerCatalogue, nil | |
| 202 | + | case path == modulePath+"/internal/protocol": | |
| 203 | + | return layerProtocol, nil | |
| 204 | + | case path == modulePath+"/internal/domain" || strings.HasPrefix(path, modulePath+"/internal/domain/"): | |
| 205 | + | return layerDomain, nil | |
| 206 | + | case path == modulePath+"/internal/usecase" || strings.HasPrefix(path, modulePath+"/internal/usecase/"): | |
| 207 | + | return layerUsecase, nil | |
| 208 | + | case path == modulePath+"/internal/persist": | |
| 209 | + | return layerPersist, nil | |
| 210 | + | case path == modulePath+"/internal/platform": | |
| 211 | + | return layerPlatform, nil | |
| 212 | + | case path == modulePath+"/internal/logging": | |
| 213 | + | return layerLogging, nil | |
| 214 | + | case path == modulePath+"/pkg" || strings.HasPrefix(path, modulePath+"/pkg/"): | |
| 215 | + | return layerPkg, nil | |
| 216 | + | default: | |
| 217 | + | return "", fmt.Errorf("unclassified package %q", path) | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | func importPathForFile(path string) string { | |
| 222 | + | dir := filepath.ToSlash(filepath.Dir(path)) | |
| 223 | + | if dir == "." { | |
| 224 | + | return modulePath | |
| 225 | + | } | |
| 226 | + | return modulePath + "/" + strings.TrimPrefix(dir, "./") | |
| 84 | 227 | } | |
A docs/architecture.md +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | # Architecture | |
| 2 | + | ||
| 3 | + | vev uses a hexagonal core with typed session messages at the client and daemon boundary. Binary framing and carriage selection stay outside the use cases. | |
| 4 | + | ||
| 5 | + | ## Package ownership | |
| 6 | + | ||
| 7 | + | - `internal/domain`: pure shared values and invariants. `domain/terminalcap` owns terminal capability values and environment detection policy. | |
| 8 | + | - `internal/protocol`: typed, transport-neutral client/daemon messages, protocol version, semantic validation, and handshake policy. | |
| 9 | + | - `internal/protocol/catalogue`: independently versioned remote discovery JSON schema and bounded validation. | |
| 10 | + | - `internal/protocol/wire`: message IDs, raw frames, strict codecs, compression, encoded limits, and raw `Transport`, `Dialer`, and `Listener` contracts. | |
| 11 | + | - `internal/ports`: application-facing interfaces and the values required by those interfaces. It contains no codecs, raw frames, environment policy, or worker implementations. | |
| 12 | + | - `internal/usecase`: client, daemon, and supporting application behavior. Production use cases may consume semantic protocol packages but never `protocol/wire` or concrete adapters. | |
| 13 | + | - `internal/adapters/sessionwire`: translates between typed session connections and raw wire transports, including direction checks and decode-failure classification. | |
| 14 | + | - `internal/adapters`: IPC, UDP, SSH stdio, PTY, terminal, persistence-facing, and observability implementations. | |
| 15 | + | - `internal/app`: CLI parsing and composition. It selects local or remote carriage, wraps raw dialers with `sessionwire`, and injects typed ports into use cases. | |
| 16 | + | - `pkg`: reusable packages that never import `internal`. | |
| 17 | + | ||
| 18 | + | ## Dependency direction | |
| 19 | + | ||
| 20 | + | ```text | |
| 21 | + | main → app → usecase → ports | |
| 22 | + | │ │ | |
| 23 | + | └────────→ protocol ← catalogue | |
| 24 | + | ↑ | |
| 25 | + | protocol/wire | |
| 26 | + | ↑ | |
| 27 | + | carriage adapters | |
| 28 | + | ``` | |
| 29 | + | ||
| 30 | + | `internal/app` may import every layer to compose the process. Adapters depend inward on ports, protocol, and domain. The existing `adapters/snapshot → usecase/snapshot` dependency is an explicit exception until the snapshot codec has its own owner. Production dependency rules and the separate test-import policy are executable in `boundary_test.go`. | |
| 31 | + | ||
| 32 | + | ## Session composition | |
| 33 | + | ||
| 34 | + | ```text | |
| 35 | + | client or daemon use case | |
| 36 | + | ↕ ports.ClientConnection / ports.ServerConnection | |
| 37 | + | adapters/sessionwire | |
| 38 | + | ↕ protocol/wire.Transport carrying wire.Frame | |
| 39 | + | IPC, UDP, or SSH stdio adapter | |
| 40 | + | ``` | |
| 41 | + | ||
| 42 | + | Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values. `sessionwire` alone maps those values to message IDs and payload codecs. Blind proxies may forward raw frames, and the UDP adapter may inspect bounded frame classification for QoS, but neither path exposes bytes to a use case. | |
| 43 | + | ||
| 44 | + | ## Adding code | |
| 45 | + | ||
| 46 | + | - Add application behavior to a use case and define any cross-layer interface in `internal/ports`. | |
| 47 | + | - Add transport-neutral session meaning to `internal/protocol`. | |
| 48 | + | - Add remote catalogue schema fields and validation to `internal/protocol/catalogue`. | |
| 49 | + | - Add message IDs, binary layouts, strict decoding, compression, or raw carriage contracts to `internal/protocol/wire`. | |
| 50 | + | - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`. | |
| 51 | + | - Keep protocol version `37` unless a negotiated wire layout changes. | |
M internal/adapters/dgram/proxy.go +5 -5
| @@ -17,8 +17,8 @@ const ( | |||
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | type ProxyRuntime struct { | |
| 20 | - | Client ports.Transport | |
| 21 | - | Daemon ports.Transport | |
| 20 | + | Client wire.Transport | |
| 21 | + | Daemon wire.Transport | |
| 22 | 22 | Log *slog.Logger | |
| 23 | 23 | IdleTTL time.Duration | |
| 24 | 24 | RetryBackoff time.Duration | |
| @@ -47,8 +47,8 @@ type proxyCopier struct { | |||
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | 49 | type proxyCopyDirection struct { | |
| 50 | - | src ports.Transport | |
| 51 | - | dst ports.Transport | |
| 50 | + | src wire.Transport | |
| 51 | + | dst wire.Transport | |
| 52 | 52 | recvKind proxyCopyErrKind | |
| 53 | 53 | sendKind proxyCopyErrKind | |
| 54 | 54 | transform func(wire.Frame) wire.Frame | |
| @@ -191,7 +191,7 @@ func (c proxyCopier) copyFrames(dir proxyCopyDirection) { | |||
| 191 | 191 | } | |
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | - | func recoverableClientLink(t ports.Transport, err error) bool { | |
| 194 | + | func recoverableClientLink(t wire.Transport, err error) bool { | |
| 195 | 195 | if errors.Is(err, ErrLinkDead) { | |
| 196 | 196 | return false | |
| 197 | 197 | } | |
M internal/adapters/dgram/ssh_dialer.go +2 -1
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | ||
| 16 | 16 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 17 | 17 | "github.com/bnema/vev/internal/ports" | |
| 18 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 18 | 19 | pdgram "github.com/bnema/vev/pkg/dgram" | |
| 19 | 20 | ) | |
| 20 | 21 | ||
| @@ -112,7 +113,7 @@ func NewRemoteDialerWithLogger(target, _ string, log *slog.Logger) RemoteDialer | |||
| 112 | 113 | return RemoteDialer{Target: target, ProbeTimeout: defaultProbeTimeout, Log: log} | |
| 113 | 114 | } | |
| 114 | 115 | ||
| 115 | - | func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) { | |
| 116 | + | func (d RemoteDialer) Dial(ctx context.Context) (wire.Transport, error) { | |
| 116 | 117 | if err := ctx.Err(); err != nil { | |
| 117 | 118 | return nil, err | |
| 118 | 119 | } | |
M internal/adapters/dgram/transport.go +2 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | // Package dgram adapts authenticated UDP-style packet connections to ports.Transport. | |
| 1 | + | // Package dgram adapts authenticated UDP-style packet connections to wire.Transport. | |
| 2 | 2 | package dgram | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| @@ -136,7 +136,7 @@ func normalizeOptions(opts Options) Options { | |||
| 136 | 136 | return opts | |
| 137 | 137 | } | |
| 138 | 138 | ||
| 139 | - | var _ ports.DatagramTransport = (*Transport)(nil) | |
| 139 | + | var _ wire.DatagramTransport = (*Transport)(nil) | |
| 140 | 140 | ||
| 141 | 141 | type Transport struct { | |
| 142 | 142 | pc net.PacketConn | |
M internal/adapters/dgram/transport_observability_test.go +2 -2
| @@ -28,7 +28,7 @@ func TestTransportObservabilityDgramFailedCloseEndsReceiveAndSend(t *testing.T) | |||
| 28 | 28 | } | |
| 29 | 29 | receiveStarted := make(chan struct{}) | |
| 30 | 30 | signalingObserver := &dgramStartObserver{RuntimeObserver: observer, receiveStarted: receiveStarted} | |
| 31 | - | reporter := ports.NewSerializedRuntimeObserver(signalingObserver, 64) | |
| 31 | + | reporter := observability.NewSerialized(signalingObserver, 64) | |
| 32 | 32 | defer reporter.Close() | |
| 33 | 33 | transport, err := NewTransportWithOptions(&dgramCloseFailPC{fakePC: aPC, err: closeErr}, testAddr("b"), key(), 1, 2, Options{Clock: behaviorClock, ResendAfter: time.Hour, Heartbeat: time.Hour}, WithRuntimeObserver(reporter)) | |
| 34 | 34 | if err != nil { | |
| @@ -142,7 +142,7 @@ func TestTransportObservabilityDgramKeepsBehaviorClockSeparate(t *testing.T) { | |||
| 142 | 142 | if err != nil { | |
| 143 | 143 | t.Fatalf("NewJSONL() error = %v", err) | |
| 144 | 144 | } | |
| 145 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 145 | + | reporter := observability.NewSerialized(observer, 64) | |
| 146 | 146 | defer reporter.Close() | |
| 147 | 147 | ||
| 148 | 148 | a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{Clock: behaviorClock, ResendAfter: time.Hour, Heartbeat: time.Hour}, WithRuntimeObserver(reporter)) | |
M internal/adapters/ipc/dial.go +2 -2
| @@ -6,14 +6,14 @@ import ( | |||
| 6 | 6 | "net" | |
| 7 | 7 | "path/filepath" | |
| 8 | 8 | ||
| 9 | - | "github.com/bnema/vev/internal/ports" | |
| 9 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | // DialContext connects to the daemon's socket in dir (dir/daemon.sock) and | |
| 13 | 13 | // returns a Transport speaking vev's framed protocol. A dial failure (no daemon | |
| 14 | 14 | // listening, or a stale/absent socket) is reported as a wrapped error so | |
| 15 | 15 | // callers can decide whether to spawn a daemon and retry. | |
| 16 | - | func DialContext(ctx context.Context, dir string, opts ...Option) (ports.Transport, error) { | |
| 16 | + | func DialContext(ctx context.Context, dir string, opts ...Option) (wire.Transport, error) { | |
| 17 | 17 | sockPath := filepath.Join(dir, socketFileName) | |
| 18 | 18 | var dialer net.Dialer | |
| 19 | 19 | conn, err := dialer.DialContext(ctx, "unix", sockPath) | |
M internal/adapters/ipc/dial_test.go +2 -1
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | "time" | |
| 8 | 8 | ||
| 9 | + | "github.com/bnema/vev/internal/adapters/observability" | |
| 9 | 10 | "github.com/bnema/vev/internal/ports" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -61,7 +62,7 @@ func TestDialContextPreservesOptions(t *testing.T) { | |||
| 61 | 62 | } | |
| 62 | 63 | defer func() { _ = listener.Close() }() | |
| 63 | 64 | ||
| 64 | - | observer := ports.NewSerializedRuntimeObserver(dialTestObserver{}, 1) | |
| 65 | + | observer := observability.NewSerialized(dialTestObserver{}, 1) | |
| 65 | 66 | defer observer.Close() | |
| 66 | 67 | transport, err := DialContext(context.Background(), dir, WithRuntimeObserver(observer)) | |
| 67 | 68 | if err != nil { | |
M internal/adapters/ipc/listener.go +5 -5
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | "path/filepath" | |
| 9 | 9 | "syscall" | |
| 10 | 10 | ||
| 11 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 12 | 12 | "github.com/bnema/vev/pkg/safedir" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -20,7 +20,7 @@ const socketFileName = "daemon.sock" | |||
| 20 | 20 | // bound by a live daemon (a dial-probe against it succeeded). | |
| 21 | 21 | var ErrDaemonRunning = errors.New("ipc: a daemon is already listening on this socket") | |
| 22 | 22 | ||
| 23 | - | // unixListener implements ports.Listener over an AF_UNIX SOCK_STREAM | |
| 23 | + | // unixListener implements wire.Listener over an AF_UNIX SOCK_STREAM | |
| 24 | 24 | // listener. | |
| 25 | 25 | type unixListener struct { | |
| 26 | 26 | ln *net.UnixListener | |
| @@ -36,7 +36,7 @@ type unixListener struct { | |||
| 36 | 36 | // the previous owner died without cleaning up, so the stale socket file is | |
| 37 | 37 | // unlinked and bind is retried once. A successful dial means a live daemon | |
| 38 | 38 | // owns the socket, and Listen returns ErrDaemonRunning. | |
| 39 | - | func Listen(dir string, opts ...Option) (ports.Listener, error) { | |
| 39 | + | func Listen(dir string, opts ...Option) (wire.Listener, error) { | |
| 40 | 40 | if err := safedir.EnsurePrivate(dir); err != nil { | |
| 41 | 41 | return nil, fmt.Errorf("ipc: securing socket directory: %w", err) | |
| 42 | 42 | } | |
| @@ -99,8 +99,8 @@ func probeLiveDaemon(sockPath string) error { | |||
| 99 | 99 | } | |
| 100 | 100 | ||
| 101 | 101 | // Accept waits for and returns the next connection, wrapped as a | |
| 102 | - | // ports.Transport. | |
| 103 | - | func (l *unixListener) Accept() (ports.Transport, error) { | |
| 102 | + | // wire.Transport. | |
| 103 | + | func (l *unixListener) Accept() (wire.Transport, error) { | |
| 104 | 104 | conn, err := l.ln.Accept() | |
| 105 | 105 | if err != nil { | |
| 106 | 106 | return nil, err | |
M internal/adapters/ipc/transport.go +3 -3
| @@ -35,7 +35,7 @@ var errClosed = errors.New("ipc: closed") | |||
| 35 | 35 | // peer into an unbounded memory commitment. | |
| 36 | 36 | const sendQueueCapacity = 8 | |
| 37 | 37 | ||
| 38 | - | // unixTransport implements ports.Transport over a net.Conn (in practice an | |
| 38 | + | // unixTransport implements wire.Transport over a net.Conn (in practice an | |
| 39 | 39 | // AF_UNIX SOCK_STREAM connection, but any net.Conn works — this also lets | |
| 40 | 40 | // tests exercise it over net.Pipe). | |
| 41 | 41 | // | |
| @@ -79,9 +79,9 @@ type unixTransport struct { | |||
| 79 | 79 | readBuf []byte | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | - | // NewTransport wraps conn as a ports.Transport speaking vev's framed | |
| 82 | + | // NewTransport wraps conn as a wire.Transport speaking vev's framed | |
| 83 | 83 | // binary protocol. | |
| 84 | - | func NewTransport(conn net.Conn, opts ...Option) ports.Transport { | |
| 84 | + | func NewTransport(conn net.Conn, opts ...Option) wire.Transport { | |
| 85 | 85 | t := &unixTransport{ | |
| 86 | 86 | conn: conn, | |
| 87 | 87 | egress: make(chan sendRequest, sendQueueCapacity), | |
M internal/adapters/ipc/transport_observability_test.go +2 -2
| @@ -21,7 +21,7 @@ func TestTransportObservabilityIPCEOFAndCloseEndFailedSpans(t *testing.T) { | |||
| 21 | 21 | if err != nil { | |
| 22 | 22 | t.Fatalf("NewJSONL() error = %v", err) | |
| 23 | 23 | } | |
| 24 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 24 | + | reporter := observability.NewSerialized(observer, 64) | |
| 25 | 25 | defer reporter.Close() | |
| 26 | 26 | ||
| 27 | 27 | // EOF from the peer and a locally closed blocked receive are distinct | |
| @@ -145,7 +145,7 @@ func TestTransportObservabilityIPCMarksCarriageWithoutChangingBytes(t *testing.T | |||
| 145 | 145 | if err != nil { | |
| 146 | 146 | t.Fatalf("NewJSONL() error = %v", err) | |
| 147 | 147 | } | |
| 148 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 148 | + | reporter := observability.NewSerialized(observer, 64) | |
| 149 | 149 | defer reporter.Close() | |
| 150 | 150 | ||
| 151 | 151 | left, right := net.Pipe() | |
M internal/adapters/ipc/transport_test.go +6 -7
| @@ -12,7 +12,6 @@ import ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/vev/internal/domain" | |
| 15 | - | "github.com/bnema/vev/internal/ports" | |
| 16 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 17 | 16 | "github.com/bnema/vev/internal/protocol/wire" | |
| 18 | 17 | ) | |
| @@ -253,7 +252,7 @@ func TestTransportAsyncEgressPreservesWelcomeBeforeOutput(t *testing.T) { | |||
| 253 | 252 | defer closeTransport(t, server) | |
| 254 | 253 | client := NewTransport(clientConn) | |
| 255 | 254 | defer closeTransport(t, client) | |
| 256 | - | async, ok := server.(ports.AsyncTransport) | |
| 255 | + | async, ok := server.(wire.AsyncTransport) | |
| 257 | 256 | if !ok { | |
| 258 | 257 | t.Fatal("IPC transport does not implement AsyncTransport") | |
| 259 | 258 | } | |
| @@ -317,7 +316,7 @@ func TestTransportSendWaitsForEgressCapacity(t *testing.T) { | |||
| 317 | 316 | } | |
| 318 | 317 | client := NewTransport(clientConn) | |
| 319 | 318 | defer closeTransport(t, client) | |
| 320 | - | async, ok := server.(ports.AsyncTransport) | |
| 319 | + | async, ok := server.(wire.AsyncTransport) | |
| 321 | 320 | if !ok { | |
| 322 | 321 | t.Fatal("IPC transport does not implement AsyncTransport") | |
| 323 | 322 | } | |
| @@ -365,7 +364,7 @@ func TestTransportCloseInterruptsSendWaitingForEgressCapacity(t *testing.T) { | |||
| 365 | 364 | if !ok { | |
| 366 | 365 | t.Fatal("IPC transport is not a unixTransport") | |
| 367 | 366 | } | |
| 368 | - | async, ok := server.(ports.AsyncTransport) | |
| 367 | + | async, ok := server.(wire.AsyncTransport) | |
| 369 | 368 | if !ok { | |
| 370 | 369 | t.Fatal("IPC transport does not implement AsyncTransport") | |
| 371 | 370 | } | |
| @@ -407,7 +406,7 @@ func waitForEgressSender(t *testing.T, transport *unixTransport, timeout time.Du | |||
| 407 | 406 | } | |
| 408 | 407 | } | |
| 409 | 408 | ||
| 410 | - | func closeTransport(t *testing.T, transport ports.Transport) { | |
| 409 | + | func closeTransport(t *testing.T, transport wire.Transport) { | |
| 411 | 410 | t.Helper() | |
| 412 | 411 | ||
| 413 | 412 | closed := make(chan error, 1) | |
| @@ -422,7 +421,7 @@ func closeTransport(t *testing.T, transport ports.Transport) { | |||
| 422 | 421 | } | |
| 423 | 422 | } | |
| 424 | 423 | ||
| 425 | - | func fillAsyncEgress(t *testing.T, transport *unixTransport, async ports.AsyncTransport, timeout time.Duration) []wire.Frame { | |
| 424 | + | func fillAsyncEgress(t *testing.T, transport *unixTransport, async wire.AsyncTransport, timeout time.Duration) []wire.Frame { | |
| 426 | 425 | t.Helper() | |
| 427 | 426 | ||
| 428 | 427 | first := wire.Frame{Type: wire.MsgOutput, Payload: []byte{0}} | |
| @@ -474,7 +473,7 @@ func TestTransportAsyncEgressIsBoundedAndCloseInterruptsWorkers(t *testing.T) { | |||
| 474 | 473 | if !ok { | |
| 475 | 474 | t.Fatal("IPC transport is not a unixTransport") | |
| 476 | 475 | } | |
| 477 | - | async, ok := tr.(ports.AsyncTransport) | |
| 476 | + | async, ok := tr.(wire.AsyncTransport) | |
| 478 | 477 | if !ok { | |
| 479 | 478 | t.Fatal("IPC transport does not implement AsyncTransport") | |
| 480 | 479 | } | |
A internal/adapters/observability/serialized.go +136 -0
| @@ -0,0 +1,136 @@ | |||
| 1 | + | package observability | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/vev/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // NewSerialized isolates producers from a potentially slow observer with one | |
| 10 | + | // ordered worker and a bounded queue. | |
| 11 | + | func NewSerialized(observer ports.RuntimeObserver, capacity int) ports.SerializedRuntimeObserver { | |
| 12 | + | if observer == nil { | |
| 13 | + | return nil | |
| 14 | + | } | |
| 15 | + | if capacity < 1 { | |
| 16 | + | capacity = 1 | |
| 17 | + | } | |
| 18 | + | o := &serializedRuntimeObserver{observer: observer, capacity: capacity, done: make(chan struct{})} | |
| 19 | + | o.cond = sync.NewCond(&o.mu) | |
| 20 | + | go o.run() | |
| 21 | + | return o | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // EnsureSerialized reuses an existing serialized owner or creates one. | |
| 25 | + | func EnsureSerialized(observer ports.RuntimeObserver, capacity int) (serialized ports.SerializedRuntimeObserver, owned bool) { | |
| 26 | + | if observer == nil { | |
| 27 | + | return nil, false | |
| 28 | + | } | |
| 29 | + | if serialized, ok := observer.(ports.SerializedRuntimeObserver); ok { | |
| 30 | + | return serialized, false | |
| 31 | + | } | |
| 32 | + | return NewSerialized(observer, capacity), true | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | type serializedRuntimeObserver struct { | |
| 36 | + | observer ports.RuntimeObserver | |
| 37 | + | capacity int | |
| 38 | + | ||
| 39 | + | mu sync.Mutex | |
| 40 | + | cond *sync.Cond | |
| 41 | + | marks []ports.RuntimeMark | |
| 42 | + | dropped uint64 | |
| 43 | + | closed bool | |
| 44 | + | active bool | |
| 45 | + | done chan struct{} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (o *serializedRuntimeObserver) ObserveRuntime(mark ports.RuntimeMark) { | |
| 49 | + | if o == nil { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | o.mu.Lock() | |
| 53 | + | defer o.mu.Unlock() | |
| 54 | + | if o.closed { | |
| 55 | + | return | |
| 56 | + | } | |
| 57 | + | if o.dropped != 0 { | |
| 58 | + | if len(o.marks)+2 > o.capacity { | |
| 59 | + | o.dropped++ | |
| 60 | + | return | |
| 61 | + | } | |
| 62 | + | o.marks = append(o.marks, gapMark(o.dropped), mark) | |
| 63 | + | o.dropped = 0 | |
| 64 | + | o.cond.Signal() | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | if len(o.marks) == o.capacity { | |
| 68 | + | o.dropped = 1 | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | o.marks = append(o.marks, mark) | |
| 72 | + | o.cond.Signal() | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func gapMark(dropped uint64) ports.RuntimeMark { | |
| 76 | + | return ports.NewRuntimeMark("observer", ports.RuntimeTransportDiagnostic, dropped, false) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func (o *serializedRuntimeObserver) Flush() { | |
| 80 | + | if o == nil { | |
| 81 | + | return | |
| 82 | + | } | |
| 83 | + | o.mu.Lock() | |
| 84 | + | for len(o.marks) != 0 || o.active || o.dropped != 0 { | |
| 85 | + | o.cond.Signal() | |
| 86 | + | o.cond.Wait() | |
| 87 | + | } | |
| 88 | + | closed := o.closed | |
| 89 | + | o.mu.Unlock() | |
| 90 | + | if closed { | |
| 91 | + | <-o.done | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | func (o *serializedRuntimeObserver) Close() { | |
| 96 | + | if o == nil { | |
| 97 | + | return | |
| 98 | + | } | |
| 99 | + | o.mu.Lock() | |
| 100 | + | if !o.closed { | |
| 101 | + | o.closed = true | |
| 102 | + | o.cond.Broadcast() | |
| 103 | + | } | |
| 104 | + | o.mu.Unlock() | |
| 105 | + | <-o.done | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func (o *serializedRuntimeObserver) run() { | |
| 109 | + | defer close(o.done) | |
| 110 | + | for { | |
| 111 | + | o.mu.Lock() | |
| 112 | + | for len(o.marks) == 0 && o.dropped == 0 && !o.closed { | |
| 113 | + | o.cond.Wait() | |
| 114 | + | } | |
| 115 | + | if len(o.marks) == 0 && o.dropped != 0 { | |
| 116 | + | o.marks = append(o.marks, gapMark(o.dropped)) | |
| 117 | + | o.dropped = 0 | |
| 118 | + | } | |
| 119 | + | if len(o.marks) == 0 && o.closed { | |
| 120 | + | o.cond.Broadcast() | |
| 121 | + | o.mu.Unlock() | |
| 122 | + | return | |
| 123 | + | } | |
| 124 | + | mark := o.marks[0] | |
| 125 | + | o.marks = o.marks[1:] | |
| 126 | + | o.active = true | |
| 127 | + | o.mu.Unlock() | |
| 128 | + | ||
| 129 | + | o.observer.ObserveRuntime(mark) | |
| 130 | + | ||
| 131 | + | o.mu.Lock() | |
| 132 | + | o.active = false | |
| 133 | + | o.cond.Broadcast() | |
| 134 | + | o.mu.Unlock() | |
| 135 | + | } | |
| 136 | + | } | |
A internal/adapters/observability/serialized_test.go +77 -0
| @@ -0,0 +1,77 @@ | |||
| 1 | + | package observability | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/vev/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | type runtimeObserverFunc func(ports.RuntimeMark) | |
| 12 | + | ||
| 13 | + | func (f runtimeObserverFunc) ObserveRuntime(mark ports.RuntimeMark) { f(mark) } | |
| 14 | + | ||
| 15 | + | func TestSerializedFlushesInOrderAndCloses(t *testing.T) { | |
| 16 | + | var mu sync.Mutex | |
| 17 | + | var got []ports.RuntimeMark | |
| 18 | + | observer := NewSerialized(runtimeObserverFunc(func(mark ports.RuntimeMark) { | |
| 19 | + | mu.Lock() | |
| 20 | + | got = append(got, mark) | |
| 21 | + | mu.Unlock() | |
| 22 | + | }), 2) | |
| 23 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeEmitStart, 1, true)) | |
| 24 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeEmitEnd, 1, true)) | |
| 25 | + | observer.Flush() | |
| 26 | + | observer.Close() | |
| 27 | + | observer.Close() | |
| 28 | + | ||
| 29 | + | mu.Lock() | |
| 30 | + | defer mu.Unlock() | |
| 31 | + | if len(got) != 2 || got[0].Kind != ports.RuntimeEmitStart || got[1].Kind != ports.RuntimeEmitEnd { | |
| 32 | + | t.Fatalf("serialized marks = %#v", got) | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func TestEnsureSerializedReusesExistingOwner(t *testing.T) { | |
| 37 | + | existing := NewSerialized(runtimeObserverFunc(func(ports.RuntimeMark) {}), 1) | |
| 38 | + | reporter, owned := EnsureSerialized(existing, 1) | |
| 39 | + | if owned || reporter != existing { | |
| 40 | + | t.Fatalf("EnsureSerialized() = (%T, %t), want existing unowned reporter", reporter, owned) | |
| 41 | + | } | |
| 42 | + | existing.Close() | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func TestSerializedReportsBoundedQueueLoss(t *testing.T) { | |
| 46 | + | entered := make(chan struct{}) | |
| 47 | + | release := make(chan struct{}) | |
| 48 | + | var mu sync.Mutex | |
| 49 | + | var got []ports.RuntimeMark | |
| 50 | + | observer := NewSerialized(runtimeObserverFunc(func(mark ports.RuntimeMark) { | |
| 51 | + | if mark.Kind == ports.RuntimeEmitStart { | |
| 52 | + | close(entered) | |
| 53 | + | <-release | |
| 54 | + | } | |
| 55 | + | mu.Lock() | |
| 56 | + | got = append(got, mark) | |
| 57 | + | mu.Unlock() | |
| 58 | + | }), 2) | |
| 59 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeEmitStart, 1, true)) | |
| 60 | + | select { | |
| 61 | + | case <-entered: | |
| 62 | + | case <-time.After(time.Second): | |
| 63 | + | t.Fatal("observer did not start") | |
| 64 | + | } | |
| 65 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeComposeStart, 1, true)) | |
| 66 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeComposeEnd, 1, true)) | |
| 67 | + | observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeEmitEnd, 1, true)) | |
| 68 | + | close(release) | |
| 69 | + | observer.Flush() | |
| 70 | + | observer.Close() | |
| 71 | + | ||
| 72 | + | mu.Lock() | |
| 73 | + | defer mu.Unlock() | |
| 74 | + | if len(got) != 4 || got[0].Kind != ports.RuntimeEmitStart || got[1].Kind != ports.RuntimeComposeStart || got[2].Kind != ports.RuntimeComposeEnd || got[3].Kind != ports.RuntimeTransportDiagnostic || got[3].Bytes != 1 || got[3].Valid { | |
| 75 | + | t.Fatalf("bounded-loss marks = %#v", got) | |
| 76 | + | } | |
| 77 | + | } | |
M internal/adapters/remote/catalog.go +42 -41
| @@ -16,8 +16,9 @@ import ( | |||
| 16 | 16 | ||
| 17 | 17 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 18 | 18 | "github.com/bnema/vev/internal/domain" | |
| 19 | - | "github.com/bnema/vev/internal/ports" | |
| 19 | + | appports "github.com/bnema/vev/internal/ports" | |
| 20 | 20 | "github.com/bnema/vev/internal/protocol" | |
| 21 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 21 | 22 | ) | |
| 22 | 23 | ||
| 23 | 24 | const ( | |
| @@ -42,10 +43,10 @@ type CatalogClient struct { | |||
| 42 | 43 | timeout time.Duration | |
| 43 | 44 | } | |
| 44 | 45 | ||
| 45 | - | var _ ports.RemoteCatalogClient = (*CatalogClient)(nil) | |
| 46 | + | var _ appports.RemoteCatalogClient = (*CatalogClient)(nil) | |
| 46 | 47 | ||
| 47 | 48 | // NewCatalogClient returns a RemoteCatalogClient that shells out to ssh. | |
| 48 | - | func NewCatalogClient() ports.RemoteCatalogClient { | |
| 49 | + | func NewCatalogClient() appports.RemoteCatalogClient { | |
| 49 | 50 | return &CatalogClient{command: exec.CommandContext} | |
| 50 | 51 | } | |
| 51 | 52 | ||
| @@ -60,13 +61,13 @@ func (c *CatalogClient) listTimeout() time.Duration { | |||
| 60 | 61 | // exactly one versioned catalog envelope from stdout. List always applies a | |
| 61 | 62 | // bounded command timeout derived from ctx so a caller with no deadline cannot | |
| 62 | 63 | // hang indefinitely, while still honoring caller cancellation. | |
| 63 | - | func (c *CatalogClient) List(ctx context.Context, target string) (ports.RemoteCatalog, error) { | |
| 64 | + | func (c *CatalogClient) List(ctx context.Context, target string) (catalogue.RemoteCatalog, error) { | |
| 64 | 65 | if err := ctx.Err(); err != nil { | |
| 65 | - | return ports.RemoteCatalog{}, err | |
| 66 | + | return catalogue.RemoteCatalog{}, err | |
| 66 | 67 | } | |
| 67 | 68 | if err := domain.ValidateRemoteHostTarget(target); err != nil { | |
| 68 | 69 | slog.Debug("remote catalog rejected target", "target", target, "err", err) | |
| 69 | - | return ports.RemoteCatalog{}, err | |
| 70 | + | return catalogue.RemoteCatalog{}, err | |
| 70 | 71 | } | |
| 71 | 72 | ||
| 72 | 73 | runCtx, cancel := context.WithTimeout(ctx, c.listTimeout()) | |
| @@ -85,29 +86,29 @@ func (c *CatalogClient) List(ctx context.Context, target string) (ports.RemoteCa | |||
| 85 | 86 | cmd.WaitDelay = catalogCommandWaitDelay | |
| 86 | 87 | err := cmd.Run() | |
| 87 | 88 | if ctxErr := runCtx.Err(); ctxErr != nil { | |
| 88 | - | return ports.RemoteCatalog{}, ctxErr | |
| 89 | + | return catalogue.RemoteCatalog{}, ctxErr | |
| 89 | 90 | } | |
| 90 | 91 | if err != nil { | |
| 91 | 92 | if stdout.overflow || stderr.overflow { | |
| 92 | 93 | slog.Debug("remote catalog output too large", "target", target, "stdout_limit", maxCatalogBytes, "stderr_limit", maxCatalogDiagnosticBytes) | |
| 93 | - | return ports.RemoteCatalog{}, errCatalogTooLarge | |
| 94 | + | return catalogue.RemoteCatalog{}, errCatalogTooLarge | |
| 94 | 95 | } | |
| 95 | 96 | stderrText := sanitizeCatalogDiagnostic(string(stderr.Bytes())) | |
| 96 | 97 | slog.Debug("remote catalog ssh failed", "target", target, "err", err, "stderr", stderrText) | |
| 97 | 98 | if stderrText != "" { | |
| 98 | - | return ports.RemoteCatalog{}, fmt.Errorf("%w: %w: %s", errCatalogSSH, err, stderrText) | |
| 99 | + | return catalogue.RemoteCatalog{}, fmt.Errorf("%w: %w: %s", errCatalogSSH, err, stderrText) | |
| 99 | 100 | } | |
| 100 | - | return ports.RemoteCatalog{}, fmt.Errorf("%w: %w", errCatalogSSH, err) | |
| 101 | + | return catalogue.RemoteCatalog{}, fmt.Errorf("%w: %w", errCatalogSSH, err) | |
| 101 | 102 | } | |
| 102 | 103 | if stdout.overflow || stderr.overflow { | |
| 103 | 104 | slog.Debug("remote catalog output too large", "target", target, "stdout_limit", maxCatalogBytes, "stderr_limit", maxCatalogDiagnosticBytes) | |
| 104 | - | return ports.RemoteCatalog{}, errCatalogTooLarge | |
| 105 | + | return catalogue.RemoteCatalog{}, errCatalogTooLarge | |
| 105 | 106 | } | |
| 106 | 107 | ||
| 107 | 108 | catalog, err := decodeRemoteCatalog(stdout.Bytes()) | |
| 108 | 109 | if err != nil { | |
| 109 | 110 | slog.Debug("remote catalog decode failed", "target", target, "err", err) | |
| 110 | - | return ports.RemoteCatalog{}, err | |
| 111 | + | return catalogue.RemoteCatalog{}, err | |
| 111 | 112 | } | |
| 112 | 113 | return catalog, nil | |
| 113 | 114 | } | |
| @@ -125,15 +126,15 @@ type catalogEnvelope struct { | |||
| 125 | 126 | } | |
| 126 | 127 | ||
| 127 | 128 | type catalogSessionEnvelope struct { | |
| 128 | - | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 129 | - | Name *string `json:"name"` | |
| 130 | - | State *ports.RemoteCatalogSessionState `json:"state"` | |
| 131 | - | Ephemeral *bool `json:"ephemeral"` | |
| 132 | - | Tabs *[]catalogTabEnvelope `json:"tabs"` | |
| 133 | - | Attached *bool `json:"attached"` | |
| 134 | - | LastUsedSeq uint64 `json:"last_used_seq,omitempty"` | |
| 135 | - | ActiveTabID string `json:"active_tab_id,omitempty"` | |
| 136 | - | Reason string `json:"reason,omitempty"` | |
| 129 | + | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 130 | + | Name *string `json:"name"` | |
| 131 | + | State *catalogue.RemoteCatalogSessionState `json:"state"` | |
| 132 | + | Ephemeral *bool `json:"ephemeral"` | |
| 133 | + | Tabs *[]catalogTabEnvelope `json:"tabs"` | |
| 134 | + | Attached *bool `json:"attached"` | |
| 135 | + | LastUsedSeq uint64 `json:"last_used_seq,omitempty"` | |
| 136 | + | ActiveTabID string `json:"active_tab_id,omitempty"` | |
| 137 | + | Reason string `json:"reason,omitempty"` | |
| 137 | 138 | } | |
| 138 | 139 | ||
| 139 | 140 | type catalogTabEnvelope struct { | |
| @@ -144,71 +145,71 @@ type catalogTabEnvelope struct { | |||
| 144 | 145 | Attention bool `json:"attention,omitempty"` | |
| 145 | 146 | } | |
| 146 | 147 | ||
| 147 | - | func decodeRemoteCatalog(raw []byte) (ports.RemoteCatalog, error) { | |
| 148 | + | func decodeRemoteCatalog(raw []byte) (catalogue.RemoteCatalog, error) { | |
| 148 | 149 | var version catalogVersionEnvelope | |
| 149 | 150 | if err := decodeCatalogEnvelope(raw, false, &version); err != nil { | |
| 150 | - | return ports.RemoteCatalog{}, err | |
| 151 | + | return catalogue.RemoteCatalog{}, err | |
| 151 | 152 | } | |
| 152 | 153 | if version.ProtocolVersion == nil { | |
| 153 | - | return ports.RemoteCatalog{}, errCatalogRequired | |
| 154 | + | return catalogue.RemoteCatalog{}, errCatalogRequired | |
| 154 | 155 | } | |
| 155 | 156 | if version.SchemaVersion == nil { | |
| 156 | - | return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{ | |
| 157 | + | return catalogue.RemoteCatalog{}, &catalogue.RemoteCatalogVersionMismatchError{ | |
| 157 | 158 | Got: 0, | |
| 158 | - | Want: ports.RemoteCatalogSchemaVersion, | |
| 159 | + | Want: catalogue.RemoteCatalogSchemaVersion, | |
| 159 | 160 | Kind: "catalog", | |
| 160 | 161 | } | |
| 161 | 162 | } | |
| 162 | 163 | if *version.ProtocolVersion != protocol.Version { | |
| 163 | - | return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{ | |
| 164 | + | return catalogue.RemoteCatalog{}, &catalogue.RemoteCatalogVersionMismatchError{ | |
| 164 | 165 | Got: *version.ProtocolVersion, | |
| 165 | 166 | Want: protocol.Version, | |
| 166 | 167 | Kind: "protocol", | |
| 167 | 168 | } | |
| 168 | 169 | } | |
| 169 | - | if *version.SchemaVersion != ports.RemoteCatalogSchemaVersion { | |
| 170 | - | return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{ | |
| 170 | + | if *version.SchemaVersion != catalogue.RemoteCatalogSchemaVersion { | |
| 171 | + | return catalogue.RemoteCatalog{}, &catalogue.RemoteCatalogVersionMismatchError{ | |
| 171 | 172 | Got: *version.SchemaVersion, | |
| 172 | - | Want: ports.RemoteCatalogSchemaVersion, | |
| 173 | + | Want: catalogue.RemoteCatalogSchemaVersion, | |
| 173 | 174 | Kind: "catalog", | |
| 174 | 175 | } | |
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | 178 | var envelope catalogEnvelope | |
| 178 | 179 | if err := decodeCatalogEnvelope(raw, true, &envelope); err != nil { | |
| 179 | - | return ports.RemoteCatalog{}, err | |
| 180 | + | return catalogue.RemoteCatalog{}, err | |
| 180 | 181 | } | |
| 181 | 182 | if envelope.ProtocolVersion == nil || envelope.SchemaVersion == nil || envelope.Sessions == nil { | |
| 182 | - | return ports.RemoteCatalog{}, errCatalogRequired | |
| 183 | + | return catalogue.RemoteCatalog{}, errCatalogRequired | |
| 183 | 184 | } | |
| 184 | - | sessions := make([]ports.RemoteCatalogSession, 0, len(*envelope.Sessions)) | |
| 185 | + | sessions := make([]catalogue.RemoteCatalogSession, 0, len(*envelope.Sessions)) | |
| 185 | 186 | for _, session := range *envelope.Sessions { | |
| 186 | 187 | if session.LifecycleID == nil || session.Name == nil || session.State == nil || session.Ephemeral == nil || session.Tabs == nil || session.Attached == nil { | |
| 187 | - | return ports.RemoteCatalog{}, errCatalogRequired | |
| 188 | + | return catalogue.RemoteCatalog{}, errCatalogRequired | |
| 188 | 189 | } | |
| 189 | - | tabs := make([]ports.RemoteCatalogTab, 0, len(*session.Tabs)) | |
| 190 | + | tabs := make([]catalogue.RemoteCatalogTab, 0, len(*session.Tabs)) | |
| 190 | 191 | for _, tab := range *session.Tabs { | |
| 191 | 192 | if tab.ID == nil || tab.Index == nil || tab.Name == nil { | |
| 192 | - | return ports.RemoteCatalog{}, errCatalogRequired | |
| 193 | + | return catalogue.RemoteCatalog{}, errCatalogRequired | |
| 193 | 194 | } | |
| 194 | - | tabs = append(tabs, ports.RemoteCatalogTab{ | |
| 195 | + | tabs = append(tabs, catalogue.RemoteCatalogTab{ | |
| 195 | 196 | ID: *tab.ID, Index: *tab.Index, Name: *tab.Name, | |
| 196 | 197 | Detail: tab.Detail, Attention: tab.Attention, | |
| 197 | 198 | }) | |
| 198 | 199 | } | |
| 199 | - | sessions = append(sessions, ports.RemoteCatalogSession{ | |
| 200 | + | sessions = append(sessions, catalogue.RemoteCatalogSession{ | |
| 200 | 201 | LifecycleID: *session.LifecycleID, Name: *session.Name, State: *session.State, | |
| 201 | 202 | Ephemeral: *session.Ephemeral, Tabs: tabs, Attached: *session.Attached, | |
| 202 | 203 | LastUsedSeq: session.LastUsedSeq, ActiveTabID: session.ActiveTabID, Reason: session.Reason, | |
| 203 | 204 | }) | |
| 204 | 205 | } | |
| 205 | - | catalog := ports.RemoteCatalog{ | |
| 206 | + | catalog := catalogue.RemoteCatalog{ | |
| 206 | 207 | ProtocolVersion: *envelope.ProtocolVersion, | |
| 207 | 208 | SchemaVersion: *envelope.SchemaVersion, | |
| 208 | 209 | Sessions: sessions, | |
| 209 | 210 | } | |
| 210 | - | if err := ports.ValidateRemoteCatalog(catalog); err != nil { | |
| 211 | - | return ports.RemoteCatalog{}, err | |
| 211 | + | if err := catalogue.ValidateRemoteCatalog(catalog); err != nil { | |
| 212 | + | return catalogue.RemoteCatalog{}, err | |
| 212 | 213 | } | |
| 213 | 214 | return catalog, nil | |
| 214 | 215 | } | |
M internal/adapters/remote/catalog_cache.go +37 -36
| @@ -12,7 +12,8 @@ import ( | |||
| 12 | 12 | "unicode/utf8" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/vev/internal/domain" | |
| 15 | - | "github.com/bnema/vev/internal/ports" | |
| 15 | + | appports "github.com/bnema/vev/internal/ports" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 18 | 19 | const ( | |
| @@ -40,14 +41,14 @@ type catalogCacheHost struct { | |||
| 40 | 41 | } | |
| 41 | 42 | ||
| 42 | 43 | type catalogCacheSession struct { | |
| 43 | - | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 44 | - | Name *string `json:"name"` | |
| 45 | - | State *ports.RemoteCatalogSessionState `json:"state"` | |
| 46 | - | Ephemeral *bool `json:"ephemeral"` | |
| 47 | - | LastUsedSeq *uint64 `json:"last_used_seq,omitempty"` | |
| 48 | - | Tabs *[]ports.RemoteCatalogTab `json:"tabs"` | |
| 49 | - | ActiveTabID *string `json:"active_tab_id,omitempty"` | |
| 50 | - | Attached *bool `json:"attached"` | |
| 44 | + | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 45 | + | Name *string `json:"name"` | |
| 46 | + | State *catalogue.RemoteCatalogSessionState `json:"state"` | |
| 47 | + | Ephemeral *bool `json:"ephemeral"` | |
| 48 | + | LastUsedSeq *uint64 `json:"last_used_seq,omitempty"` | |
| 49 | + | Tabs *[]catalogue.RemoteCatalogTab `json:"tabs"` | |
| 50 | + | ActiveTabID *string `json:"active_tab_id,omitempty"` | |
| 51 | + | Attached *bool `json:"attached"` | |
| 51 | 52 | } | |
| 52 | 53 | ||
| 53 | 54 | type catalogCacheFileV2 struct { | |
| @@ -62,15 +63,15 @@ type catalogCacheHostV2 struct { | |||
| 62 | 63 | } | |
| 63 | 64 | ||
| 64 | 65 | type catalogCacheSessionV2 struct { | |
| 65 | - | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id,omitempty"` | |
| 66 | - | Name *string `json:"name"` | |
| 67 | - | State *ports.RemoteCatalogSessionState `json:"state"` | |
| 68 | - | Ephemeral *bool `json:"ephemeral"` | |
| 69 | - | LastUsedSeq *uint64 `json:"last_used_seq,omitempty"` | |
| 70 | - | Tabs *uint16 `json:"tabs,omitempty"` | |
| 71 | - | TabList *[]ports.RemoteCatalogTab `json:"tab_list,omitempty"` | |
| 72 | - | ActiveTabID *string `json:"active_tab_id,omitempty"` | |
| 73 | - | Attached *bool `json:"attached"` | |
| 66 | + | LifecycleID *domain.SessionLifecycleID `json:"lifecycle_id,omitempty"` | |
| 67 | + | Name *string `json:"name"` | |
| 68 | + | State *catalogue.RemoteCatalogSessionState `json:"state"` | |
| 69 | + | Ephemeral *bool `json:"ephemeral"` | |
| 70 | + | LastUsedSeq *uint64 `json:"last_used_seq,omitempty"` | |
| 71 | + | Tabs *uint16 `json:"tabs,omitempty"` | |
| 72 | + | TabList *[]catalogue.RemoteCatalogTab `json:"tab_list,omitempty"` | |
| 73 | + | ActiveTabID *string `json:"active_tab_id,omitempty"` | |
| 74 | + | Attached *bool `json:"attached"` | |
| 74 | 75 | } | |
| 75 | 76 | ||
| 76 | 77 | type fileCatalogCache struct { | |
| @@ -78,19 +79,19 @@ type fileCatalogCache struct { | |||
| 78 | 79 | } | |
| 79 | 80 | ||
| 80 | 81 | // NewFileCatalogCache returns a durable remote catalog cache backed by path. | |
| 81 | - | func NewFileCatalogCache(path string) ports.RemoteCatalogCache { | |
| 82 | + | func NewFileCatalogCache(path string) appports.RemoteCatalogCache { | |
| 82 | 83 | return &fileCatalogCache{path: path} | |
| 83 | 84 | } | |
| 84 | 85 | ||
| 85 | - | var _ ports.RemoteCatalogCache = (*fileCatalogCache)(nil) | |
| 86 | + | var _ appports.RemoteCatalogCache = (*fileCatalogCache)(nil) | |
| 86 | 87 | ||
| 87 | 88 | // Load reads and validates one complete cache snapshot. Exact v2 tab-list | |
| 88 | 89 | // snapshots are upgraded in memory; ambiguous count-only v2 data is rejected. | |
| 89 | 90 | // A missing cache is an empty snapshot, and Load never changes cache data. | |
| 90 | - | func (c *fileCatalogCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 91 | + | func (c *fileCatalogCache) Load() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 91 | 92 | raw, err := os.ReadFile(c.path) | |
| 92 | 93 | if errors.Is(err, os.ErrNotExist) { | |
| 93 | - | return []ports.RemoteCatalogCacheEntry{}, nil | |
| 94 | + | return []catalogue.RemoteCatalogCacheEntry{}, nil | |
| 94 | 95 | } | |
| 95 | 96 | if err != nil { | |
| 96 | 97 | return nil, err | |
| @@ -130,7 +131,7 @@ func (c *fileCatalogCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |||
| 130 | 131 | return nil, fmt.Errorf("remote catalog cache: malformed cache file: missing hosts") | |
| 131 | 132 | } | |
| 132 | 133 | ||
| 133 | - | entries := make([]ports.RemoteCatalogCacheEntry, 0, len(*file.Hosts)) | |
| 134 | + | entries := make([]catalogue.RemoteCatalogCacheEntry, 0, len(*file.Hosts)) | |
| 134 | 135 | for _, host := range *file.Hosts { | |
| 135 | 136 | if host.Target == nil || host.FetchedAtUnixNano == nil || host.Sessions == nil { | |
| 136 | 137 | return nil, fmt.Errorf("remote catalog cache: malformed cache file: missing host fields") | |
| @@ -141,10 +142,10 @@ func (c *fileCatalogCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |||
| 141 | 142 | if *host.FetchedAtUnixNano <= 0 { | |
| 142 | 143 | return nil, fmt.Errorf("remote catalog cache: malformed cache file: non-positive fetched time") | |
| 143 | 144 | } | |
| 144 | - | entry := ports.RemoteCatalogCacheEntry{ | |
| 145 | + | entry := catalogue.RemoteCatalogCacheEntry{ | |
| 145 | 146 | Host: *host.Target, | |
| 146 | 147 | FetchedAt: time.Unix(0, *host.FetchedAtUnixNano), | |
| 147 | - | Sessions: make([]ports.RemoteCatalogSession, 0, len(*host.Sessions)), | |
| 148 | + | Sessions: make([]catalogue.RemoteCatalogSession, 0, len(*host.Sessions)), | |
| 148 | 149 | } | |
| 149 | 150 | for _, session := range *host.Sessions { | |
| 150 | 151 | if session.LifecycleID == nil || session.Name == nil || session.State == nil || session.Ephemeral == nil || session.Attached == nil || session.Tabs == nil { | |
| @@ -153,9 +154,9 @@ func (c *fileCatalogCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |||
| 153 | 154 | if !utf8.ValidString(*session.Name) || !utf8.ValidString(string(*session.State)) { | |
| 154 | 155 | return nil, fmt.Errorf("remote catalog cache: malformed cache file: invalid UTF-8") | |
| 155 | 156 | } | |
| 156 | - | tabs := make([]ports.RemoteCatalogTab, len(*session.Tabs)) | |
| 157 | + | tabs := make([]catalogue.RemoteCatalogTab, len(*session.Tabs)) | |
| 157 | 158 | copy(tabs, *session.Tabs) | |
| 158 | - | decoded := ports.RemoteCatalogSession{ | |
| 159 | + | decoded := catalogue.RemoteCatalogSession{ | |
| 159 | 160 | LifecycleID: *session.LifecycleID, | |
| 160 | 161 | Name: *session.Name, | |
| 161 | 162 | State: *session.State, | |
| @@ -228,7 +229,7 @@ func migrateExactCatalogCacheV2(legacy catalogCacheFileV2) (catalogCacheFile, er | |||
| 228 | 229 | } | |
| 229 | 230 | ||
| 230 | 231 | // Store atomically replaces the cache with a complete normalized snapshot. | |
| 231 | - | func (c *fileCatalogCache) Store(entries []ports.RemoteCatalogCacheEntry) error { | |
| 232 | + | func (c *fileCatalogCache) Store(entries []catalogue.RemoteCatalogCacheEntry) error { | |
| 232 | 233 | normalized, err := normalizeCatalogCacheEntries(entries) | |
| 233 | 234 | if err != nil { | |
| 234 | 235 | return err | |
| @@ -244,7 +245,7 @@ func (c *fileCatalogCache) Store(entries []ports.RemoteCatalogCacheEntry) error | |||
| 244 | 245 | ephemeral := session.Ephemeral | |
| 245 | 246 | attached := session.Attached | |
| 246 | 247 | id := session.LifecycleID | |
| 247 | - | tabs := make([]ports.RemoteCatalogTab, len(session.Tabs)) | |
| 248 | + | tabs := make([]catalogue.RemoteCatalogTab, len(session.Tabs)) | |
| 248 | 249 | copy(tabs, session.Tabs) | |
| 249 | 250 | row := catalogCacheSession{LifecycleID: &id, Name: &name, State: &state, Ephemeral: &ephemeral, Tabs: &tabs, Attached: &attached} | |
| 250 | 251 | if session.LastUsedSeq != 0 { | |
| @@ -271,11 +272,11 @@ func (c *fileCatalogCache) Store(entries []ports.RemoteCatalogCacheEntry) error | |||
| 271 | 272 | return atomicReplacePrivateFile(c.path, append(payload, '\n')) | |
| 272 | 273 | } | |
| 273 | 274 | ||
| 274 | - | func normalizeCatalogCacheEntries(entries []ports.RemoteCatalogCacheEntry) ([]ports.RemoteCatalogCacheEntry, error) { | |
| 275 | - | if err := ports.ValidateRemoteCatalogCacheEntries(entries); err != nil { | |
| 275 | + | func normalizeCatalogCacheEntries(entries []catalogue.RemoteCatalogCacheEntry) ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 276 | + | if err := catalogue.ValidateRemoteCatalogCacheEntries(entries); err != nil { | |
| 276 | 277 | return nil, err | |
| 277 | 278 | } | |
| 278 | - | normalized := make([]ports.RemoteCatalogCacheEntry, 0, len(entries)) | |
| 279 | + | normalized := make([]catalogue.RemoteCatalogCacheEntry, 0, len(entries)) | |
| 279 | 280 | hosts := make(map[string]struct{}, len(entries)) | |
| 280 | 281 | for _, entry := range entries { | |
| 281 | 282 | if err := validateHostTarget(entry.Host); err != nil { | |
| @@ -289,10 +290,10 @@ func normalizeCatalogCacheEntries(entries []ports.RemoteCatalogCacheEntry) ([]po | |||
| 289 | 290 | } | |
| 290 | 291 | hosts[entry.Host] = struct{}{} | |
| 291 | 292 | ||
| 292 | - | copyEntry := ports.RemoteCatalogCacheEntry{ | |
| 293 | + | copyEntry := catalogue.RemoteCatalogCacheEntry{ | |
| 293 | 294 | Host: entry.Host, | |
| 294 | 295 | FetchedAt: entry.FetchedAt, | |
| 295 | - | Sessions: make([]ports.RemoteCatalogSession, 0, len(entry.Sessions)), | |
| 296 | + | Sessions: make([]catalogue.RemoteCatalogSession, 0, len(entry.Sessions)), | |
| 296 | 297 | } | |
| 297 | 298 | sessions := make(map[string]struct{}, len(entry.Sessions)) | |
| 298 | 299 | for _, session := range entry.Sessions { | |
| @@ -310,9 +311,9 @@ func normalizeCatalogCacheEntries(entries []ports.RemoteCatalogCacheEntry) ([]po | |||
| 310 | 311 | } | |
| 311 | 312 | sessions[session.Name] = struct{}{} | |
| 312 | 313 | copySession := session | |
| 313 | - | copySession.Tabs = make([]ports.RemoteCatalogTab, len(session.Tabs)) | |
| 314 | + | copySession.Tabs = make([]catalogue.RemoteCatalogTab, len(session.Tabs)) | |
| 314 | 315 | for i, tab := range session.Tabs { | |
| 315 | - | copySession.Tabs[i] = ports.RemoteCatalogTab{ID: tab.ID, Index: tab.Index, Name: tab.Name} | |
| 316 | + | copySession.Tabs[i] = catalogue.RemoteCatalogTab{ID: tab.ID, Index: tab.Index, Name: tab.Name} | |
| 316 | 317 | } | |
| 317 | 318 | copyEntry.Sessions = append(copyEntry.Sessions, copySession) | |
| 318 | 319 | } | |
M internal/adapters/remote/catalog_cache_test.go +25 -25
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | "testing" | |
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | - | "github.com/bnema/vev/internal/ports" | |
| 12 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 13 | 13 | "github.com/bnema/vev/pkg/safedir" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| @@ -27,22 +27,22 @@ func TestCatalogCacheStoreLoad(t *testing.T) { | |||
| 27 | 27 | path := filepath.Join(t.TempDir(), "vev", "remote-catalog-cache.json") | |
| 28 | 28 | cache := NewFileCatalogCache(path) | |
| 29 | 29 | fetchedAt := time.Unix(0, 1780000000000000000) | |
| 30 | - | entries := []ports.RemoteCatalogCacheEntry{ | |
| 30 | + | entries := []catalogue.RemoteCatalogCacheEntry{ | |
| 31 | 31 | { | |
| 32 | 32 | Host: "zebra", | |
| 33 | 33 | FetchedAt: fetchedAt, | |
| 34 | - | Sessions: []ports.RemoteCatalogSession{ | |
| 34 | + | Sessions: []catalogue.RemoteCatalogSession{ | |
| 35 | 35 | { | |
| 36 | 36 | LifecycleID: [16]byte{1}, Name: "work", State: "up", LastUsedSeq: 42, ActiveTabID: "work-2", | |
| 37 | - | Tabs: []ports.RemoteCatalogTab{{ID: "work-1", Detail: "dynamic", Attention: true}, {ID: "work-2", Index: 1}}, | |
| 37 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "work-1", Detail: "dynamic", Attention: true}, {ID: "work-2", Index: 1}}, | |
| 38 | 38 | }, | |
| 39 | - | {LifecycleID: [16]byte{2}, Name: "alpha", State: "down", Ephemeral: true, Tabs: []ports.RemoteCatalogTab{{ID: "alpha-1"}}, Attached: true}, | |
| 39 | + | {LifecycleID: [16]byte{2}, Name: "alpha", State: "down", Ephemeral: true, Tabs: []catalogue.RemoteCatalogTab{{ID: "alpha-1"}}, Attached: true}, | |
| 40 | 40 | }, | |
| 41 | 41 | }, | |
| 42 | 42 | { | |
| 43 | 43 | Host: "arch", | |
| 44 | 44 | FetchedAt: fetchedAt.Add(time.Second), | |
| 45 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 45 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 46 | 46 | }, | |
| 47 | 47 | } | |
| 48 | 48 | ||
| @@ -73,18 +73,18 @@ func TestCatalogCacheStoreLoad(t *testing.T) { | |||
| 73 | 73 | if err != nil { | |
| 74 | 74 | t.Fatalf("Load() error = %v", err) | |
| 75 | 75 | } | |
| 76 | - | want := []ports.RemoteCatalogCacheEntry{ | |
| 76 | + | want := []catalogue.RemoteCatalogCacheEntry{ | |
| 77 | 77 | { | |
| 78 | 78 | Host: "arch", | |
| 79 | 79 | FetchedAt: fetchedAt.Add(time.Second), | |
| 80 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 80 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 81 | 81 | }, | |
| 82 | 82 | { | |
| 83 | 83 | Host: "zebra", | |
| 84 | 84 | FetchedAt: fetchedAt, | |
| 85 | - | Sessions: []ports.RemoteCatalogSession{ | |
| 86 | - | {LifecycleID: [16]byte{2}, Name: "alpha", State: "down", Ephemeral: true, Tabs: []ports.RemoteCatalogTab{{ID: "alpha-1"}}, Attached: true}, | |
| 87 | - | {LifecycleID: [16]byte{1}, Name: "work", State: "up", LastUsedSeq: 42, ActiveTabID: "work-2", Tabs: []ports.RemoteCatalogTab{{ID: "work-1"}, {ID: "work-2", Index: 1}}}, | |
| 85 | + | Sessions: []catalogue.RemoteCatalogSession{ | |
| 86 | + | {LifecycleID: [16]byte{2}, Name: "alpha", State: "down", Ephemeral: true, Tabs: []catalogue.RemoteCatalogTab{{ID: "alpha-1"}}, Attached: true}, | |
| 87 | + | {LifecycleID: [16]byte{1}, Name: "work", State: "up", LastUsedSeq: 42, ActiveTabID: "work-2", Tabs: []catalogue.RemoteCatalogTab{{ID: "work-1"}, {ID: "work-2", Index: 1}}}, | |
| 88 | 88 | }, | |
| 89 | 89 | }, | |
| 90 | 90 | } | |
| @@ -133,14 +133,14 @@ func TestCatalogCacheLoadMigratesExactV2TabList(t *testing.T) { | |||
| 133 | 133 | if err != nil { | |
| 134 | 134 | t.Fatalf("Load() error = %v", err) | |
| 135 | 135 | } | |
| 136 | - | want := []ports.RemoteCatalogCacheEntry{{ | |
| 136 | + | want := []catalogue.RemoteCatalogCacheEntry{{ | |
| 137 | 137 | Host: "user@arch", | |
| 138 | 138 | FetchedAt: time.Unix(0, 1780000000000000000), | |
| 139 | - | Sessions: []ports.RemoteCatalogSession{ | |
| 140 | - | {LifecycleID: [16]byte{2}, Name: "dumber", State: ports.RemoteCatalogSessionDown, Tabs: []ports.RemoteCatalogTab{}}, | |
| 139 | + | Sessions: []catalogue.RemoteCatalogSession{ | |
| 140 | + | {LifecycleID: [16]byte{2}, Name: "dumber", State: catalogue.RemoteCatalogSessionDown, Tabs: []catalogue.RemoteCatalogTab{}}, | |
| 141 | 141 | { | |
| 142 | - | LifecycleID: [16]byte{1}, Name: "work", State: ports.RemoteCatalogSessionUp, | |
| 143 | - | LastUsedSeq: 42, Tabs: []ports.RemoteCatalogTab{{ID: "t_work", Name: "shell"}}, ActiveTabID: "t_work", | |
| 142 | + | LifecycleID: [16]byte{1}, Name: "work", State: catalogue.RemoteCatalogSessionUp, | |
| 143 | + | LastUsedSeq: 42, Tabs: []catalogue.RemoteCatalogTab{{ID: "t_work", Name: "shell"}}, ActiveTabID: "t_work", | |
| 144 | 144 | }, | |
| 145 | 145 | }, | |
| 146 | 146 | }} | |
| @@ -208,10 +208,10 @@ func TestCatalogCacheStoreFailurePreservesExistingFile(t *testing.T) { | |||
| 208 | 208 | ||
| 209 | 209 | path := filepath.Join(t.TempDir(), "vev", "remote-catalog-cache.json") | |
| 210 | 210 | cache := NewFileCatalogCache(path) | |
| 211 | - | valid := []ports.RemoteCatalogCacheEntry{{ | |
| 211 | + | valid := []catalogue.RemoteCatalogCacheEntry{{ | |
| 212 | 212 | Host: "arch", | |
| 213 | 213 | FetchedAt: time.Unix(0, 1), | |
| 214 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 214 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 215 | 215 | }} | |
| 216 | 216 | if err := cache.Store(valid); err != nil { | |
| 217 | 217 | t.Fatalf("initial Store() error = %v", err) | |
| @@ -221,10 +221,10 @@ func TestCatalogCacheStoreFailurePreservesExistingFile(t *testing.T) { | |||
| 221 | 221 | t.Fatalf("read initial cache: %v", err) | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | - | invalid := []ports.RemoteCatalogCacheEntry{{ | |
| 224 | + | invalid := []catalogue.RemoteCatalogCacheEntry{{ | |
| 225 | 225 | Host: "arch", | |
| 226 | 226 | FetchedAt: time.Time{}, | |
| 227 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 227 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 228 | 228 | }} | |
| 229 | 229 | if err := cache.Store(invalid); err == nil { | |
| 230 | 230 | t.Fatal("Store() error = nil, want validation error") | |
| @@ -251,10 +251,10 @@ func TestCatalogCacheStoreRejectsNonPositiveFetchedTimes(t *testing.T) { | |||
| 251 | 251 | t.Run(tc.name, func(t *testing.T) { | |
| 252 | 252 | t.Parallel() | |
| 253 | 253 | path := filepath.Join(t.TempDir(), "remote-catalog-cache.json") | |
| 254 | - | entries := []ports.RemoteCatalogCacheEntry{{ | |
| 254 | + | entries := []catalogue.RemoteCatalogCacheEntry{{ | |
| 255 | 255 | Host: "arch", | |
| 256 | 256 | FetchedAt: tc.fetchedAt, | |
| 257 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 257 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 258 | 258 | }} | |
| 259 | 259 | ||
| 260 | 260 | err := NewFileCatalogCache(path).Store(entries) | |
| @@ -281,7 +281,7 @@ func TestCatalogCacheStoreHardensExistingFilePermissions(t *testing.T) { | |||
| 281 | 281 | } | |
| 282 | 282 | ||
| 283 | 283 | cache := NewFileCatalogCache(path) | |
| 284 | - | if err := cache.Store([]ports.RemoteCatalogCacheEntry{}); err != nil { | |
| 284 | + | if err := cache.Store([]catalogue.RemoteCatalogCacheEntry{}); err != nil { | |
| 285 | 285 | t.Fatalf("Store() error = %v", err) | |
| 286 | 286 | } | |
| 287 | 287 | info, err := os.Stat(path) | |
| @@ -306,7 +306,7 @@ func TestCatalogCacheStoreRenameFailureCleansTemporaryFile(t *testing.T) { | |||
| 306 | 306 | } | |
| 307 | 307 | ||
| 308 | 308 | cache := NewFileCatalogCache(path) | |
| 309 | - | if err := cache.Store([]ports.RemoteCatalogCacheEntry{}); err == nil { | |
| 309 | + | if err := cache.Store([]catalogue.RemoteCatalogCacheEntry{}); err == nil { | |
| 310 | 310 | t.Fatal("Store() error = nil, want rename error") | |
| 311 | 311 | } | |
| 312 | 312 | entries, err := os.ReadDir(dir) | |
| @@ -320,6 +320,6 @@ func TestCatalogCacheStoreRenameFailureCleansTemporaryFile(t *testing.T) { | |||
| 320 | 320 | } | |
| 321 | 321 | } | |
| 322 | 322 | ||
| 323 | - | func equalRemoteCatalogCacheEntry(a, b ports.RemoteCatalogCacheEntry) bool { | |
| 323 | + | func equalRemoteCatalogCacheEntry(a, b catalogue.RemoteCatalogCacheEntry) bool { | |
| 324 | 324 | return a.Host == b.Host && a.FetchedAt.Equal(b.FetchedAt) && reflect.DeepEqual(a.Sessions, b.Sessions) | |
| 325 | 325 | } | |
M internal/adapters/remote/catalog_test.go +31 -31
| @@ -12,8 +12,8 @@ import ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 15 | - | "github.com/bnema/vev/internal/ports" | |
| 16 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | func TestRemoteCatalogClientCommandConstruction(t *testing.T) { | |
| @@ -37,10 +37,10 @@ func TestRemoteCatalogClientCommandConstruction(t *testing.T) { | |||
| 37 | 37 | command: func(ctx context.Context, name string, args ...string) *exec.Cmd { | |
| 38 | 38 | gotPath = name | |
| 39 | 39 | gotArgs = append([]string(nil), args...) | |
| 40 | - | return stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{ | |
| 40 | + | return stdoutCmd(ctx, mustCatalogJSON(t, catalogue.RemoteCatalog{ | |
| 41 | 41 | ProtocolVersion: protocol.Version, | |
| 42 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 43 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 42 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 43 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 44 | 44 | })) | |
| 45 | 45 | }, | |
| 46 | 46 | } | |
| @@ -83,11 +83,11 @@ func TestRemoteCatalogClientRejectsUnsafeTarget(t *testing.T) { | |||
| 83 | 83 | func TestRemoteCatalogClientDecode(t *testing.T) { | |
| 84 | 84 | t.Parallel() | |
| 85 | 85 | ||
| 86 | - | valid := ports.RemoteCatalog{ | |
| 86 | + | valid := catalogue.RemoteCatalog{ | |
| 87 | 87 | ProtocolVersion: protocol.Version, | |
| 88 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 89 | - | Sessions: []ports.RemoteCatalogSession{ | |
| 90 | - | {LifecycleID: [16]byte{1}, Name: "work", State: "up", Ephemeral: false, Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, Attached: true}, | |
| 88 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 89 | + | Sessions: []catalogue.RemoteCatalogSession{ | |
| 90 | + | {LifecycleID: [16]byte{1}, Name: "work", State: "up", Ephemeral: false, Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, Attached: true}, | |
| 91 | 91 | }, | |
| 92 | 92 | } | |
| 93 | 93 | validJSON := mustCatalogJSON(t, valid) | |
| @@ -97,7 +97,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 97 | 97 | stdout string | |
| 98 | 98 | stderr string | |
| 99 | 99 | exit int | |
| 100 | - | want ports.RemoteCatalog | |
| 100 | + | want catalogue.RemoteCatalog | |
| 101 | 101 | wantErr error | |
| 102 | 102 | check func(t *testing.T, err error) | |
| 103 | 103 | }{ | |
| @@ -113,10 +113,10 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 113 | 113 | }, | |
| 114 | 114 | { | |
| 115 | 115 | name: "mismatching protocol version", | |
| 116 | - | stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version + 1, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{}}), | |
| 116 | + | stdout: mustCatalogJSON(t, catalogue.RemoteCatalog{ProtocolVersion: protocol.Version + 1, SchemaVersion: catalogue.RemoteCatalogSchemaVersion, Sessions: []catalogue.RemoteCatalogSession{}}), | |
| 117 | 117 | check: func(t *testing.T, err error) { | |
| 118 | 118 | t.Helper() | |
| 119 | - | var mismatch *ports.RemoteCatalogVersionMismatchError | |
| 119 | + | var mismatch *catalogue.RemoteCatalogVersionMismatchError | |
| 120 | 120 | if !errors.As(err, &mismatch) { | |
| 121 | 121 | t.Fatalf("error = %v, want RemoteCatalogVersionMismatchError", err) | |
| 122 | 122 | } | |
| @@ -131,8 +131,8 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 131 | 131 | protocol.Version), | |
| 132 | 132 | check: func(t *testing.T, err error) { | |
| 133 | 133 | t.Helper() | |
| 134 | - | var mismatch *ports.RemoteCatalogVersionMismatchError | |
| 135 | - | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 2 || mismatch.Want != ports.RemoteCatalogSchemaVersion { | |
| 134 | + | var mismatch *catalogue.RemoteCatalogVersionMismatchError | |
| 135 | + | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 2 || mismatch.Want != catalogue.RemoteCatalogSchemaVersion { | |
| 136 | 136 | t.Fatalf("error = %v, want obsolete catalogue version mismatch", err) | |
| 137 | 137 | } | |
| 138 | 138 | }, | |
| @@ -142,8 +142,8 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 142 | 142 | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":4,"sessions":[]}`, protocol.Version), | |
| 143 | 143 | check: func(t *testing.T, err error) { | |
| 144 | 144 | t.Helper() | |
| 145 | - | var mismatch *ports.RemoteCatalogVersionMismatchError | |
| 146 | - | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 4 || mismatch.Want != ports.RemoteCatalogSchemaVersion { | |
| 145 | + | var mismatch *catalogue.RemoteCatalogVersionMismatchError | |
| 146 | + | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 4 || mismatch.Want != catalogue.RemoteCatalogSchemaVersion { | |
| 147 | 147 | t.Fatalf("error = %v, want unknown catalogue version mismatch", err) | |
| 148 | 148 | } | |
| 149 | 149 | }, | |
| @@ -161,19 +161,19 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 161 | 161 | { | |
| 162 | 162 | name: "count-only tabs rejected", | |
| 163 | 163 | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}`, | |
| 164 | - | protocol.Version, ports.RemoteCatalogSchemaVersion), | |
| 164 | + | protocol.Version, catalogue.RemoteCatalogSchemaVersion), | |
| 165 | 165 | wantErr: errCatalogDecode, | |
| 166 | 166 | }, | |
| 167 | 167 | { | |
| 168 | 168 | name: "unknown fields rejected", | |
| 169 | 169 | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[],"future":true}`, | |
| 170 | - | protocol.Version, ports.RemoteCatalogSchemaVersion), | |
| 170 | + | protocol.Version, catalogue.RemoteCatalogSchemaVersion), | |
| 171 | 171 | wantErr: errCatalogDecode, | |
| 172 | 172 | }, | |
| 173 | 173 | { | |
| 174 | 174 | name: "missing required session field", | |
| 175 | 175 | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","tabs":[],"attached":false}]}`, | |
| 176 | - | protocol.Version, ports.RemoteCatalogSchemaVersion), | |
| 176 | + | protocol.Version, catalogue.RemoteCatalogSchemaVersion), | |
| 177 | 177 | wantErr: errCatalogRequired, | |
| 178 | 178 | }, | |
| 179 | 179 | { | |
| @@ -191,30 +191,30 @@ func TestRemoteCatalogClientDecode(t *testing.T) { | |||
| 191 | 191 | stdout: fmt.Sprintf(`{"protocol_version":%d,"sessions":[]}`+"\n", protocol.Version), | |
| 192 | 192 | check: func(t *testing.T, err error) { | |
| 193 | 193 | t.Helper() | |
| 194 | - | var mismatch *ports.RemoteCatalogVersionMismatchError | |
| 195 | - | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 0 || mismatch.Want != ports.RemoteCatalogSchemaVersion { | |
| 194 | + | var mismatch *catalogue.RemoteCatalogVersionMismatchError | |
| 195 | + | if !errors.As(err, &mismatch) || mismatch.Kind != "catalog" || mismatch.Got != 0 || mismatch.Want != catalogue.RemoteCatalogSchemaVersion { | |
| 196 | 196 | t.Fatalf("error = %v, want missing catalogue version mismatch", err) | |
| 197 | 197 | } | |
| 198 | 198 | }, | |
| 199 | 199 | }, | |
| 200 | 200 | { | |
| 201 | 201 | name: "missing sessions", | |
| 202 | - | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", protocol.Version, ports.RemoteCatalogSchemaVersion), | |
| 202 | + | stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", protocol.Version, catalogue.RemoteCatalogSchemaVersion), | |
| 203 | 203 | wantErr: errCatalogRequired, | |
| 204 | 204 | }, | |
| 205 | 205 | { | |
| 206 | 206 | name: "invalid session name", | |
| 207 | - | stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{ | |
| 208 | - | LifecycleID: [16]byte{1}, Name: "bad name", State: "up", Tabs: []ports.RemoteCatalogTab{}, | |
| 207 | + | stdout: mustCatalogJSON(t, catalogue.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: catalogue.RemoteCatalogSchemaVersion, Sessions: []catalogue.RemoteCatalogSession{{ | |
| 208 | + | LifecycleID: [16]byte{1}, Name: "bad name", State: "up", Tabs: []catalogue.RemoteCatalogTab{}, | |
| 209 | 209 | }}}), | |
| 210 | - | wantErr: ports.ErrInvalidRemoteCatalog, | |
| 210 | + | wantErr: catalogue.ErrInvalidRemoteCatalog, | |
| 211 | 211 | }, | |
| 212 | 212 | { | |
| 213 | 213 | name: "invalid session state", | |
| 214 | - | stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{ | |
| 215 | - | LifecycleID: [16]byte{1}, Name: "work", State: "weird", Tabs: []ports.RemoteCatalogTab{}, | |
| 214 | + | stdout: mustCatalogJSON(t, catalogue.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: catalogue.RemoteCatalogSchemaVersion, Sessions: []catalogue.RemoteCatalogSession{{ | |
| 215 | + | LifecycleID: [16]byte{1}, Name: "work", State: "weird", Tabs: []catalogue.RemoteCatalogTab{}, | |
| 216 | 216 | }}}), | |
| 217 | - | wantErr: ports.ErrRemoteCatalogUnknownState, | |
| 217 | + | wantErr: catalogue.ErrRemoteCatalogUnknownState, | |
| 218 | 218 | }, | |
| 219 | 219 | { | |
| 220 | 220 | name: "remote exit with stderr", | |
| @@ -292,10 +292,10 @@ func TestRemoteCatalogClientAppliesLifetimeAndWaitDelay(t *testing.T) { | |||
| 292 | 292 | client := &CatalogClient{ | |
| 293 | 293 | command: func(ctx context.Context, name string, args ...string) *exec.Cmd { | |
| 294 | 294 | commandCtx = ctx | |
| 295 | - | cmd = stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{ | |
| 295 | + | cmd = stdoutCmd(ctx, mustCatalogJSON(t, catalogue.RemoteCatalog{ | |
| 296 | 296 | ProtocolVersion: protocol.Version, | |
| 297 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 298 | - | Sessions: []ports.RemoteCatalogSession{}, | |
| 297 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 298 | + | Sessions: []catalogue.RemoteCatalogSession{}, | |
| 299 | 299 | })) | |
| 300 | 300 | return cmd | |
| 301 | 301 | }, | |
| @@ -549,7 +549,7 @@ func stdoutCmd(ctx context.Context, stdout string) *exec.Cmd { | |||
| 549 | 549 | return cmd | |
| 550 | 550 | } | |
| 551 | 551 | ||
| 552 | - | func mustCatalogJSON(t *testing.T, catalog ports.RemoteCatalog) string { | |
| 552 | + | func mustCatalogJSON(t *testing.T, catalog catalogue.RemoteCatalog) string { | |
| 553 | 553 | t.Helper() | |
| 554 | 554 | raw, err := json.Marshal(catalog) | |
| 555 | 555 | if err != nil { | |
M internal/adapters/remote/remote_factory.go +14 -9
| @@ -8,33 +8,38 @@ import ( | |||
| 8 | 8 | "github.com/bnema/vev/internal/adapters/dgram" | |
| 9 | 9 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 10 | 10 | "github.com/bnema/vev/internal/ports" | |
| 11 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| 13 | - | // DialerFactory selects the requested remote transport adapter. | |
| 14 | + | // TransportMode selects a concrete remote carriage after app validation. | |
| 15 | + | type TransportMode string | |
| 16 | + | ||
| 17 | + | const ( | |
| 18 | + | TransportUDP TransportMode = "udp" | |
| 19 | + | TransportStdio TransportMode = "stdio" | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | // DialerFactory constructs target-specific raw carriage dialers. | |
| 14 | 23 | type DialerFactory struct { | |
| 15 | 24 | observer ports.SerializedRuntimeObserver | |
| 16 | 25 | } | |
| 17 | 26 | ||
| 18 | - | var _ ports.RemoteDialerFactory = DialerFactory{} | |
| 19 | - | ||
| 20 | 27 | func NewDialerFactory() DialerFactory { return DialerFactory{} } | |
| 21 | 28 | ||
| 22 | - | // NewDialerFactoryWithRuntimeObserver wires the one process-local sink into | |
| 23 | - | // whichever concrete remote carriage adapter is selected. | |
| 24 | 29 | func NewDialerFactoryWithRuntimeObserver(observer ports.SerializedRuntimeObserver) DialerFactory { | |
| 25 | 30 | return DialerFactory{observer: observer} | |
| 26 | 31 | } | |
| 27 | 32 | ||
| 28 | - | func (f DialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |
| 33 | + | func (f DialerFactory) DialerForRemote(target, session string, mode TransportMode, log *slog.Logger) (wire.Dialer, error) { | |
| 29 | 34 | switch mode { | |
| 30 | - | case ports.RemoteTransportUDP: | |
| 35 | + | case TransportUDP: | |
| 31 | 36 | if log != nil { | |
| 32 | 37 | log.Info("remote transport selected", "mode", mode, "target", target, "session", session) | |
| 33 | 38 | } | |
| 34 | 39 | dialer := dgram.NewRemoteDialerWithLogger(target, "", log) | |
| 35 | 40 | dialer.RuntimeObserver = f.observer | |
| 36 | 41 | return dialer, nil | |
| 37 | - | case ports.RemoteTransportStdio: | |
| 42 | + | case TransportStdio: | |
| 38 | 43 | if log != nil { | |
| 39 | 44 | log.Info("remote transport selected", "mode", mode, "target", target, "session", session) | |
| 40 | 45 | } | |
| @@ -50,6 +55,6 @@ type stdioDialer struct { | |||
| 50 | 55 | observer ports.SerializedRuntimeObserver | |
| 51 | 56 | } | |
| 52 | 57 | ||
| 53 | - | func (d stdioDialer) Dial(ctx context.Context) (ports.Transport, error) { | |
| 58 | + | func (d stdioDialer) Dial(ctx context.Context) (wire.Transport, error) { | |
| 54 | 59 | return sshstdio.DialContextWithRuntimeObserver(ctx, d.target, "", d.log, sshstdio.WithRuntimeObserver(d.observer)) | |
| 55 | 60 | } | |
M internal/adapters/remote/remote_factory_test.go +4 -5
| @@ -6,7 +6,6 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/vev/internal/adapters/dgram" | |
| 9 | - | "github.com/bnema/vev/internal/ports" | |
| 10 | 9 | ) | |
| 11 | 10 | ||
| 12 | 11 | func TestDialerFactorySelectsExplicitModes(t *testing.T) { | |
| @@ -14,11 +13,11 @@ func TestDialerFactorySelectsExplicitModes(t *testing.T) { | |||
| 14 | 13 | ||
| 15 | 14 | tests := []struct { | |
| 16 | 15 | name string | |
| 17 | - | mode ports.RemoteTransportMode | |
| 16 | + | mode TransportMode | |
| 18 | 17 | wantType any | |
| 19 | 18 | }{ | |
| 20 | - | {name: "udp", mode: ports.RemoteTransportUDP, wantType: dgram.RemoteDialer{}}, | |
| 21 | - | {name: "stdio", mode: ports.RemoteTransportStdio, wantType: stdioDialer{}}, | |
| 19 | + | {name: "udp", mode: TransportUDP, wantType: dgram.RemoteDialer{}}, | |
| 20 | + | {name: "stdio", mode: TransportStdio, wantType: stdioDialer{}}, | |
| 22 | 21 | } | |
| 23 | 22 | for _, tt := range tests { | |
| 24 | 23 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -51,7 +50,7 @@ func TestDialerFactorySelectsExplicitModes(t *testing.T) { | |||
| 51 | 50 | func TestDialerFactoryRejectsUnsupportedMode(t *testing.T) { | |
| 52 | 51 | factory := NewDialerFactory() | |
| 53 | 52 | ||
| 54 | - | dialer, err := factory.DialerForRemote("remote.example", "work", ports.RemoteTransportMode("serial"), nil) | |
| 53 | + | dialer, err := factory.DialerForRemote("remote.example", "work", TransportMode("serial"), nil) | |
| 55 | 54 | if err == nil { | |
| 56 | 55 | t.Fatal("DialerForRemote() error = nil, want unsupported mode error") | |
| 57 | 56 | } | |
M internal/adapters/sessionwire/client.go +4 -4
| @@ -9,12 +9,12 @@ import ( | |||
| 9 | 9 | "github.com/bnema/vev/internal/protocol/wire" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | - | type clientConnection struct{ raw ports.Transport } | |
| 12 | + | type clientConnection struct{ raw wire.Transport } | |
| 13 | 13 | ||
| 14 | 14 | var _ ports.ClientConnection = (*clientConnection)(nil) | |
| 15 | 15 | ||
| 16 | 16 | // NewClientConnection wraps one raw client connection incarnation. | |
| 17 | - | func NewClientConnection(raw ports.Transport) ports.ClientConnection { | |
| 17 | + | func NewClientConnection(raw wire.Transport) ports.ClientConnection { | |
| 18 | 18 | if raw == nil { | |
| 19 | 19 | return nil | |
| 20 | 20 | } | |
| @@ -55,12 +55,12 @@ func (c *clientConnection) LinkState() ports.LinkState { return rawLinkS | |||
| 55 | 55 | func (c *clientConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) } | |
| 56 | 56 | func (c *clientConnection) Close() error { return c.raw.Close() } | |
| 57 | 57 | ||
| 58 | - | type clientDialer struct{ raw ports.Dialer } | |
| 58 | + | type clientDialer struct{ raw wire.Dialer } | |
| 59 | 59 | ||
| 60 | 60 | var _ ports.ClientDialer = (*clientDialer)(nil) | |
| 61 | 61 | ||
| 62 | 62 | // NewClientDialer wraps every dialed raw connection in a stable typed adapter. | |
| 63 | - | func NewClientDialer(raw ports.Dialer) ports.ClientDialer { | |
| 63 | + | func NewClientDialer(raw wire.Dialer) ports.ClientDialer { | |
| 64 | 64 | if raw == nil { | |
| 65 | 65 | return nil | |
| 66 | 66 | } | |
M internal/adapters/sessionwire/client_test.go +2 -2
| @@ -131,10 +131,10 @@ func TestClientDialerWrapsEachConnectionAndPreservesErrors(t *testing.T) { | |||
| 131 | 131 | } | |
| 132 | 132 | ||
| 133 | 133 | type scriptedDialer struct { | |
| 134 | - | transport ports.Transport | |
| 134 | + | transport wire.Transport | |
| 135 | 135 | err error | |
| 136 | 136 | } | |
| 137 | 137 | ||
| 138 | - | func (d *scriptedDialer) Dial(context.Context) (ports.Transport, error) { | |
| 138 | + | func (d *scriptedDialer) Dial(context.Context) (wire.Transport, error) { | |
| 139 | 139 | return d.transport, d.err | |
| 140 | 140 | } | |
M internal/adapters/sessionwire/server.go +14 -14
| @@ -17,12 +17,12 @@ var ( | |||
| 17 | 17 | ErrUnsupportedSend = errors.New("sessionwire: send mode is unsupported") | |
| 18 | 18 | ) | |
| 19 | 19 | ||
| 20 | - | type serverConnection struct{ raw ports.Transport } | |
| 20 | + | type serverConnection struct{ raw wire.Transport } | |
| 21 | 21 | ||
| 22 | 22 | var _ ports.ServerConnection = (*serverConnection)(nil) | |
| 23 | 23 | ||
| 24 | 24 | // NewServerConnection wraps one raw connection incarnation exactly once. | |
| 25 | - | func NewServerConnection(raw ports.Transport) ports.ServerConnection { | |
| 25 | + | func NewServerConnection(raw wire.Transport) ports.ServerConnection { | |
| 26 | 26 | if raw == nil { | |
| 27 | 27 | return nil | |
| 28 | 28 | } | |
| @@ -71,7 +71,7 @@ func (c *serverConnection) SendServer(message protocol.ServerMessage) error { | |||
| 71 | 71 | } | |
| 72 | 72 | ||
| 73 | 73 | func (c *serverConnection) SendServerAsync(message protocol.ServerMessage) error { | |
| 74 | - | transport, ok := c.raw.(ports.AsyncTransport) | |
| 74 | + | transport, ok := c.raw.(wire.AsyncTransport) | |
| 75 | 75 | if !ok { | |
| 76 | 76 | return ErrUnsupportedSend | |
| 77 | 77 | } | |
| @@ -83,7 +83,7 @@ func (c *serverConnection) SendServerAsync(message protocol.ServerMessage) error | |||
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | 85 | func (c *serverConnection) SendServerSynchronous(message protocol.ServerMessage) error { | |
| 86 | - | transport, ok := c.raw.(ports.OwnedSynchronousTransport) | |
| 86 | + | transport, ok := c.raw.(wire.OwnedSynchronousTransport) | |
| 87 | 87 | if !ok { | |
| 88 | 88 | return ErrUnsupportedSend | |
| 89 | 89 | } | |
| @@ -103,7 +103,7 @@ func (c *serverConnection) SendOutput(output protocol.Output) error { | |||
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | 105 | func (c *serverConnection) SendOutputAsync(output protocol.Output) error { | |
| 106 | - | transport, ok := c.raw.(ports.AsyncTransport) | |
| 106 | + | transport, ok := c.raw.(wire.AsyncTransport) | |
| 107 | 107 | if !ok { | |
| 108 | 108 | return ErrUnsupportedSend | |
| 109 | 109 | } | |
| @@ -115,7 +115,7 @@ func (c *serverConnection) SendOutputAsync(output protocol.Output) error { | |||
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | 117 | func (c *serverConnection) SendOutputSynchronous(output protocol.Output) error { | |
| 118 | - | transport, ok := c.raw.(ports.OwnedSynchronousTransport) | |
| 118 | + | transport, ok := c.raw.(wire.OwnedSynchronousTransport) | |
| 119 | 119 | if !ok { | |
| 120 | 120 | return ErrUnsupportedSend | |
| 121 | 121 | } | |
| @@ -138,10 +138,10 @@ func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities { | |||
| 138 | 138 | func (c *serverConnection) LinkState() ports.LinkState { return rawLinkState(c.raw) } | |
| 139 | 139 | func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) } | |
| 140 | 140 | ||
| 141 | - | func rawCapabilities(raw ports.Transport) protocol.ConnectionCapabilities { | |
| 142 | - | _, datagram := raw.(ports.DatagramTransport) | |
| 143 | - | _, async := raw.(ports.AsyncTransport) | |
| 144 | - | _, synchronous := raw.(ports.OwnedSynchronousTransport) | |
| 141 | + | func rawCapabilities(raw wire.Transport) protocol.ConnectionCapabilities { | |
| 142 | + | _, datagram := raw.(wire.DatagramTransport) | |
| 143 | + | _, async := raw.(wire.AsyncTransport) | |
| 144 | + | _, synchronous := raw.(wire.OwnedSynchronousTransport) | |
| 145 | 145 | _, linkState := raw.(ports.LinkStateReporter) | |
| 146 | 146 | window := uint8(protocol.MaxOutputWindow) | |
| 147 | 147 | if datagram { | |
| @@ -156,14 +156,14 @@ func rawCapabilities(raw ports.Transport) protocol.ConnectionCapabilities { | |||
| 156 | 156 | } | |
| 157 | 157 | } | |
| 158 | 158 | ||
| 159 | - | func rawLinkState(raw ports.Transport) ports.LinkState { | |
| 159 | + | func rawLinkState(raw wire.Transport) ports.LinkState { | |
| 160 | 160 | if reporter, ok := raw.(ports.LinkStateReporter); ok { | |
| 161 | 161 | return reporter.LinkState() | |
| 162 | 162 | } | |
| 163 | 163 | return ports.LinkStateConnected | |
| 164 | 164 | } | |
| 165 | 165 | ||
| 166 | - | func rawLinkEvents(raw ports.Transport) <-chan ports.LinkEvent { | |
| 166 | + | func rawLinkEvents(raw wire.Transport) <-chan ports.LinkEvent { | |
| 167 | 167 | if reporter, ok := raw.(ports.LinkStateReporter); ok { | |
| 168 | 168 | return reporter.LinkEvents() | |
| 169 | 169 | } | |
| @@ -172,12 +172,12 @@ func rawLinkEvents(raw ports.Transport) <-chan ports.LinkEvent { | |||
| 172 | 172 | ||
| 173 | 173 | func (c *serverConnection) Close() error { return c.raw.Close() } | |
| 174 | 174 | ||
| 175 | - | type serverListener struct{ raw ports.Listener } | |
| 175 | + | type serverListener struct{ raw wire.Listener } | |
| 176 | 176 | ||
| 177 | 177 | var _ ports.ServerListener = (*serverListener)(nil) | |
| 178 | 178 | ||
| 179 | 179 | // NewServerListener wraps every accepted raw connection in a stable typed adapter. | |
| 180 | - | func NewServerListener(raw ports.Listener) ports.ServerListener { | |
| 180 | + | func NewServerListener(raw wire.Listener) ports.ServerListener { | |
| 181 | 181 | if raw == nil { | |
| 182 | 182 | return nil | |
| 183 | 183 | } | |
M internal/adapters/sessionwire/server_test.go +4 -4
| @@ -279,8 +279,8 @@ func TestServerListenerWrapsEachAcceptedIncarnation(t *testing.T) { | |||
| 279 | 279 | require.NoError(t, wrapped.Close()) | |
| 280 | 280 | } | |
| 281 | 281 | ||
| 282 | - | type scriptedListener struct{ transport ports.Transport } | |
| 282 | + | type scriptedListener struct{ transport wire.Transport } | |
| 283 | 283 | ||
| 284 | - | func (l *scriptedListener) Accept() (ports.Transport, error) { return l.transport, nil } | |
| 285 | - | func (*scriptedListener) Close() error { return nil } | |
| 286 | - | func (*scriptedListener) Addr() string { return "test" } | |
| 284 | + | func (l *scriptedListener) Accept() (wire.Transport, error) { return l.transport, nil } | |
| 285 | + | func (*scriptedListener) Close() error { return nil } | |
| 286 | + | func (*scriptedListener) Addr() string { return "test" } | |
M internal/adapters/sshstdio/transport.go +5 -5
| @@ -57,7 +57,7 @@ func WithRuntimeObserver(observer ports.SerializedRuntimeObserver) Option { | |||
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | 59 | // NewTransport wraps separate reader/writer streams as a framed Transport. | |
| 60 | - | func NewTransport(r io.Reader, w io.Writer, closeFn closeFunc, opts ...Option) ports.Transport { | |
| 60 | + | func NewTransport(r io.Reader, w io.Writer, closeFn closeFunc, opts ...Option) wire.Transport { | |
| 61 | 61 | t := newTransport(r, w, closeFn, nil) | |
| 62 | 62 | for _, opt := range opts { | |
| 63 | 63 | if opt != nil { | |
| @@ -533,12 +533,12 @@ func shellQuote(s string) string { | |||
| 533 | 533 | // Dial starts ssh target vev _stdio and returns a Transport over the child | |
| 534 | 534 | // process' stdio. Session selection is carried in Hello. The subprocess is | |
| 535 | 535 | // started with exec.Command argv, never through a shell. | |
| 536 | - | func Dial(target, session string) (ports.Transport, error) { | |
| 536 | + | func Dial(target, session string) (wire.Transport, error) { | |
| 537 | 537 | return DialContext(context.Background(), target, session) | |
| 538 | 538 | } | |
| 539 | 539 | ||
| 540 | 540 | // DialContextWithRuntimeObserver is the option-bearing production constructor. | |
| 541 | - | func DialContextWithRuntimeObserver(ctx context.Context, target, session string, logger *slog.Logger, opts ...Option) (ports.Transport, error) { | |
| 541 | + | func DialContextWithRuntimeObserver(ctx context.Context, target, session string, logger *slog.Logger, opts ...Option) (wire.Transport, error) { | |
| 542 | 542 | return dialContext(ctx, target, session, logger, opts...) | |
| 543 | 543 | } | |
| 544 | 544 | ||
| @@ -547,7 +547,7 @@ func DialContextWithRuntimeObserver(ctx context.Context, target, session string, | |||
| 547 | 547 | // handshake context must not kill an already-established carriage. Callers may | |
| 548 | 548 | // pass a logger to record ssh start failures and non-clean exits without logging | |
| 549 | 549 | // the generated command line. | |
| 550 | - | func DialContext(ctx context.Context, target, session string, logger ...*slog.Logger) (ports.Transport, error) { | |
| 550 | + | func DialContext(ctx context.Context, target, session string, logger ...*slog.Logger) (wire.Transport, error) { | |
| 551 | 551 | var log *slog.Logger | |
| 552 | 552 | if len(logger) > 0 { | |
| 553 | 553 | log = logger[0] | |
| @@ -555,7 +555,7 @@ func DialContext(ctx context.Context, target, session string, logger ...*slog.Lo | |||
| 555 | 555 | return dialContext(ctx, target, session, log) | |
| 556 | 556 | } | |
| 557 | 557 | ||
| 558 | - | func dialContext(ctx context.Context, target, session string, log *slog.Logger, opts ...Option) (ports.Transport, error) { | |
| 558 | + | func dialContext(ctx context.Context, target, session string, log *slog.Logger, opts ...Option) (wire.Transport, error) { | |
| 559 | 559 | if err := ctx.Err(); err != nil { | |
| 560 | 560 | return nil, err | |
| 561 | 561 | } | |
M internal/adapters/sshstdio/transport_observability_test.go +4 -4
| @@ -22,7 +22,7 @@ func TestTransportObservabilitySSHStdioEOFEndsReceive(t *testing.T) { | |||
| 22 | 22 | if err != nil { | |
| 23 | 23 | t.Fatalf("NewJSONL() error = %v", err) | |
| 24 | 24 | } | |
| 25 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 25 | + | reporter := observability.NewSerialized(observer, 64) | |
| 26 | 26 | defer reporter.Close() | |
| 27 | 27 | if _, err := NewTransport(bytes.NewReader(nil), &bytes.Buffer{}, nil, WithRuntimeObserver(reporter)).Recv(); err == nil { | |
| 28 | 28 | t.Fatal("Recv() error = nil at EOF") | |
| @@ -40,7 +40,7 @@ func TestTransportObservabilitySSHStdioCloseEndsBlockedReceive(t *testing.T) { | |||
| 40 | 40 | if err != nil { | |
| 41 | 41 | t.Fatalf("NewJSONL() error = %v", err) | |
| 42 | 42 | } | |
| 43 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 43 | + | reporter := observability.NewSerialized(observer, 64) | |
| 44 | 44 | defer reporter.Close() | |
| 45 | 45 | reader := &sshShutdownReader{entered: make(chan struct{}), done: make(chan struct{})} | |
| 46 | 46 | shutdownErr := errors.New("ssh shutdown failed") | |
| @@ -152,7 +152,7 @@ func TestTransportObservabilitySSHStdioCloseDoesNotWaitForUnownedReceive(t *test | |||
| 152 | 152 | if err != nil { | |
| 153 | 153 | t.Fatalf("NewJSONL() error = %v", err) | |
| 154 | 154 | } | |
| 155 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 155 | + | reporter := observability.NewSerialized(observer, 64) | |
| 156 | 156 | defer reporter.Close() | |
| 157 | 157 | reader := &sshUnownedBlockedReader{entered: make(chan struct{}), release: make(chan struct{})} | |
| 158 | 158 | t.Cleanup(reader.unblock) | |
| @@ -210,7 +210,7 @@ func TestTransportObservabilitySSHStdioPreservesCarriage(t *testing.T) { | |||
| 210 | 210 | if err != nil { | |
| 211 | 211 | t.Fatalf("NewJSONL() error = %v", err) | |
| 212 | 212 | } | |
| 213 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 213 | + | reporter := observability.NewSerialized(observer, 64) | |
| 214 | 214 | defer reporter.Close() | |
| 215 | 215 | ||
| 216 | 216 | frame := wire.Frame{Type: wire.MsgOutput, Payload: []byte("stdio bytes stay exact")} | |
M internal/app/cmd.go +3 -3
| @@ -129,8 +129,8 @@ var errDaemonUnreachable = errors.New("daemon not running") | |||
| 129 | 129 | type cmdDeps struct { | |
| 130 | 130 | stdout io.Writer | |
| 131 | 131 | getenv func(string) string | |
| 132 | - | dial func(context.Context, string) (ports.Transport, error) | |
| 133 | - | ensure func(context.Context, string) (ports.Transport, error) | |
| 132 | + | dial func(context.Context, string) (wire.Transport, error) | |
| 133 | + | ensure func(context.Context, string) (wire.Transport, error) | |
| 134 | 134 | clock ports.Clock | |
| 135 | 135 | } | |
| 136 | 136 | ||
| @@ -139,7 +139,7 @@ func runCmd(ctx context.Context, invocation cmdInvocation) error { | |||
| 139 | 139 | stdout: os.Stdout, | |
| 140 | 140 | getenv: os.Getenv, | |
| 141 | 141 | dial: realDial, | |
| 142 | - | ensure: func(ctx context.Context, dir string) (ports.Transport, error) { | |
| 142 | + | ensure: func(ctx context.Context, dir string) (wire.Transport, error) { | |
| 143 | 143 | return ensureDaemonWithLifecycle(ctx, dir, realDial, realSpawn, defaultBackoff) | |
| 144 | 144 | }, | |
| 145 | 145 | clock: clock.New(), | |
M internal/app/cmd_test.go +14 -15
| @@ -11,7 +11,6 @@ import ( | |||
| 11 | 11 | "testing" | |
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | - | "github.com/bnema/vev/internal/ports" | |
| 15 | 14 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 17 | 16 | "github.com/bnema/vev/internal/protocol/wire" | |
| @@ -91,7 +90,7 @@ func TestCmdHelpUsesRegistryWithoutDialing(t *testing.T) { | |||
| 91 | 90 | err := runCmdWithDeps(context.Background(), cmdInvocation{help: true}, cmdDeps{ | |
| 92 | 91 | stdout: out, | |
| 93 | 92 | getenv: func(string) string { return "" }, | |
| 94 | - | dial: func(context.Context, string) (ports.Transport, error) { | |
| 93 | + | dial: func(context.Context, string) (wire.Transport, error) { | |
| 95 | 94 | called = true | |
| 96 | 95 | return nil, errors.New("must not dial") | |
| 97 | 96 | }, | |
| @@ -146,7 +145,7 @@ func TestMoveCmdPreservesPositionalArguments(t *testing.T) { | |||
| 146 | 145 | require.NoError(t, runCmdWithDeps(context.Background(), parsed.cmd, cmdDeps{ | |
| 147 | 146 | stdout: io.Discard, | |
| 148 | 147 | getenv: func(string) string { return "" }, | |
| 149 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 148 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 150 | 149 | })) | |
| 151 | 150 | require.Len(t, transport.sent, 1) | |
| 152 | 151 | request, err := wire.UnmarshalCommandRequest(transport.sent[0].Payload) | |
| @@ -164,11 +163,11 @@ func TestRemoteCatalogCommandEnsuresDaemon(t *testing.T) { | |||
| 164 | 163 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "remote-catalog", jsonOut: true}, cmdDeps{ | |
| 165 | 164 | stdout: io.Discard, | |
| 166 | 165 | getenv: func(string) string { return "" }, | |
| 167 | - | dial: func(context.Context, string) (ports.Transport, error) { | |
| 166 | + | dial: func(context.Context, string) (wire.Transport, error) { | |
| 168 | 167 | t.Fatal("remote catalog used non-starting daemon dial") | |
| 169 | 168 | return nil, errors.New("unexpected dial") | |
| 170 | 169 | }, | |
| 171 | - | ensure: func(context.Context, string) (ports.Transport, error) { | |
| 170 | + | ensure: func(context.Context, string) (wire.Transport, error) { | |
| 172 | 171 | ensureCalls++ | |
| 173 | 172 | return transport, nil | |
| 174 | 173 | }, | |
| @@ -191,7 +190,7 @@ func TestMoveCmdInvalidArgumentResultExitsTwo(t *testing.T) { | |||
| 191 | 190 | err := runCmdWithDeps(context.Background(), invocation, cmdDeps{ | |
| 192 | 191 | stdout: io.Discard, | |
| 193 | 192 | getenv: func(string) string { return "" }, | |
| 194 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 193 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 195 | 194 | }) | |
| 196 | 195 | if ExitCode(err) != 2 { | |
| 197 | 196 | t.Fatalf("ExitCode(%v) = %d, want 2", err, ExitCode(err)) | |
| @@ -212,7 +211,7 @@ func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) { | |||
| 212 | 211 | require.NoError(t, runCmdWithDeps(context.Background(), invocation.cmd, cmdDeps{ | |
| 213 | 212 | stdout: io.Discard, | |
| 214 | 213 | getenv: func(string) string { return "session=work,tab=t_abc,pane=p_def" }, | |
| 215 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 214 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 216 | 215 | })) | |
| 217 | 216 | require.Len(t, transport.sent, 1) | |
| 218 | 217 | request, err := wire.UnmarshalCommandRequest(transport.sent[0].Payload) | |
| @@ -232,7 +231,7 @@ func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) { | |||
| 232 | 231 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right", self: true}, cmdDeps{ | |
| 233 | 232 | stdout: out, | |
| 234 | 233 | getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" }, | |
| 235 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 234 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 236 | 235 | }) | |
| 237 | 236 | if err != nil { | |
| 238 | 237 | t.Fatalf("runCmdWithDeps: %v", err) | |
| @@ -257,7 +256,7 @@ func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) { | |||
| 257 | 256 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 258 | 257 | stdout: io.Discard, | |
| 259 | 258 | getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" }, | |
| 260 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 259 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 261 | 260 | }) | |
| 262 | 261 | if err != nil { | |
| 263 | 262 | t.Fatalf("runCmdWithDeps: %v", err) | |
| @@ -276,7 +275,7 @@ func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) { | |||
| 276 | 275 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "new-tab", session: "current"}, cmdDeps{ | |
| 277 | 276 | stdout: io.Discard, | |
| 278 | 277 | getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" }, | |
| 279 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 278 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 280 | 279 | }) | |
| 281 | 280 | if err != nil { | |
| 282 | 281 | t.Fatalf("runCmdWithDeps: %v", err) | |
| @@ -298,7 +297,7 @@ func TestRunCmdRejectsTooManyArgumentsBeforeDialing(t *testing.T) { | |||
| 298 | 297 | }, cmdDeps{ | |
| 299 | 298 | stdout: io.Discard, | |
| 300 | 299 | getenv: func(string) string { return "" }, | |
| 301 | - | dial: func(context.Context, string) (ports.Transport, error) { | |
| 300 | + | dial: func(context.Context, string) (wire.Transport, error) { | |
| 302 | 301 | dialed = true | |
| 303 | 302 | return nil, errors.New("must not dial") | |
| 304 | 303 | }, | |
| @@ -330,7 +329,7 @@ func TestRunCmdTimesOutPendingRequest(t *testing.T) { | |||
| 330 | 329 | done <- runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 331 | 330 | stdout: io.Discard, | |
| 332 | 331 | getenv: func(string) string { return "" }, | |
| 333 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 332 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 334 | 333 | clock: clock, | |
| 335 | 334 | }) | |
| 336 | 335 | }() | |
| @@ -358,7 +357,7 @@ func TestRunCmdZeroRequestIDReplyReturnsDaemonError(t *testing.T) { | |||
| 358 | 357 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 359 | 358 | stdout: io.Discard, | |
| 360 | 359 | getenv: func(string) string { return "" }, | |
| 361 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 360 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 362 | 361 | }) | |
| 363 | 362 | if err == nil || err.Error() != want { | |
| 364 | 363 | t.Fatalf("error = %v, want daemon error %q", err, want) | |
| @@ -374,7 +373,7 @@ func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) { | |||
| 374 | 373 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 375 | 374 | stdout: io.Discard, | |
| 376 | 375 | getenv: func(string) string { return "" }, | |
| 377 | - | dial: func(context.Context, string) (ports.Transport, error) { | |
| 376 | + | dial: func(context.Context, string) (wire.Transport, error) { | |
| 378 | 377 | dials++ | |
| 379 | 378 | return nil, dialErr | |
| 380 | 379 | }, | |
| @@ -420,7 +419,7 @@ func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) { | |||
| 420 | 419 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 421 | 420 | stdout: io.Discard, | |
| 422 | 421 | getenv: func(string) string { return "" }, | |
| 423 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 422 | + | dial: func(context.Context, string) (wire.Transport, error) { return transport, nil }, | |
| 424 | 423 | }) | |
| 425 | 424 | if err == nil || ExitCode(err) != tt.wantCode || err.Error() != tt.result.Text { | |
| 426 | 425 | t.Fatalf("error=%v code=%d, want message %q and code %d", err, ExitCode(err), tt.result.Text, tt.wantCode) | |
M internal/app/daemon_integration_test.go +8 -8
| @@ -66,10 +66,10 @@ func startDaemonInDir(t *testing.T, dir string, opts ...daemon.Option) (string, | |||
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | 68 | // pump wires a background Recv pump to a transport so tests can await frames | |
| 69 | - | // with a timeout (ports.Transport has no read deadline). | |
| 69 | + | // with a timeout (wire.Transport has no read deadline). | |
| 70 | 70 | type pump struct{ ch chan wire.Frame } | |
| 71 | 71 | ||
| 72 | - | func recvPump(tr ports.Transport) *pump { | |
| 72 | + | func recvPump(tr wire.Transport) *pump { | |
| 73 | 73 | p := &pump{ch: make(chan wire.Frame, 128)} | |
| 74 | 74 | go func() { | |
| 75 | 75 | for { | |
| @@ -85,12 +85,12 @@ func recvPump(tr ports.Transport) *pump { | |||
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | 87 | // attach dials, handshakes, and returns the transport plus its frame pump. | |
| 88 | - | func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) (ports.Transport, *pump) { | |
| 88 | + | func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) (wire.Transport, *pump) { | |
| 89 | 89 | t.Helper() | |
| 90 | 90 | return attachWithEnvironment(t, dir, intent, name, sz, nil) | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | - | func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, sz domain.Size, env []string) (ports.Transport, *pump) { | |
| 93 | + | func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, sz domain.Size, env []string) (wire.Transport, *pump) { | |
| 94 | 94 | t.Helper() | |
| 95 | 95 | tr, err := ipc.DialContext(context.Background(), dir) | |
| 96 | 96 | require.NoError(t, err) | |
| @@ -263,7 +263,7 @@ func shellFixture(t *testing.T, label string) string { | |||
| 263 | 263 | return path | |
| 264 | 264 | } | |
| 265 | 265 | ||
| 266 | - | func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) { | |
| 266 | + | func assertChildEnvironment(t *testing.T, tr wire.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) { | |
| 267 | 267 | t.Helper() | |
| 268 | 268 | command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n" | |
| 269 | 269 | require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{Data: []byte(command)})})) | |
| @@ -531,7 +531,7 @@ func TestMultipleClientsOneLifecycleOwner(t *testing.T) { | |||
| 531 | 531 | ||
| 532 | 532 | ready := make(chan struct{}) | |
| 533 | 533 | transport := &integrationTransport{} | |
| 534 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 534 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 535 | 535 | select { | |
| 536 | 536 | case <-ready: | |
| 537 | 537 | return transport, nil | |
| @@ -1075,7 +1075,7 @@ func TestLifecycleManualTimerRepeatedFireIsNonBlocking(t *testing.T) { | |||
| 1075 | 1075 | } | |
| 1076 | 1076 | ||
| 1077 | 1077 | type lifecycleObservedListener struct { | |
| 1078 | - | ports.Listener | |
| 1078 | + | wire.Listener | |
| 1079 | 1079 | closed chan struct{} | |
| 1080 | 1080 | once sync.Once | |
| 1081 | 1081 | } | |
| @@ -1249,7 +1249,7 @@ func assertNewDaemonStartupPending(t *testing.T, durableOpen, listen <-chan stru | |||
| 1249 | 1249 | } | |
| 1250 | 1250 | ||
| 1251 | 1251 | type lifecycleControlledListener struct { | |
| 1252 | - | ports.Listener | |
| 1252 | + | wire.Listener | |
| 1253 | 1253 | entered chan struct{} | |
| 1254 | 1254 | proceed chan struct{} | |
| 1255 | 1255 | closed chan struct{} | |
M internal/app/final_acceptance_test.go +2 -3
| @@ -7,14 +7,13 @@ import ( | |||
| 7 | 7 | "time" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/domain" | |
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol" | |
| 12 | 11 | "github.com/bnema/vev/internal/protocol/wire" | |
| 13 | 12 | "github.com/bnema/vev/internal/usecase/daemon" | |
| 14 | 13 | "github.com/stretchr/testify/require" | |
| 15 | 14 | ) | |
| 16 | 15 | ||
| 17 | - | func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) { | |
| 16 | + | func sendAcceptanceInput(t *testing.T, tr wire.Transport, data string) { | |
| 18 | 17 | t.Helper() | |
| 19 | 18 | require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{Data: []byte(data)})})) | |
| 20 | 19 | } | |
| @@ -60,7 +59,7 @@ func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(protocol.Output | |||
| 60 | 59 | return output | |
| 61 | 60 | } | |
| 62 | 61 | ||
| 63 | - | func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) protocol.CommandResult { | |
| 62 | + | func awaitAcceptanceCommandResult(t *testing.T, tr wire.Transport, p *pump, requestID uint64, slug string) protocol.CommandResult { | |
| 64 | 63 | t.Helper() | |
| 65 | 64 | payload, err := wire.MarshalCommandRequest(protocol.CommandRequest{ | |
| 66 | 65 | Version: protocol.Version, RequestID: requestID, Attached: true, Slug: slug, | |
A internal/app/remote_dialer_factory_test.go +78 -0
| @@ -0,0 +1,78 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "log/slog" | |
| 6 | + | "sync" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | remoteadapter "github.com/bnema/vev/internal/adapters/remote" | |
| 10 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | type remoteDialerFactoryMock struct { | |
| 14 | + | mu sync.Mutex | |
| 15 | + | calls []*remoteDialerFactoryCall | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | type remoteDialerFactoryExpecter struct{ mock *remoteDialerFactoryMock } | |
| 19 | + | ||
| 20 | + | type remoteDialerFactoryCall struct { | |
| 21 | + | target, session string | |
| 22 | + | mode remoteadapter.TransportMode | |
| 23 | + | dialer wire.Dialer | |
| 24 | + | err error | |
| 25 | + | remaining int | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func newRemoteDialerFactoryMock(t *testing.T) *remoteDialerFactoryMock { | |
| 29 | + | t.Helper() | |
| 30 | + | m := &remoteDialerFactoryMock{} | |
| 31 | + | t.Cleanup(func() { | |
| 32 | + | m.mu.Lock() | |
| 33 | + | defer m.mu.Unlock() | |
| 34 | + | for _, call := range m.calls { | |
| 35 | + | if call.remaining != 0 { | |
| 36 | + | t.Errorf("unmet DialerForRemote(%q, %q, %q) calls: %d", call.target, call.session, call.mode, call.remaining) | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | }) | |
| 40 | + | return m | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (m *remoteDialerFactoryMock) EXPECT() *remoteDialerFactoryExpecter { | |
| 44 | + | return &remoteDialerFactoryExpecter{mock: m} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func (e *remoteDialerFactoryExpecter) DialerForRemote(target, session string, mode remoteadapter.TransportMode, _ any) *remoteDialerFactoryCall { | |
| 48 | + | call := &remoteDialerFactoryCall{target: target, session: session, mode: mode, remaining: 1} | |
| 49 | + | e.mock.mu.Lock() | |
| 50 | + | e.mock.calls = append(e.mock.calls, call) | |
| 51 | + | e.mock.mu.Unlock() | |
| 52 | + | return call | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | func (c *remoteDialerFactoryCall) Return(dialer wire.Dialer, err error) *remoteDialerFactoryCall { | |
| 56 | + | c.dialer, c.err = dialer, err | |
| 57 | + | return c | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func (c *remoteDialerFactoryCall) Once() *remoteDialerFactoryCall { return c.Times(1) } | |
| 61 | + | ||
| 62 | + | func (c *remoteDialerFactoryCall) Times(n int) *remoteDialerFactoryCall { | |
| 63 | + | c.remaining = n | |
| 64 | + | return c | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (m *remoteDialerFactoryMock) DialerForRemote(target, session string, mode remoteadapter.TransportMode, _ *slog.Logger) (wire.Dialer, error) { | |
| 68 | + | m.mu.Lock() | |
| 69 | + | defer m.mu.Unlock() | |
| 70 | + | for _, call := range m.calls { | |
| 71 | + | if call.remaining == 0 || call.target != target || call.session != session || call.mode != mode { | |
| 72 | + | continue | |
| 73 | + | } | |
| 74 | + | call.remaining-- | |
| 75 | + | return call.dialer, call.err | |
| 76 | + | } | |
| 77 | + | return nil, fmt.Errorf("unexpected DialerForRemote(%q, %q, %q)", target, session, mode) | |
| 78 | + | } | |
M internal/app/remote_final_acceptance_test.go +7 -7
| @@ -11,9 +11,9 @@ import ( | |||
| 11 | 11 | "sync" | |
| 12 | 12 | "testing" | |
| 13 | 13 | ||
| 14 | + | remoteadapter "github.com/bnema/vev/internal/adapters/remote" | |
| 14 | 15 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 15 | 16 | "github.com/bnema/vev/internal/domain" | |
| 16 | - | "github.com/bnema/vev/internal/ports" | |
| 17 | 17 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 18 | 18 | "github.com/bnema/vev/internal/protocol" | |
| 19 | 19 | "github.com/bnema/vev/internal/protocol/wire" | |
| @@ -28,7 +28,7 @@ type acceptanceRemoteFactory struct { | |||
| 28 | 28 | outputs chan string | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | - | func (f *acceptanceRemoteFactory) DialerForRemote(target string, _ string, _ ports.RemoteTransportMode, _ *slog.Logger) (ports.Dialer, error) { | |
| 31 | + | func (f *acceptanceRemoteFactory) DialerForRemote(target string, _ string, _ remoteadapter.TransportMode, _ *slog.Logger) (wire.Dialer, error) { | |
| 32 | 32 | f.mu.Lock() | |
| 33 | 33 | f.calls = append(f.calls, target) | |
| 34 | 34 | targetHandoff := f.handoff[target] | |
| @@ -42,7 +42,7 @@ type acceptanceRemoteDialer struct { | |||
| 42 | 42 | outputs chan<- string | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | - | func (d acceptanceRemoteDialer) Dial(context.Context) (ports.Transport, error) { | |
| 45 | + | func (d acceptanceRemoteDialer) Dial(context.Context) (wire.Transport, error) { | |
| 46 | 46 | clientConn, serverConn := net.Pipe() | |
| 47 | 47 | clientTransport := sshstdio.NewTransport(clientConn, clientConn, clientConn.Close) | |
| 48 | 48 | serverTransport := sshstdio.NewTransport(serverConn, serverConn, serverConn.Close) | |
| @@ -50,7 +50,7 @@ func (d acceptanceRemoteDialer) Dial(context.Context) (ports.Transport, error) { | |||
| 50 | 50 | return clientTransport, nil | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | - | func serveAcceptanceRemote(tr ports.Transport, target string, handoff protocol.AttachTarget, outputs chan<- string) { | |
| 53 | + | func serveAcceptanceRemote(tr wire.Transport, target string, handoff protocol.AttachTarget, outputs chan<- string) { | |
| 54 | 54 | defer func() { _ = tr.Close() }() | |
| 55 | 55 | welcomed := false | |
| 56 | 56 | for { | |
| @@ -125,12 +125,12 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | |||
| 125 | 125 | terminals := make([]*acceptanceRemoteTerminal, 0, 3) | |
| 126 | 126 | deps := runAttachDeps{ | |
| 127 | 127 | hostStore: hostStore, | |
| 128 | - | localDialer: func() ports.Dialer { | |
| 128 | + | localDialer: func() wire.Dialer { | |
| 129 | 129 | localCalls++ | |
| 130 | 130 | return acceptanceRemoteDialer{} | |
| 131 | 131 | }, | |
| 132 | - | remoteDialerFactory: factory, | |
| 133 | - | selectedRemoteTransport: string(ports.RemoteTransportStdio), | |
| 132 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 133 | + | selectedRemoteTransport: string(remoteadapter.TransportStdio), | |
| 134 | 134 | runClient: func(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| 135 | 135 | term := &acceptanceRemoteTerminal{in: bytes.NewReader(nil)} | |
| 136 | 136 | terminals = append(terminals, term) | |
M internal/app/remote_hosts.go +6 -5
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | "github.com/bnema/vev/internal/platform" | |
| 16 | 16 | "github.com/bnema/vev/internal/ports" | |
| 17 | 17 | "github.com/bnema/vev/internal/protocol" | |
| 18 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| 20 | 21 | const ( | |
| @@ -196,21 +197,21 @@ func listAllSessions(ctx context.Context, deps remoteHostDeps, hosts []domain.Re | |||
| 196 | 197 | return nil | |
| 197 | 198 | } | |
| 198 | 199 | ||
| 199 | - | func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) []protocol.SessionInfo { | |
| 200 | + | func catalogSessionsAsInfo(host string, sessions []catalogue.RemoteCatalogSession) []protocol.SessionInfo { | |
| 200 | 201 | out := make([]protocol.SessionInfo, 0, len(sessions)) | |
| 201 | 202 | for _, session := range sessions { | |
| 202 | 203 | info := protocol.SessionInfo{ | |
| 203 | 204 | Name: domain.RemoteSessionDisplay(session.Name, host), | |
| 204 | 205 | Ephemeral: session.Ephemeral, | |
| 205 | - | Tabs: ports.SaturateUint16(ports.CatalogTabCount(session)), | |
| 206 | + | Tabs: catalogue.SaturateUint16(catalogue.CatalogTabCount(session)), | |
| 206 | 207 | Attached: session.Attached, | |
| 207 | 208 | } | |
| 208 | 209 | switch session.State { | |
| 209 | - | case ports.RemoteCatalogSessionUp: | |
| 210 | + | case catalogue.RemoteCatalogSessionUp: | |
| 210 | 211 | info.State = protocol.SessionUp | |
| 211 | - | case ports.RemoteCatalogSessionDown: | |
| 212 | + | case catalogue.RemoteCatalogSessionDown: | |
| 212 | 213 | info.State = protocol.SessionDown | |
| 213 | - | case ports.RemoteCatalogSessionBroken: | |
| 214 | + | case catalogue.RemoteCatalogSessionBroken: | |
| 214 | 215 | info.State = protocol.SessionBroken | |
| 215 | 216 | default: | |
| 216 | 217 | slog.Debug("remote catalog session has unknown state", "host", host, "session", session.Name, "state", session.State) | |
M internal/app/remote_test.go +19 -17
| @@ -8,10 +8,12 @@ import ( | |||
| 8 | 8 | "strings" | |
| 9 | 9 | "testing" | |
| 10 | 10 | ||
| 11 | + | remoteadapter "github.com/bnema/vev/internal/adapters/remote" | |
| 11 | 12 | "github.com/bnema/vev/internal/domain" | |
| 12 | 13 | "github.com/bnema/vev/internal/ports" | |
| 13 | 14 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 14 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 15 | 17 | "github.com/bnema/vev/internal/protocol/wire" | |
| 16 | 18 | "github.com/bnema/vev/internal/usecase/client" | |
| 17 | 19 | "github.com/stretchr/testify/mock" | |
| @@ -173,7 +175,7 @@ func TestRemoteHostListingUsesUnifiedStore(t *testing.T) { | |||
| 173 | 175 | store := portsmocks.NewMockRemoteHostStore(t) | |
| 174 | 176 | store.EXPECT().Hosts().Return([]string{"arch"}, []string{"beta"}, nil).Once() | |
| 175 | 177 | catalog := portsmocks.NewMockRemoteCatalogClient(t) | |
| 176 | - | catalog.EXPECT().List(mock.Anything, "arch").Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "build", State: "up"}}}, nil).Once() | |
| 178 | + | catalog.EXPECT().List(mock.Anything, "arch").Return(catalogue.RemoteCatalog{Sessions: []catalogue.RemoteCatalogSession{{Name: "build", State: "up"}}}, nil).Once() | |
| 177 | 179 | var out bytes.Buffer | |
| 178 | 180 | err := runRemoteList(context.Background(), command{listHost: "arch"}, remoteHostDeps{ | |
| 179 | 181 | store: store, | |
| @@ -210,9 +212,9 @@ func TestListAllSessionsInvariants(t *testing.T) { | |||
| 210 | 212 | ctx, cancel := context.WithCancel(context.Background()) | |
| 211 | 213 | defer cancel() | |
| 212 | 214 | catalog := portsmocks.NewMockRemoteCatalogClient(t) | |
| 213 | - | catalog.EXPECT().List(mock.Anything, "arch").RunAndReturn(func(context.Context, string) (ports.RemoteCatalog, error) { | |
| 215 | + | catalog.EXPECT().List(mock.Anything, "arch").RunAndReturn(func(context.Context, string) (catalogue.RemoteCatalog, error) { | |
| 214 | 216 | cancel() | |
| 215 | - | return ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil | |
| 217 | + | return catalogue.RemoteCatalog{Sessions: []catalogue.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil | |
| 216 | 218 | }).Once() | |
| 217 | 219 | var out bytes.Buffer | |
| 218 | 220 | err := listAllSessions(ctx, remoteHostDeps{ | |
| @@ -235,10 +237,10 @@ func TestListAllSessionsInvariants(t *testing.T) { | |||
| 235 | 237 | var calls []string | |
| 236 | 238 | catalog.EXPECT().List(mock.Anything, "arch").Run(func(_ context.Context, target string) { | |
| 237 | 239 | calls = append(calls, target) | |
| 238 | - | }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil).Once() | |
| 240 | + | }).Return(catalogue.RemoteCatalog{Sessions: []catalogue.RemoteCatalogSession{{Name: "dev", State: "up"}}}, nil).Once() | |
| 239 | 241 | catalog.EXPECT().List(mock.Anything, "mule").Run(func(_ context.Context, target string) { | |
| 240 | 242 | calls = append(calls, target) | |
| 241 | - | }).Return(ports.RemoteCatalog{Sessions: []ports.RemoteCatalogSession{{Name: "ops", State: "up"}}}, nil).Once() | |
| 243 | + | }).Return(catalogue.RemoteCatalog{Sessions: []catalogue.RemoteCatalogSession{{Name: "ops", State: "up"}}}, nil).Once() | |
| 242 | 244 | var out bytes.Buffer | |
| 243 | 245 | err := listAllSessions(context.Background(), remoteHostDeps{ | |
| 244 | 246 | catalog: catalog, | |
| @@ -263,18 +265,18 @@ func TestListAllSessionsInvariants(t *testing.T) { | |||
| 263 | 265 | func TestCatalogSessionsAsInfoInvariants(t *testing.T) { | |
| 264 | 266 | tests := []struct { | |
| 265 | 267 | name string | |
| 266 | - | session ports.RemoteCatalogSession | |
| 268 | + | session catalogue.RemoteCatalogSession | |
| 267 | 269 | wantState protocol.SessionState | |
| 268 | 270 | wantTabs uint16 | |
| 269 | 271 | }{ | |
| 270 | - | {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionUp, wantTabs: 2}, | |
| 271 | - | {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionDown, wantTabs: 2}, | |
| 272 | - | {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2}, | |
| 273 | - | {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2}, | |
| 272 | + | {name: "up", session: catalogue.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []catalogue.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionUp, wantTabs: 2}, | |
| 273 | + | {name: "down", session: catalogue.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []catalogue.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionDown, wantTabs: 2}, | |
| 274 | + | {name: "broken", session: catalogue.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []catalogue.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2}, | |
| 275 | + | {name: "unknown fails closed", session: catalogue.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []catalogue.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2}, | |
| 274 | 276 | } | |
| 275 | 277 | for _, tt := range tests { | |
| 276 | 278 | t.Run(tt.name, func(t *testing.T) { | |
| 277 | - | infos := catalogSessionsAsInfo("user@arch", []ports.RemoteCatalogSession{tt.session}) | |
| 279 | + | infos := catalogSessionsAsInfo("user@arch", []catalogue.RemoteCatalogSession{tt.session}) | |
| 278 | 280 | require.Len(t, infos, 1) | |
| 279 | 281 | require.Equal(t, "dev@arch", infos[0].Name) | |
| 280 | 282 | require.Equal(t, tt.wantState, infos[0].State) | |
| @@ -286,12 +288,12 @@ func TestCatalogSessionsAsInfoInvariants(t *testing.T) { | |||
| 286 | 288 | func TestRunAttachWithDepsRemoteLearning(t *testing.T) { | |
| 287 | 289 | store := portsmocks.NewMockRemoteHostStore(t) | |
| 288 | 290 | store.EXPECT().Remember("build@mule").Return(nil).Once() | |
| 289 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 290 | - | factory.EXPECT().DialerForRemote("build@mule", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 291 | + | factory := newRemoteDialerFactoryMock(t) | |
| 292 | + | factory.EXPECT().DialerForRemote("build@mule", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 291 | 293 | ||
| 292 | 294 | var learner ports.RemoteHostLearner | |
| 293 | 295 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "build@mule", "", nil, runAttachDeps{ | |
| 294 | - | remoteDialerFactory: factory, | |
| 296 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 295 | 297 | hostStore: store, | |
| 296 | 298 | runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error { | |
| 297 | 299 | learner = deps.RemoteHostLearner | |
| @@ -306,11 +308,11 @@ func TestRunAttachWithDepsRemoteLearning(t *testing.T) { | |||
| 306 | 308 | func TestRunAttachWithDepsAlwaysLearnsRemoteHost(t *testing.T) { | |
| 307 | 309 | store := portsmocks.NewMockRemoteHostStore(t) | |
| 308 | 310 | store.EXPECT().Remember("arch").Return(nil).Once() | |
| 309 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 310 | - | factory.EXPECT().DialerForRemote("arch", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 311 | + | factory := newRemoteDialerFactoryMock(t) | |
| 312 | + | factory.EXPECT().DialerForRemote("arch", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 311 | 313 | var learner ports.RemoteHostLearner | |
| 312 | 314 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "arch", "", nil, runAttachDeps{ | |
| 313 | - | remoteDialerFactory: factory, | |
| 315 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 314 | 316 | hostStore: store, | |
| 315 | 317 | runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error { | |
| 316 | 318 | learner = deps.RemoteHostLearner | |
M internal/app/run.go +33 -33
| @@ -132,7 +132,7 @@ var ( | |||
| 132 | 132 | date = "unknown" | |
| 133 | 133 | ||
| 134 | 134 | openCatalogue = persist.OpenOrCreate | |
| 135 | - | listenDaemon = func(dir string, observer ports.SerializedRuntimeObserver) (ports.Listener, error) { | |
| 135 | + | listenDaemon = func(dir string, observer ports.SerializedRuntimeObserver) (wire.Listener, error) { | |
| 136 | 136 | return ipc.Listen(dir, ipc.WithRuntimeObserver(observer)) | |
| 137 | 137 | } | |
| 138 | 138 | ) | |
| @@ -343,6 +343,8 @@ func parseHostArgs(args []string) (command, error) { | |||
| 343 | 343 | } | |
| 344 | 344 | } | |
| 345 | 345 | ||
| 346 | + | type remoteDialerForTarget func(target, session string, mode remoteadapter.TransportMode, log *slog.Logger) (wire.Dialer, error) | |
| 347 | + | ||
| 346 | 348 | // performanceTrace creates one serialized timestamp owner for this process. | |
| 347 | 349 | // An empty trace environment leaves all production behavior and wire bytes | |
| 348 | 350 | // unchanged. | |
| @@ -354,8 +356,8 @@ var ( | |||
| 354 | 356 | newRemoteCatalogCache = remoteadapter.NewFileCatalogCache | |
| 355 | 357 | newRemoteCatalogClient = remoteadapter.NewCatalogClient | |
| 356 | 358 | newRemotePreviewClient = remoteadapter.NewPreviewClient | |
| 357 | - | newRemoteDialerFactoryWithRuntimeObserver = func(observer ports.SerializedRuntimeObserver) ports.RemoteDialerFactory { | |
| 358 | - | return remoteadapter.NewDialerFactoryWithRuntimeObserver(observer) | |
| 359 | + | newRemoteDialerFactoryWithRuntimeObserver = func(observer ports.SerializedRuntimeObserver) remoteDialerForTarget { | |
| 360 | + | return remoteadapter.NewDialerFactoryWithRuntimeObserver(observer).DialerForRemote | |
| 359 | 361 | } | |
| 360 | 362 | runClientWithDeps runClientFunc = func( | |
| 361 | 363 | ctx context.Context, | |
| @@ -408,7 +410,7 @@ func performanceTraceWithFactories( | |||
| 408 | 410 | setupErr := fmt.Errorf("configure runtime trace correlation: %w", err) | |
| 409 | 411 | return nil, nil, closeTraceAfterSetupFailure(closer, setupErr) | |
| 410 | 412 | } | |
| 411 | - | reporter := ports.NewSerializedRuntimeObserver(observer, runtimeTraceQueueDepth) | |
| 413 | + | reporter := observability.NewSerialized(observer, runtimeTraceQueueDepth) | |
| 412 | 414 | return reporter, &runtimeTraceCloser{reporter: reporter, closer: closer}, nil | |
| 413 | 415 | } | |
| 414 | 416 | ||
| @@ -620,8 +622,8 @@ func logStartupRecoveryCounts(log *slog.Logger, records []domain.CatalogueRecord | |||
| 620 | 622 | func constructDaemonBeforeSocketPublication( | |
| 621 | 623 | construct func() *daemon.Daemon, | |
| 622 | 624 | prepare func(*daemon.Daemon) error, | |
| 623 | - | listen func() (ports.Listener, error), | |
| 624 | - | ) (*daemon.Daemon, ports.Listener, error) { | |
| 625 | + | listen func() (wire.Listener, error), | |
| 626 | + | ) (*daemon.Daemon, wire.Listener, error) { | |
| 625 | 627 | d := construct() | |
| 626 | 628 | if err := prepare(d); err != nil { | |
| 627 | 629 | return d, nil, err | |
| @@ -648,7 +650,7 @@ func runDaemonOwnedWithLogger(ctx context.Context, log *slog.Logger) (retErr err | |||
| 648 | 650 | if observerCloser != nil { | |
| 649 | 651 | defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }() | |
| 650 | 652 | } | |
| 651 | - | remoteDiscoveryOpt, err := remoteDiscoveryDaemonOption(platform.StateDir(), observer, os.Getenv(envRemoteTransport)) | |
| 653 | + | remoteDiscoveryOpt, err := remoteDiscoveryDaemonOption(platform.StateDir(), os.Getenv(envRemoteTransport)) | |
| 652 | 654 | if err != nil { | |
| 653 | 655 | return err | |
| 654 | 656 | } | |
| @@ -723,7 +725,7 @@ func runDaemonOwnedWithLogger(ctx context.Context, log *slog.Logger) (retErr err | |||
| 723 | 725 | } | |
| 724 | 726 | return nil | |
| 725 | 727 | }, | |
| 726 | - | func() (ports.Listener, error) { return listenDaemon(ipc.SocketDir(), observer) }, | |
| 728 | + | func() (wire.Listener, error) { return listenDaemon(ipc.SocketDir(), observer) }, | |
| 727 | 729 | ) | |
| 728 | 730 | if err != nil { | |
| 729 | 731 | closeErr := opened.Catalogue.Close() | |
| @@ -779,7 +781,7 @@ func runAttach(ctx context.Context, intent uint8, name, remoteTarget string) (re | |||
| 779 | 781 | defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }() | |
| 780 | 782 | } | |
| 781 | 783 | return runAttachWithDeps(ctx, intent, name, remoteTarget, os.Getenv("VEV"), log, runAttachDeps{ | |
| 782 | - | localDialer: func() ports.Dialer { | |
| 784 | + | localDialer: func() wire.Dialer { | |
| 783 | 785 | return localDaemonDialer{dir: ipc.SocketDir(), observer: observer} | |
| 784 | 786 | }, | |
| 785 | 787 | remoteDialerFactory: newRemoteDialerFactoryWithRuntimeObserver(observer), | |
| @@ -794,17 +796,17 @@ func runAttach(ctx context.Context, intent uint8, name, remoteTarget string) (re | |||
| 794 | 796 | ||
| 795 | 797 | const envRemoteTransport = "VEV_REMOTE_TRANSPORT" | |
| 796 | 798 | ||
| 797 | - | func defaultLocalDialer() ports.Dialer { return localDaemonDialer{dir: ipc.SocketDir()} } | |
| 799 | + | func defaultLocalDialer() wire.Dialer { return localDaemonDialer{dir: ipc.SocketDir()} } | |
| 798 | 800 | ||
| 799 | - | func defaultRemoteDialerFactory() ports.RemoteDialerFactory { | |
| 800 | - | return remoteadapter.NewDialerFactory() | |
| 801 | + | func defaultRemoteDialerFactory() remoteDialerForTarget { | |
| 802 | + | return remoteadapter.NewDialerFactory().DialerForRemote | |
| 801 | 803 | } | |
| 802 | 804 | ||
| 803 | 805 | type runClientFunc func(context.Context, client.Dependencies, client.AttachRequest) error | |
| 804 | 806 | ||
| 805 | 807 | type runAttachDeps struct { | |
| 806 | - | localDialer func() ports.Dialer | |
| 807 | - | remoteDialerFactory ports.RemoteDialerFactory | |
| 808 | + | localDialer func() wire.Dialer | |
| 809 | + | remoteDialerFactory remoteDialerForTarget | |
| 808 | 810 | selectedRemoteTransport string | |
| 809 | 811 | runClient runClientFunc | |
| 810 | 812 | createDetached func(context.Context, string) error | |
| @@ -819,14 +821,14 @@ type runAttachDeps struct { | |||
| 819 | 821 | hostStore ports.RemoteHostStore | |
| 820 | 822 | } | |
| 821 | 823 | ||
| 822 | - | func remoteTransportModeFromEnv(value string) (ports.RemoteTransportMode, error) { | |
| 824 | + | func remoteTransportModeFromEnv(value string) (remoteadapter.TransportMode, error) { | |
| 823 | 825 | switch value { | |
| 824 | - | case "", string(ports.RemoteTransportUDP): | |
| 825 | - | return ports.RemoteTransportUDP, nil | |
| 826 | - | case string(ports.RemoteTransportStdio): | |
| 827 | - | return ports.RemoteTransportStdio, nil | |
| 826 | + | case "", string(remoteadapter.TransportUDP): | |
| 827 | + | return remoteadapter.TransportUDP, nil | |
| 828 | + | case string(remoteadapter.TransportStdio): | |
| 829 | + | return remoteadapter.TransportStdio, nil | |
| 828 | 830 | default: | |
| 829 | - | return "", fmt.Errorf("vev: invalid remote transport %q (want %q or %q)", value, ports.RemoteTransportUDP, ports.RemoteTransportStdio) | |
| 831 | + | return "", fmt.Errorf("vev: invalid remote transport %q (want %q or %q)", value, remoteadapter.TransportUDP, remoteadapter.TransportStdio) | |
| 830 | 832 | } | |
| 831 | 833 | } | |
| 832 | 834 | ||
| @@ -853,8 +855,8 @@ func validateRemoteAttachHandoff(target protocol.AttachTarget) error { | |||
| 853 | 855 | ||
| 854 | 856 | // remoteDiscoveryDaemonOption constructs the daemon-owned discovery ports from | |
| 855 | 857 | // the same validated transport selection used by direct remote attach. | |
| 856 | - | func remoteDiscoveryDaemonOption(stateDir string, observer ports.SerializedRuntimeObserver, transport string) (daemon.Option, error) { | |
| 857 | - | mode, err := remoteTransportModeFromEnv(transport) | |
| 858 | + | func remoteDiscoveryDaemonOption(stateDir, transport string) (daemon.Option, error) { | |
| 859 | + | _, err := remoteTransportModeFromEnv(transport) | |
| 858 | 860 | if err != nil { | |
| 859 | 861 | return nil, err | |
| 860 | 862 | } | |
| @@ -862,8 +864,6 @@ func remoteDiscoveryDaemonOption(stateDir string, observer ports.SerializedRunti | |||
| 862 | 864 | newRemoteHostStore(remoteadapter.HostStorePath(stateDir)), | |
| 863 | 865 | newRemoteCatalogClient(), | |
| 864 | 866 | newRemoteCatalogCache(remoteadapter.CatalogCachePath(stateDir)), | |
| 865 | - | newRemoteDialerFactoryWithRuntimeObserver(observer), | |
| 866 | - | mode, | |
| 867 | 867 | ) | |
| 868 | 868 | preview := daemon.WithRemotePreview(newRemotePreviewClient()) | |
| 869 | 869 | return func(d *daemon.Daemon) { | |
| @@ -929,7 +929,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac | |||
| 929 | 929 | copyTarget := *target.RemoteTarget | |
| 930 | 930 | selection = ©Target | |
| 931 | 931 | } | |
| 932 | - | dialer, err := factory.DialerForRemote(target.Endpoint, target.Session, mode, log) | |
| 932 | + | dialer, err := factory(target.Endpoint, target.Session, mode, log) | |
| 933 | 933 | if err != nil { | |
| 934 | 934 | return nil, client.AttachRequest{}, err | |
| 935 | 935 | } | |
| @@ -957,7 +957,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac | |||
| 957 | 957 | if log != nil { | |
| 958 | 958 | log.Info("attaching to remote session", "target", remoteTarget, "name", name, "transport", string(mode)) | |
| 959 | 959 | } | |
| 960 | - | dialer, dialErr := factory.DialerForRemote(remoteTarget, name, mode, log) | |
| 960 | + | dialer, dialErr := factory(remoteTarget, name, mode, log) | |
| 961 | 961 | if dialErr != nil { | |
| 962 | 962 | return dialErr | |
| 963 | 963 | } | |
| @@ -1043,10 +1043,10 @@ type localDaemonDialer struct { | |||
| 1043 | 1043 | observer ports.SerializedRuntimeObserver | |
| 1044 | 1044 | } | |
| 1045 | 1045 | ||
| 1046 | - | func (d localDaemonDialer) Dial(ctx context.Context) (ports.Transport, error) { | |
| 1046 | + | func (d localDaemonDialer) Dial(ctx context.Context) (wire.Transport, error) { | |
| 1047 | 1047 | dial := realDial | |
| 1048 | 1048 | if d.observer != nil { | |
| 1049 | - | dial = func(ctx context.Context, dir string) (ports.Transport, error) { | |
| 1049 | + | dial = func(ctx context.Context, dir string) (wire.Transport, error) { | |
| 1050 | 1050 | return ipc.DialContext(ctx, dir, ipc.WithRuntimeObserver(d.observer)) | |
| 1051 | 1051 | } | |
| 1052 | 1052 | } | |
| @@ -1161,7 +1161,7 @@ func runStdio(ctx context.Context) (retErr error) { | |||
| 1161 | 1161 | if observerCloser != nil { | |
| 1162 | 1162 | defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }() | |
| 1163 | 1163 | } | |
| 1164 | - | transport, err := ensureDaemonWithLifecycle(ctx, ipc.SocketDir(), func(ctx context.Context, dir string) (ports.Transport, error) { | |
| 1164 | + | transport, err := ensureDaemonWithLifecycle(ctx, ipc.SocketDir(), func(ctx context.Context, dir string) (wire.Transport, error) { | |
| 1165 | 1165 | return ipc.DialContext(ctx, dir, ipc.WithRuntimeObserver(observer)) | |
| 1166 | 1166 | }, realSpawn, defaultBackoff) | |
| 1167 | 1167 | if err != nil { | |
| @@ -1293,7 +1293,7 @@ func runUDPProxy(ctx context.Context, ready io.Writer) (retErr error) { | |||
| 1293 | 1293 | return err | |
| 1294 | 1294 | } | |
| 1295 | 1295 | defer func() { _ = conn.Close() }() | |
| 1296 | - | daemonTr, err := ensureDaemonWithLifecycle(ctx, ipc.SocketDir(), func(ctx context.Context, dir string) (ports.Transport, error) { | |
| 1296 | + | daemonTr, err := ensureDaemonWithLifecycle(ctx, ipc.SocketDir(), func(ctx context.Context, dir string) (wire.Transport, error) { | |
| 1297 | 1297 | return ipc.DialContext(ctx, dir, ipc.WithRuntimeObserver(observer)) | |
| 1298 | 1298 | }, realSpawn, defaultBackoff) | |
| 1299 | 1299 | if err != nil { | |
| @@ -1429,15 +1429,15 @@ func listenUDPInRange(ctx context.Context, r udpPortRange) (net.PacketConn, erro | |||
| 1429 | 1429 | ||
| 1430 | 1430 | // runStdioProxy forwards the framed transport without interpreting protocol | |
| 1431 | 1431 | // messages. Session selection remains in the Hello sent by the thin client. | |
| 1432 | - | func runStdioProxy(ctx context.Context, client, daemon ports.Transport, log *slog.Logger) error { | |
| 1432 | + | func runStdioProxy(ctx context.Context, client, daemon wire.Transport, log *slog.Logger) error { | |
| 1433 | 1433 | return proxyTransports(ctx, client, daemon, log) | |
| 1434 | 1434 | } | |
| 1435 | 1435 | ||
| 1436 | - | func runUDPProxyRuntime(ctx context.Context, client, daemon ports.Transport, log *slog.Logger) error { | |
| 1436 | + | func runUDPProxyRuntime(ctx context.Context, client, daemon wire.Transport, log *slog.Logger) error { | |
| 1437 | 1437 | return dgram.ProxyRuntime{Client: client, Daemon: daemon, Log: log, IdleTTL: udpProxyIdleTTL}.Run(ctx) | |
| 1438 | 1438 | } | |
| 1439 | 1439 | ||
| 1440 | - | func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error { | |
| 1440 | + | func proxyTransports(ctx context.Context, a, b wire.Transport, log *slog.Logger) error { | |
| 1441 | 1441 | return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx) | |
| 1442 | 1442 | } | |
| 1443 | 1443 | ||
M internal/app/run_test.go +47 -45
| @@ -23,8 +23,10 @@ import ( | |||
| 23 | 23 | "github.com/bnema/vev/internal/domain" | |
| 24 | 24 | "github.com/bnema/vev/internal/persist" | |
| 25 | 25 | "github.com/bnema/vev/internal/ports" | |
| 26 | - | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 27 | 26 | "github.com/bnema/vev/internal/protocol" | |
| 27 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 28 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 29 | + | wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks" | |
| 28 | 30 | "github.com/bnema/vev/internal/usecase/client" | |
| 29 | 31 | "github.com/bnema/vev/internal/usecase/daemon" | |
| 30 | 32 | "github.com/bnema/vev/pkg/kv" | |
| @@ -220,7 +222,7 @@ func TestCatalogueFailureDoesNotListen(t *testing.T) { | |||
| 220 | 222 | require.Equal(t, stateDir, gotStateDir) | |
| 221 | 223 | return persist.OpenResult{}, catalogueErr | |
| 222 | 224 | } | |
| 223 | - | listenDaemon = func(string, ports.SerializedRuntimeObserver) (ports.Listener, error) { | |
| 225 | + | listenDaemon = func(string, ports.SerializedRuntimeObserver) (wire.Listener, error) { | |
| 224 | 226 | listenCalls++ | |
| 225 | 227 | return nil, errors.New("IPC listen must not run after catalogue failure") | |
| 226 | 228 | } | |
| @@ -251,7 +253,7 @@ func TestCatalogueRegistryConstructionPrecedesSocketPublication(t *testing.T) { | |||
| 251 | 253 | events = append(events, "startup-garbage-collection") | |
| 252 | 254 | return nil | |
| 253 | 255 | }, | |
| 254 | - | func() (ports.Listener, error) { | |
| 256 | + | func() (wire.Listener, error) { | |
| 255 | 257 | require.Equal(t, []string{"catalogue-registry", "startup-garbage-collection"}, events) | |
| 256 | 258 | events = append(events, "socket-publication") | |
| 257 | 259 | return nil, nil | |
| @@ -577,7 +579,7 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) { | |||
| 577 | 579 | t.Run(tt.name, func(t *testing.T) { | |
| 578 | 580 | cmd, err := parseArgs([]string{"attach", tt.arg}) | |
| 579 | 581 | require.NoError(t, err) | |
| 580 | - | connection, err := client.NewSessionConnection(sessionwire.NewClientConnection(portsmocks.NewMockTransport(t)), client.SessionTarget{ | |
| 582 | + | connection, err := client.NewSessionConnection(sessionwire.NewClientConnection(wiremocks.NewMockTransport(t)), client.SessionTarget{ | |
| 581 | 583 | Intent: cmd.intent, | |
| 582 | 584 | SessionName: cmd.name, | |
| 583 | 585 | }) | |
| @@ -808,7 +810,7 @@ func TestDetachedLocalHelloIncludesTrueColor(t *testing.T) { | |||
| 808 | 810 | ||
| 809 | 811 | type namedDialer struct{ name string } | |
| 810 | 812 | ||
| 811 | - | func (d namedDialer) Dial(context.Context) (ports.Transport, error) { | |
| 813 | + | func (d namedDialer) Dial(context.Context) (wire.Transport, error) { | |
| 812 | 814 | return nil, fmt.Errorf("not used: %s", d.name) | |
| 813 | 815 | } | |
| 814 | 816 | ||
| @@ -827,10 +829,10 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) { | |||
| 827 | 829 | tests := []struct { | |
| 828 | 830 | name string | |
| 829 | 831 | selectedTransport string | |
| 830 | - | wantMode ports.RemoteTransportMode | |
| 832 | + | wantMode remoteadapter.TransportMode | |
| 831 | 833 | }{ | |
| 832 | - | {name: "default remote mode is udp", selectedTransport: "", wantMode: ports.RemoteTransportUDP}, | |
| 833 | - | {name: "explicit stdio mode", selectedTransport: "stdio", wantMode: ports.RemoteTransportStdio}, | |
| 834 | + | {name: "default remote mode is udp", selectedTransport: "", wantMode: remoteadapter.TransportUDP}, | |
| 835 | + | {name: "explicit stdio mode", selectedTransport: "stdio", wantMode: remoteadapter.TransportStdio}, | |
| 834 | 836 | } | |
| 835 | 837 | ||
| 836 | 838 | for _, tt := range tests { | |
| @@ -839,11 +841,11 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) { | |||
| 839 | 841 | var gotRemote bool | |
| 840 | 842 | var gotClipboard ports.ClipboardReader | |
| 841 | 843 | clip := &fakeClipboardReader{} | |
| 842 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 844 | + | factory := newRemoteDialerFactoryMock(t) | |
| 843 | 845 | factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil) | |
| 844 | 846 | ||
| 845 | 847 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 846 | - | remoteDialerFactory: factory, | |
| 848 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 847 | 849 | selectedRemoteTransport: tt.selectedTransport, | |
| 848 | 850 | clipboard: clip, | |
| 849 | 851 | runClient: func(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| @@ -877,12 +879,12 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) { | |||
| 877 | 879 | } | |
| 878 | 880 | ||
| 879 | 881 | func TestRunAttachWithDepsDirectLegacyCallbackPreservesClientOwnership(t *testing.T) { | |
| 880 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 881 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once() | |
| 882 | - | factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once() | |
| 882 | + | factory := newRemoteDialerFactoryMock(t) | |
| 883 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once() | |
| 884 | + | factory.EXPECT().DialerForRemote("selected.example", "picked", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once() | |
| 883 | 885 | ||
| 884 | 886 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 885 | - | remoteDialerFactory: factory, | |
| 887 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 886 | 888 | runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| 887 | 889 | require.True(t, request.Remote) | |
| 888 | 890 | require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy) | |
| @@ -900,12 +902,12 @@ func TestRunAttachWithDepsDirectLegacyCallbackPreservesClientOwnership(t *testin | |||
| 900 | 902 | } | |
| 901 | 903 | ||
| 902 | 904 | func TestRunAttachWithDepsRemotePickerHandoffReopensDirectConnection(t *testing.T) { | |
| 903 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 904 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once() | |
| 905 | - | factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once() | |
| 905 | + | factory := newRemoteDialerFactoryMock(t) | |
| 906 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once() | |
| 907 | + | factory.EXPECT().DialerForRemote("selected.example", "picked", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once() | |
| 906 | 908 | var calls int | |
| 907 | 909 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 908 | - | remoteDialerFactory: factory, | |
| 910 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 909 | 911 | runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| 910 | 912 | calls++ | |
| 911 | 913 | require.NotNil(t, deps.Dialer) | |
| @@ -922,12 +924,12 @@ func TestRunAttachWithDepsRemotePickerHandoffReopensDirectConnection(t *testing. | |||
| 922 | 924 | } | |
| 923 | 925 | ||
| 924 | 926 | func TestRunAttachWithDepsAllowsRevisitingRemoteHandoff(t *testing.T) { | |
| 925 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 926 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 927 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-revisit"}, nil).Once() | |
| 927 | + | factory := newRemoteDialerFactoryMock(t) | |
| 928 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 929 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote-revisit"}, nil).Once() | |
| 928 | 930 | ||
| 929 | 931 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 930 | - | remoteDialerFactory: factory, | |
| 932 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 931 | 933 | runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error { | |
| 932 | 934 | dialer, request, err := deps.AttachHandoff(protocol.AttachTarget{ | |
| 933 | 935 | Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach, | |
| @@ -942,12 +944,12 @@ func TestRunAttachWithDepsAllowsRevisitingRemoteHandoff(t *testing.T) { | |||
| 942 | 944 | } | |
| 943 | 945 | ||
| 944 | 946 | func TestRunAttachWithDepsBoundsRepeatedAttachTargetHandoffs(t *testing.T) { | |
| 945 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 946 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Times(maxAttachTargetHandoffs + 1) | |
| 947 | + | factory := newRemoteDialerFactoryMock(t) | |
| 948 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Times(maxAttachTargetHandoffs + 1) | |
| 947 | 949 | ||
| 948 | 950 | calls := 0 | |
| 949 | 951 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 950 | - | remoteDialerFactory: factory, | |
| 952 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 951 | 953 | runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error { | |
| 952 | 954 | calls++ | |
| 953 | 955 | return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}} | |
| @@ -958,13 +960,13 @@ func TestRunAttachWithDepsBoundsRepeatedAttachTargetHandoffs(t *testing.T) { | |||
| 958 | 960 | } | |
| 959 | 961 | ||
| 960 | 962 | func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T) { | |
| 961 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 962 | - | factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 963 | + | factory := newRemoteDialerFactoryMock(t) | |
| 964 | + | factory.EXPECT().DialerForRemote("selected.example", "picked", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 963 | 965 | ||
| 964 | 966 | localCalls, remoteCalls := 0, 0 | |
| 965 | 967 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "", "", nil, runAttachDeps{ | |
| 966 | - | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, | |
| 967 | - | remoteDialerFactory: factory, | |
| 968 | + | localDialer: func() wire.Dialer { return namedDialer{name: "local"} }, | |
| 969 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 968 | 970 | runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| 969 | 971 | if deps.Remote { | |
| 970 | 972 | remoteCalls++ | |
| @@ -1003,11 +1005,11 @@ func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) { | |||
| 1003 | 1005 | } | |
| 1004 | 1006 | for _, tt := range tests { | |
| 1005 | 1007 | t.Run(tt.name, func(t *testing.T) { | |
| 1006 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 1007 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 1008 | + | factory := newRemoteDialerFactoryMock(t) | |
| 1009 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once() | |
| 1008 | 1010 | calls := 0 | |
| 1009 | 1011 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 1010 | - | remoteDialerFactory: factory, | |
| 1012 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 1011 | 1013 | runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error { | |
| 1012 | 1014 | calls++ | |
| 1013 | 1015 | return &client.AttachTargetError{Target: tt.target} | |
| @@ -1020,11 +1022,11 @@ func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) { | |||
| 1020 | 1022 | } | |
| 1021 | 1023 | ||
| 1022 | 1024 | func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing.T) { | |
| 1023 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 1025 | + | factory := newRemoteDialerFactoryMock(t) | |
| 1024 | 1026 | runClientCalled := false | |
| 1025 | 1027 | ||
| 1026 | 1028 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 1027 | - | remoteDialerFactory: factory, | |
| 1029 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 1028 | 1030 | selectedRemoteTransport: "serial", | |
| 1029 | 1031 | runClient: func(context.Context, client.Dependencies, client.AttachRequest) error { | |
| 1030 | 1032 | runClientCalled = true | |
| @@ -1041,12 +1043,12 @@ func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing. | |||
| 1041 | 1043 | ||
| 1042 | 1044 | func TestRunAttachWithDepsReturnsFactoryErrorBeforeRunClient(t *testing.T) { | |
| 1043 | 1045 | factoryErr := errors.New("factory failed") | |
| 1044 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 1045 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr) | |
| 1046 | + | factory := newRemoteDialerFactoryMock(t) | |
| 1047 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportUDP, mock.Anything).Return(nil, factoryErr) | |
| 1046 | 1048 | runClientCalled := false | |
| 1047 | 1049 | ||
| 1048 | 1050 | err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{ | |
| 1049 | - | remoteDialerFactory: factory, | |
| 1051 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 1050 | 1052 | runClient: func(context.Context, client.Dependencies, client.AttachRequest) error { | |
| 1051 | 1053 | runClientCalled = true | |
| 1052 | 1054 | return nil | |
| @@ -1064,10 +1066,10 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) { | |||
| 1064 | 1066 | var gotDialer string | |
| 1065 | 1067 | gotRemote := true | |
| 1066 | 1068 | gotClipboard := ports.ClipboardReader(&fakeClipboardReader{}) | |
| 1067 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 1069 | + | factory := newRemoteDialerFactoryMock(t) | |
| 1068 | 1070 | err := runAttachWithDeps(context.Background(), protocol.IntentEphemeral, "", "", "", nil, runAttachDeps{ | |
| 1069 | - | localDialer: func() ports.Dialer { return namedDialer{name: "local"} }, | |
| 1070 | - | remoteDialerFactory: factory, | |
| 1071 | + | localDialer: func() wire.Dialer { return namedDialer{name: "local"} }, | |
| 1072 | + | remoteDialerFactory: factory.DialerForRemote, | |
| 1071 | 1073 | clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach | |
| 1072 | 1074 | runClient: func(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error { | |
| 1073 | 1075 | requireNamedClientDialer(t, ctx, deps.Dialer, "local") | |
| @@ -1287,12 +1289,12 @@ type appCatalogCacheStub struct { | |||
| 1287 | 1289 | loads int | |
| 1288 | 1290 | } | |
| 1289 | 1291 | ||
| 1290 | - | func (s *appCatalogCacheStub) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 1292 | + | func (s *appCatalogCacheStub) Load() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 1291 | 1293 | s.loads++ | |
| 1292 | 1294 | return nil, nil | |
| 1293 | 1295 | } | |
| 1294 | 1296 | ||
| 1295 | - | func (s *appCatalogCacheStub) Store([]ports.RemoteCatalogCacheEntry) error { return nil } | |
| 1297 | + | func (s *appCatalogCacheStub) Store([]catalogue.RemoteCatalogCacheEntry) error { return nil } | |
| 1296 | 1298 | ||
| 1297 | 1299 | func TestRemoteDiscoveryDaemonOptionWiresProductionPorts(t *testing.T) { | |
| 1298 | 1300 | oldHostStore := newRemoteHostStore | |
| @@ -1318,9 +1320,9 @@ func TestRemoteDiscoveryDaemonOptionWiresProductionPorts(t *testing.T) { | |||
| 1318 | 1320 | return cache | |
| 1319 | 1321 | } | |
| 1320 | 1322 | newRemoteCatalogClient = func() ports.RemoteCatalogClient { return nil } | |
| 1321 | - | newRemoteDialerFactoryWithRuntimeObserver = func(ports.SerializedRuntimeObserver) ports.RemoteDialerFactory { return nil } | |
| 1323 | + | newRemoteDialerFactoryWithRuntimeObserver = func(ports.SerializedRuntimeObserver) remoteDialerForTarget { return nil } | |
| 1322 | 1324 | ||
| 1323 | - | option, err := remoteDiscoveryDaemonOption(stateDir, nil, "stdio") | |
| 1325 | + | option, err := remoteDiscoveryDaemonOption(stateDir, "stdio") | |
| 1324 | 1326 | require.NoError(t, err) | |
| 1325 | 1327 | _ = daemon.New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), option) | |
| 1326 | 1328 | require.Equal(t, remoteadapter.HostStorePath(stateDir), hostPath) | |
| @@ -1329,6 +1331,6 @@ func TestRemoteDiscoveryDaemonOptionWiresProductionPorts(t *testing.T) { | |||
| 1329 | 1331 | } | |
| 1330 | 1332 | ||
| 1331 | 1333 | func TestRemoteDiscoveryDaemonOptionRejectsInvalidTransport(t *testing.T) { | |
| 1332 | - | _, err := remoteDiscoveryDaemonOption(t.TempDir(), nil, "serial") | |
| 1334 | + | _, err := remoteDiscoveryDaemonOption(t.TempDir(), "serial") | |
| 1333 | 1335 | require.EqualError(t, err, `vev: invalid remote transport "serial" (want "udp" or "stdio")`) | |
| 1334 | 1336 | } | |
M internal/app/spawn.go +9 -9
| @@ -16,7 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 17 | 17 | "github.com/bnema/vev/internal/adapters/lifecycle" | |
| 18 | 18 | "github.com/bnema/vev/internal/platform" | |
| 19 | - | "github.com/bnema/vev/internal/ports" | |
| 19 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 20 | 20 | "github.com/bnema/vev/pkg/safedir" | |
| 21 | 21 | ) | |
| 22 | 22 | ||
| @@ -35,7 +35,7 @@ const staleLockAge = 10 * time.Second | |||
| 35 | 35 | ||
| 36 | 36 | // dialFunc dials the daemon socket in dir. Injected so tests can drive the | |
| 37 | 37 | // spawn/backoff logic without a real socket. | |
| 38 | - | type dialFunc func(ctx context.Context, dir string) (ports.Transport, error) | |
| 38 | + | type dialFunc func(ctx context.Context, dir string) (wire.Transport, error) | |
| 39 | 39 | ||
| 40 | 40 | // spawnFunc launches a detached daemon process. Injected so tests never | |
| 41 | 41 | // re-exec a real binary. | |
| @@ -115,11 +115,11 @@ func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg ba | |||
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | 117 | type daemonOrLifecycle struct { | |
| 118 | - | transport ports.Transport | |
| 118 | + | transport wire.Transport | |
| 119 | 119 | owner lifecycleOwnership | |
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | - | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, lifecycleOwnership, error) { | |
| 122 | + | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (wire.Transport, lifecycleOwnership, error) { | |
| 123 | 123 | result, err := retryAttempts(ctx, cfg, func() (daemonOrLifecycle, bool, error) { | |
| 124 | 124 | if transport, err := dial(ctx, dir); err == nil { | |
| 125 | 125 | return daemonOrLifecycle{transport: transport}, true, nil | |
| @@ -149,7 +149,7 @@ func waitBackoff(ctx context.Context, duration time.Duration) error { | |||
| 149 | 149 | ||
| 150 | 150 | // ensureDaemonWithLifecycle never elects a spawner while another lifecycle | |
| 151 | 151 | // owner may still be initializing or tearing down durable state. | |
| 152 | - | func ensureDaemonWithLifecycle(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (ports.Transport, error) { | |
| 152 | + | func ensureDaemonWithLifecycle(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (wire.Transport, error) { | |
| 153 | 153 | if err := ctx.Err(); err != nil { | |
| 154 | 154 | return nil, err | |
| 155 | 155 | } | |
| @@ -172,7 +172,7 @@ func ensureDaemonWithLifecycle(ctx context.Context, dir string, dial dialFunc, s | |||
| 172 | 172 | // ensureDaemon returns a transport to a running daemon, spawning one if | |
| 173 | 173 | // necessary after ensureDaemonWithLifecycle has established lifecycle | |
| 174 | 174 | // availability. Tests also exercise this lower-level spawn election directly. | |
| 175 | - | func ensureDaemon(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (ports.Transport, error) { | |
| 175 | + | func ensureDaemon(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (wire.Transport, error) { | |
| 176 | 176 | if err := ctx.Err(); err != nil { | |
| 177 | 177 | return nil, err | |
| 178 | 178 | } | |
| @@ -237,8 +237,8 @@ func acquireSpawnLock(dir string) (release func(), acquired bool, err error) { | |||
| 237 | 237 | ||
| 238 | 238 | // retryDial dials repeatedly with exponential backoff until the daemon | |
| 239 | 239 | // answers, the context is cancelled, or the total budget is exhausted. | |
| 240 | - | func retryDial(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, error) { | |
| 241 | - | transport, err := retryAttempts(ctx, cfg, func() (ports.Transport, bool, error) { | |
| 240 | + | func retryDial(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (wire.Transport, error) { | |
| 241 | + | transport, err := retryAttempts(ctx, cfg, func() (wire.Transport, bool, error) { | |
| 242 | 242 | transport, err := dial(ctx, dir) | |
| 243 | 243 | return transport, err == nil, nil | |
| 244 | 244 | }) | |
| @@ -249,7 +249,7 @@ func retryDial(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig | |||
| 249 | 249 | } | |
| 250 | 250 | ||
| 251 | 251 | // realDial is the production dialer. | |
| 252 | - | func realDial(ctx context.Context, dir string) (ports.Transport, error) { | |
| 252 | + | func realDial(ctx context.Context, dir string) (wire.Transport, error) { | |
| 253 | 253 | return ipc.DialContext(ctx, dir) | |
| 254 | 254 | } | |
| 255 | 255 | ||
M internal/app/spawn_test.go +18 -13
| @@ -13,12 +13,17 @@ import ( | |||
| 13 | 13 | "testing" | |
| 14 | 14 | "time" | |
| 15 | 15 | ||
| 16 | - | "github.com/bnema/vev/internal/ports" | |
| 17 | - | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 18 | 17 | ) | |
| 19 | 18 | ||
| 20 | 19 | var errDialFailed = errors.New("dial failed") | |
| 21 | 20 | ||
| 21 | + | type spawnTestTransport struct{} | |
| 22 | + | ||
| 23 | + | func (spawnTestTransport) Send(wire.Frame) error { return nil } | |
| 24 | + | func (spawnTestTransport) Recv() (wire.Frame, error) { return wire.Frame{}, nil } | |
| 25 | + | func (spawnTestTransport) Close() error { return nil } | |
| 26 | + | ||
| 22 | 27 | const ( | |
| 23 | 28 | spawnTestChildFileEnv = "VEV_SPAWN_TEST_CHILD_FILE" | |
| 24 | 29 | spawnTestLauncherFileEnv = "VEV_SPAWN_TEST_LAUNCHER_FILE" | |
| @@ -305,9 +310,9 @@ func TestRetryAttemptsClampsNonPositiveInitialDelay(t *testing.T) { | |||
| 305 | 310 | } | |
| 306 | 311 | ||
| 307 | 312 | func TestRetryDialSucceedsAfterTransientFailures(t *testing.T) { | |
| 308 | - | want := portsmocks.NewMockTransport(t) | |
| 313 | + | want := spawnTestTransport{} | |
| 309 | 314 | var calls atomic.Int32 | |
| 310 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 315 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 311 | 316 | if calls.Add(1) <= 3 { | |
| 312 | 317 | return nil, errDialFailed | |
| 313 | 318 | } | |
| @@ -327,7 +332,7 @@ func TestRetryDialSucceedsAfterTransientFailures(t *testing.T) { | |||
| 327 | 332 | } | |
| 328 | 333 | ||
| 329 | 334 | func TestRetryDialPermanentFailure(t *testing.T) { | |
| 330 | - | dial := func(context.Context, string) (ports.Transport, error) { return nil, errDialFailed } | |
| 335 | + | dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed } | |
| 331 | 336 | _, err := retryDial(context.Background(), t.TempDir(), dial, fastBackoff) | |
| 332 | 337 | if !errors.Is(err, ErrDaemonUnreachable) { | |
| 333 | 338 | t.Fatalf("retryDial error = %v, want ErrDaemonUnreachable", err) | |
| @@ -338,7 +343,7 @@ func TestRetryDialContextCancelled(t *testing.T) { | |||
| 338 | 343 | ctx, cancel := context.WithCancel(context.Background()) | |
| 339 | 344 | cancel() | |
| 340 | 345 | var calls atomic.Int32 | |
| 341 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 346 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 342 | 347 | calls.Add(1) | |
| 343 | 348 | return nil, errDialFailed | |
| 344 | 349 | } | |
| @@ -354,7 +359,7 @@ func TestRetryDialContextCancelled(t *testing.T) { | |||
| 354 | 359 | func TestEnsureDaemonContextCancelledDoesNotDialOrSpawn(t *testing.T) { | |
| 355 | 360 | ctx, cancel := context.WithCancel(context.Background()) | |
| 356 | 361 | cancel() | |
| 357 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 362 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 358 | 363 | t.Fatal("ensureDaemon dialed with a canceled context") | |
| 359 | 364 | return nil, nil | |
| 360 | 365 | } | |
| @@ -373,7 +378,7 @@ func TestEnsureDaemonDoesNotSpawnWhenContextCancelsAfterFailedDial(t *testing.T) | |||
| 373 | 378 | ctx, cancel := context.WithCancel(context.Background()) | |
| 374 | 379 | defer cancel() | |
| 375 | 380 | var spawnCalls atomic.Int32 | |
| 376 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 381 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 377 | 382 | cancel() | |
| 378 | 383 | return nil, errDialFailed | |
| 379 | 384 | } | |
| @@ -393,9 +398,9 @@ func TestEnsureDaemonDoesNotSpawnWhenContextCancelsAfterFailedDial(t *testing.T) | |||
| 393 | 398 | ||
| 394 | 399 | func TestEnsureDaemonSpawnsThenDials(t *testing.T) { | |
| 395 | 400 | dir := filepath.Join(t.TempDir(), "vev") | |
| 396 | - | want := portsmocks.NewMockTransport(t) | |
| 401 | + | want := spawnTestTransport{} | |
| 397 | 402 | var dialCalls, spawnCalls atomic.Int32 | |
| 398 | - | dial := func(context.Context, string) (ports.Transport, error) { | |
| 403 | + | dial := func(context.Context, string) (wire.Transport, error) { | |
| 399 | 404 | // First (pre-spawn) dial fails; the post-spawn retry succeeds. | |
| 400 | 405 | if dialCalls.Add(1) == 1 { | |
| 401 | 406 | return nil, errDialFailed | |
| @@ -424,9 +429,9 @@ func TestEnsureDaemonSpawnsThenDials(t *testing.T) { | |||
| 424 | 429 | } | |
| 425 | 430 | ||
| 426 | 431 | func TestEnsureDaemonReturnsExistingDaemon(t *testing.T) { | |
| 427 | - | want := portsmocks.NewMockTransport(t) | |
| 432 | + | want := spawnTestTransport{} | |
| 428 | 433 | var spawnCalls atomic.Int32 | |
| 429 | - | dial := func(context.Context, string) (ports.Transport, error) { return want, nil } | |
| 434 | + | dial := func(context.Context, string) (wire.Transport, error) { return want, nil } | |
| 430 | 435 | spawn := func() error { spawnCalls.Add(1); return nil } | |
| 431 | 436 | ||
| 432 | 437 | got, err := ensureDaemon(context.Background(), t.TempDir(), dial, spawn, fastBackoff) | |
| @@ -443,7 +448,7 @@ func TestEnsureDaemonReturnsExistingDaemon(t *testing.T) { | |||
| 443 | 448 | ||
| 444 | 449 | func TestEnsureDaemonSpawnFailure(t *testing.T) { | |
| 445 | 450 | spawnErr := errors.New("exec boom") | |
| 446 | - | dial := func(context.Context, string) (ports.Transport, error) { return nil, errDialFailed } | |
| 451 | + | dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed } | |
| 447 | 452 | spawn := func() error { return spawnErr } | |
| 448 | 453 | ||
| 449 | 454 | _, err := ensureDaemon(context.Background(), filepath.Join(t.TempDir(), "vev"), dial, spawn, fastBackoff) | |
M internal/app/transport_observability_test.go +9 -7
| @@ -13,6 +13,8 @@ import ( | |||
| 13 | 13 | "testing" | |
| 14 | 14 | "time" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/vev/internal/adapters/observability" | |
| 17 | + | remoteadapter "github.com/bnema/vev/internal/adapters/remote" | |
| 16 | 18 | "github.com/bnema/vev/internal/ports" | |
| 17 | 19 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 18 | 20 | "github.com/bnema/vev/internal/protocol" | |
| @@ -182,7 +184,7 @@ func TestRunAttachJoinsTraceCloseError(t *testing.T) { | |||
| 182 | 184 | traceCloseErr := errors.New("trace close failed") | |
| 183 | 185 | original := newPerformanceTrace | |
| 184 | 186 | newPerformanceTrace = func(ports.Clock) (ports.SerializedRuntimeObserver, io.Closer, error) { | |
| 185 | - | return ports.NewSerializedRuntimeObserver(runtimeObserverFunc(func(ports.RuntimeMark) {}), 1), errorCloser{err: traceCloseErr}, nil | |
| 187 | + | return observability.NewSerialized(runtimeObserverFunc(func(ports.RuntimeMark) {}), 1), errorCloser{err: traceCloseErr}, nil | |
| 186 | 188 | } | |
| 187 | 189 | t.Cleanup(func() { newPerformanceTrace = original }) | |
| 188 | 190 | t.Setenv("VEV", "") | |
| @@ -211,9 +213,9 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) { | |||
| 211 | 213 | t.Setenv("VEV", "") | |
| 212 | 214 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 213 | 215 | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |
| 214 | - | t.Setenv(envRemoteTransport, string(ports.RemoteTransportStdio)) | |
| 216 | + | t.Setenv(envRemoteTransport, string(remoteadapter.TransportStdio)) | |
| 215 | 217 | ||
| 216 | - | observer := ports.NewSerializedRuntimeObserver(runtimeObserverFunc(func(ports.RuntimeMark) {}), 1) | |
| 218 | + | observer := observability.NewSerialized(runtimeObserverFunc(func(ports.RuntimeMark) {}), 1) | |
| 217 | 219 | originalTrace := newPerformanceTrace | |
| 218 | 220 | traceCalls := 0 | |
| 219 | 221 | newPerformanceTrace = func(ports.Clock) (ports.SerializedRuntimeObserver, io.Closer, error) { | |
| @@ -222,16 +224,16 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) { | |||
| 222 | 224 | } | |
| 223 | 225 | t.Cleanup(func() { newPerformanceTrace = originalTrace }) | |
| 224 | 226 | ||
| 225 | - | factory := portsmocks.NewMockRemoteDialerFactory(t) | |
| 226 | - | factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportStdio, mock.Anything).Return(namedDialer{name: "remote"}, nil) | |
| 227 | + | factory := newRemoteDialerFactoryMock(t) | |
| 228 | + | factory.EXPECT().DialerForRemote("remote.example", "work", remoteadapter.TransportStdio, mock.Anything).Return(namedDialer{name: "remote"}, nil) | |
| 227 | 229 | originalFactory := newRemoteDialerFactoryWithRuntimeObserver | |
| 228 | 230 | factoryCalls := 0 | |
| 229 | - | newRemoteDialerFactoryWithRuntimeObserver = func(got ports.SerializedRuntimeObserver) ports.RemoteDialerFactory { | |
| 231 | + | newRemoteDialerFactoryWithRuntimeObserver = func(got ports.SerializedRuntimeObserver) remoteDialerForTarget { | |
| 230 | 232 | factoryCalls++ | |
| 231 | 233 | if got != observer { | |
| 232 | 234 | t.Fatalf("remote transport observer = %v, want process observer %v", got, observer) | |
| 233 | 235 | } | |
| 234 | - | return factory | |
| 236 | + | return factory.DialerForRemote | |
| 235 | 237 | } | |
| 236 | 238 | t.Cleanup(func() { newRemoteDialerFactoryWithRuntimeObserver = originalFactory }) | |
| 237 | 239 | ||
A internal/domain/terminalcap/capabilities.go +100 -0
| @@ -0,0 +1,100 @@ | |||
| 1 | + | // Package terminalcap owns pure terminal capability values and detection policy. | |
| 2 | + | package terminalcap | |
| 3 | + | ||
| 4 | + | import "strings" | |
| 5 | + | ||
| 6 | + | // ColorMode is the color output mode selected for one attachment. | |
| 7 | + | type ColorMode uint8 | |
| 8 | + | ||
| 9 | + | const ( | |
| 10 | + | // TrueColor is the zero value so manually constructed attachments retain the | |
| 11 | + | // historical renderer behavior; live Hello paths always use the detector. | |
| 12 | + | TrueColor ColorMode = iota | |
| 13 | + | Indexed256 | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // Source records how confidently a terminal capability was selected. | |
| 17 | + | type Source uint8 | |
| 18 | + | ||
| 19 | + | const ( | |
| 20 | + | SourceUnknown Source = iota | |
| 21 | + | SourceHeuristic | |
| 22 | + | SourceDeclared | |
| 23 | + | ) | |
| 24 | + | ||
| 25 | + | // Application identifies a known terminal application when its environment | |
| 26 | + | // provides a trustworthy origin signal. | |
| 27 | + | type Application uint8 | |
| 28 | + | ||
| 29 | + | const ( | |
| 30 | + | ApplicationUnknown Application = iota | |
| 31 | + | ApplicationKitty | |
| 32 | + | ) | |
| 33 | + | ||
| 34 | + | // Capabilities describes the output features selected for one client attachment. | |
| 35 | + | type Capabilities struct { | |
| 36 | + | ColorMode ColorMode | |
| 37 | + | ColorSource Source | |
| 38 | + | Application Application | |
| 39 | + | KittyGraphics bool | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // SupportsKittyGraphics reports whether the active outer-terminal probe | |
| 43 | + | // accepted the Kitty graphics protocol. Environment detection never sets it. | |
| 44 | + | func (c Capabilities) SupportsKittyGraphics() bool { return c.KittyGraphics } | |
| 45 | + | ||
| 46 | + | // TrueColor reports whether this attachment can receive RGB ANSI output. | |
| 47 | + | func (c Capabilities) TrueColor() bool { return c.ColorMode == TrueColor } | |
| 48 | + | ||
| 49 | + | // Detect derives conservative attachment capabilities from a client environment. | |
| 50 | + | func Detect(env []string) Capabilities { | |
| 51 | + | values := environmentValues(env) | |
| 52 | + | term := strings.ToLower(strings.TrimSpace(values["TERM"])) | |
| 53 | + | colorTerm := strings.ToLower(strings.TrimSpace(values["COLORTERM"])) | |
| 54 | + | caps := Capabilities{ColorMode: Indexed256} | |
| 55 | + | ||
| 56 | + | if values["KITTY_WINDOW_ID"] != "" || values["KITTY_PID"] != "" || values["KITTY_LISTEN_ON"] != "" { | |
| 57 | + | caps.Application = ApplicationKitty | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | kittyIdentity := term == "xterm-kitty" && caps.Application == ApplicationKitty | |
| 61 | + | switch colorTerm { | |
| 62 | + | case "truecolor", "24bit": | |
| 63 | + | caps.ColorMode = TrueColor | |
| 64 | + | caps.ColorSource = SourceDeclared | |
| 65 | + | } | |
| 66 | + | if term == "xterm-direct" || strings.HasSuffix(term, "-direct") { | |
| 67 | + | caps.ColorMode = TrueColor | |
| 68 | + | caps.ColorSource = SourceDeclared | |
| 69 | + | } | |
| 70 | + | if kittyIdentity { | |
| 71 | + | if caps.ColorSource == SourceUnknown { | |
| 72 | + | caps.ColorSource = SourceHeuristic | |
| 73 | + | } | |
| 74 | + | caps.ColorMode = TrueColor | |
| 75 | + | return caps | |
| 76 | + | } | |
| 77 | + | if caps.ColorSource == SourceDeclared { | |
| 78 | + | return caps | |
| 79 | + | } | |
| 80 | + | if strings.Contains(term, "256color") || term == "dumb" { | |
| 81 | + | caps.ColorSource = SourceDeclared | |
| 82 | + | } | |
| 83 | + | return caps | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func environmentValues(env []string) map[string]string { | |
| 87 | + | values := make(map[string]string, len(env)) | |
| 88 | + | for _, entry := range env { | |
| 89 | + | key, value, ok := strings.Cut(entry, "=") | |
| 90 | + | if ok { | |
| 91 | + | values[key] = value | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | return values | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | |
| 98 | + | func DetectTrueColor(termEnv, colorTerm string) bool { | |
| 99 | + | return Detect([]string{"TERM=" + termEnv, "COLORTERM=" + colorTerm}).TrueColor() | |
| 100 | + | } | |
A internal/domain/terminalcap/capabilities_test.go +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | package terminalcap | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestDetect(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | env []string | |
| 13 | + | wantMode ColorMode | |
| 14 | + | wantSource Source | |
| 15 | + | wantApp Application | |
| 16 | + | wantGraphics bool | |
| 17 | + | }{ | |
| 18 | + | {name: "declared truecolor", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, wantMode: TrueColor, wantSource: SourceDeclared}, | |
| 19 | + | {name: "kitty signals infer truecolor", env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, wantMode: TrueColor, wantSource: SourceHeuristic, wantApp: ApplicationKitty}, | |
| 20 | + | {name: "kitty environment with declared truecolor remains color-only", env: []string{"TERM=xterm-kitty", "COLORTERM=truecolor", "KITTY_WINDOW_ID=1"}, wantMode: TrueColor, wantSource: SourceDeclared, wantApp: ApplicationKitty}, | |
| 21 | + | {name: "256 color terminal remains a constrained attachment", env: []string{"TERM=xterm-256color"}, wantMode: Indexed256, wantSource: SourceDeclared}, | |
| 22 | + | {name: "unknown terminal is conservatively indexed", env: []string{"TERM=unknown"}, wantMode: Indexed256, wantSource: SourceUnknown}, | |
| 23 | + | {name: "kitty environment behind a multiplexer is not graphics evidence", env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, wantMode: Indexed256, wantSource: SourceDeclared, wantApp: ApplicationKitty}, | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | for _, tt := range tests { | |
| 27 | + | t.Run(tt.name, func(t *testing.T) { | |
| 28 | + | got := Detect(tt.env) | |
| 29 | + | require.Equal(t, tt.wantMode, got.ColorMode) | |
| 30 | + | require.Equal(t, tt.wantSource, got.ColorSource) | |
| 31 | + | require.Equal(t, tt.wantApp, got.Application) | |
| 32 | + | require.Equal(t, tt.wantGraphics, got.SupportsKittyGraphics()) | |
| 33 | + | }) | |
| 34 | + | } | |
| 35 | + | } | |
M internal/ports/mocks/remote_mock.go +22 -130
| @@ -6,121 +6,13 @@ package portsmocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "log/slog" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/vev/internal/domain" | |
| 12 | - | "github.com/bnema/vev/internal/ports" | |
| 13 | 11 | "github.com/bnema/vev/internal/protocol" | |
| 12 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 14 | 13 | mock "github.com/stretchr/testify/mock" | |
| 15 | 14 | ) | |
| 16 | 15 | ||
| 17 | - | // 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. | |
| 18 | - | // The first argument is typically a *testing.T value. | |
| 19 | - | func NewMockRemoteDialerFactory(t interface { | |
| 20 | - | mock.TestingT | |
| 21 | - | Cleanup(func()) | |
| 22 | - | }) *MockRemoteDialerFactory { | |
| 23 | - | mock := &MockRemoteDialerFactory{} | |
| 24 | - | mock.Mock.Test(t) | |
| 25 | - | ||
| 26 | - | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 27 | - | ||
| 28 | - | return mock | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | // MockRemoteDialerFactory is an autogenerated mock type for the RemoteDialerFactory type | |
| 32 | - | type MockRemoteDialerFactory struct { | |
| 33 | - | mock.Mock | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | type MockRemoteDialerFactory_Expecter struct { | |
| 37 | - | mock *mock.Mock | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | func (_m *MockRemoteDialerFactory) EXPECT() *MockRemoteDialerFactory_Expecter { | |
| 41 | - | return &MockRemoteDialerFactory_Expecter{mock: &_m.Mock} | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | // DialerForRemote provides a mock function for the type MockRemoteDialerFactory | |
| 45 | - | func (_mock *MockRemoteDialerFactory) DialerForRemote(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error) { | |
| 46 | - | ret := _mock.Called(target, session, mode, log) | |
| 47 | - | ||
| 48 | - | if len(ret) == 0 { | |
| 49 | - | panic("no return value specified for DialerForRemote") | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | var r0 ports.Dialer | |
| 53 | - | var r1 error | |
| 54 | - | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) (ports.Dialer, error)); ok { | |
| 55 | - | return returnFunc(target, session, mode, log) | |
| 56 | - | } | |
| 57 | - | if returnFunc, ok := ret.Get(0).(func(string, string, ports.RemoteTransportMode, *slog.Logger) ports.Dialer); ok { | |
| 58 | - | r0 = returnFunc(target, session, mode, log) | |
| 59 | - | } else { | |
| 60 | - | if ret.Get(0) != nil { | |
| 61 | - | r0 = ret.Get(0).(ports.Dialer) | |
| 62 | - | } | |
| 63 | - | } | |
| 64 | - | if returnFunc, ok := ret.Get(1).(func(string, string, ports.RemoteTransportMode, *slog.Logger) error); ok { | |
| 65 | - | r1 = returnFunc(target, session, mode, log) | |
| 66 | - | } else { | |
| 67 | - | r1 = ret.Error(1) | |
| 68 | - | } | |
| 69 | - | return r0, r1 | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | // MockRemoteDialerFactory_DialerForRemote_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DialerForRemote' | |
| 73 | - | type MockRemoteDialerFactory_DialerForRemote_Call struct { | |
| 74 | - | *mock.Call | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | // DialerForRemote is a helper method to define mock.On call | |
| 78 | - | // - target string | |
| 79 | - | // - session string | |
| 80 | - | // - mode ports.RemoteTransportMode | |
| 81 | - | // - log *slog.Logger | |
| 82 | - | func (_e *MockRemoteDialerFactory_Expecter) DialerForRemote(target any, session any, mode any, log any) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 83 | - | return &MockRemoteDialerFactory_DialerForRemote_Call{Call: _e.mock.On("DialerForRemote", target, session, mode, log)} | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Run(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger)) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 87 | - | _c.Call.Run(func(args mock.Arguments) { | |
| 88 | - | var arg0 string | |
| 89 | - | if args[0] != nil { | |
| 90 | - | arg0 = args[0].(string) | |
| 91 | - | } | |
| 92 | - | var arg1 string | |
| 93 | - | if args[1] != nil { | |
| 94 | - | arg1 = args[1].(string) | |
| 95 | - | } | |
| 96 | - | var arg2 ports.RemoteTransportMode | |
| 97 | - | if args[2] != nil { | |
| 98 | - | arg2 = args[2].(ports.RemoteTransportMode) | |
| 99 | - | } | |
| 100 | - | var arg3 *slog.Logger | |
| 101 | - | if args[3] != nil { | |
| 102 | - | arg3 = args[3].(*slog.Logger) | |
| 103 | - | } | |
| 104 | - | run( | |
| 105 | - | arg0, | |
| 106 | - | arg1, | |
| 107 | - | arg2, | |
| 108 | - | arg3, | |
| 109 | - | ) | |
| 110 | - | }) | |
| 111 | - | return _c | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) Return(dialer ports.Dialer, err error) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 115 | - | _c.Call.Return(dialer, err) | |
| 116 | - | return _c | |
| 117 | - | } | |
| 118 | - | ||
| 119 | - | func (_c *MockRemoteDialerFactory_DialerForRemote_Call) RunAndReturn(run func(target string, session string, mode ports.RemoteTransportMode, log *slog.Logger) (ports.Dialer, error)) *MockRemoteDialerFactory_DialerForRemote_Call { | |
| 120 | - | _c.Call.Return(run) | |
| 121 | - | return _c | |
| 122 | - | } | |
| 123 | - | ||
| 124 | 16 | // NewMockRemoteHostStore creates a new instance of MockRemoteHostStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 125 | 17 | // The first argument is typically a *testing.T value. | |
| 126 | 18 | func NewMockRemoteHostStore(t interface { | |
| @@ -574,22 +466,22 @@ func (_m *MockRemoteCatalogClient) EXPECT() *MockRemoteCatalogClient_Expecter { | |||
| 574 | 466 | } | |
| 575 | 467 | ||
| 576 | 468 | // List provides a mock function for the type MockRemoteCatalogClient | |
| 577 | - | func (_mock *MockRemoteCatalogClient) List(ctx context.Context, target string) (ports.RemoteCatalog, error) { | |
| 469 | + | func (_mock *MockRemoteCatalogClient) List(ctx context.Context, target string) (catalogue.RemoteCatalog, error) { | |
| 578 | 470 | ret := _mock.Called(ctx, target) | |
| 579 | 471 | ||
| 580 | 472 | if len(ret) == 0 { | |
| 581 | 473 | panic("no return value specified for List") | |
| 582 | 474 | } | |
| 583 | 475 | ||
| 584 | - | var r0 ports.RemoteCatalog | |
| 476 | + | var r0 catalogue.RemoteCatalog | |
| 585 | 477 | var r1 error | |
| 586 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (ports.RemoteCatalog, error)); ok { | |
| 478 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (catalogue.RemoteCatalog, error)); ok { | |
| 587 | 479 | return returnFunc(ctx, target) | |
| 588 | 480 | } | |
| 589 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ports.RemoteCatalog); ok { | |
| 481 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) catalogue.RemoteCatalog); ok { | |
| 590 | 482 | r0 = returnFunc(ctx, target) | |
| 591 | 483 | } else { | |
| 592 | - | r0 = ret.Get(0).(ports.RemoteCatalog) | |
| 484 | + | r0 = ret.Get(0).(catalogue.RemoteCatalog) | |
| 593 | 485 | } | |
| 594 | 486 | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 595 | 487 | r1 = returnFunc(ctx, target) | |
| @@ -629,12 +521,12 @@ func (_c *MockRemoteCatalogClient_List_Call) Run(run func(ctx context.Context, t | |||
| 629 | 521 | return _c | |
| 630 | 522 | } | |
| 631 | 523 | ||
| 632 | - | func (_c *MockRemoteCatalogClient_List_Call) Return(remoteCatalog ports.RemoteCatalog, err error) *MockRemoteCatalogClient_List_Call { | |
| 524 | + | func (_c *MockRemoteCatalogClient_List_Call) Return(remoteCatalog catalogue.RemoteCatalog, err error) *MockRemoteCatalogClient_List_Call { | |
| 633 | 525 | _c.Call.Return(remoteCatalog, err) | |
| 634 | 526 | return _c | |
| 635 | 527 | } | |
| 636 | 528 | ||
| 637 | - | func (_c *MockRemoteCatalogClient_List_Call) RunAndReturn(run func(ctx context.Context, target string) (ports.RemoteCatalog, error)) *MockRemoteCatalogClient_List_Call { | |
| 529 | + | func (_c *MockRemoteCatalogClient_List_Call) RunAndReturn(run func(ctx context.Context, target string) (catalogue.RemoteCatalog, error)) *MockRemoteCatalogClient_List_Call { | |
| 638 | 530 | _c.Call.Return(run) | |
| 639 | 531 | return _c | |
| 640 | 532 | } | |
| @@ -772,23 +664,23 @@ func (_m *MockRemoteCatalogCache) EXPECT() *MockRemoteCatalogCache_Expecter { | |||
| 772 | 664 | } | |
| 773 | 665 | ||
| 774 | 666 | // Load provides a mock function for the type MockRemoteCatalogCache | |
| 775 | - | func (_mock *MockRemoteCatalogCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 667 | + | func (_mock *MockRemoteCatalogCache) Load() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 776 | 668 | ret := _mock.Called() | |
| 777 | 669 | ||
| 778 | 670 | if len(ret) == 0 { | |
| 779 | 671 | panic("no return value specified for Load") | |
| 780 | 672 | } | |
| 781 | 673 | ||
| 782 | - | var r0 []ports.RemoteCatalogCacheEntry | |
| 674 | + | var r0 []catalogue.RemoteCatalogCacheEntry | |
| 783 | 675 | var r1 error | |
| 784 | - | if returnFunc, ok := ret.Get(0).(func() ([]ports.RemoteCatalogCacheEntry, error)); ok { | |
| 676 | + | if returnFunc, ok := ret.Get(0).(func() ([]catalogue.RemoteCatalogCacheEntry, error)); ok { | |
| 785 | 677 | return returnFunc() | |
| 786 | 678 | } | |
| 787 | - | if returnFunc, ok := ret.Get(0).(func() []ports.RemoteCatalogCacheEntry); ok { | |
| 679 | + | if returnFunc, ok := ret.Get(0).(func() []catalogue.RemoteCatalogCacheEntry); ok { | |
| 788 | 680 | r0 = returnFunc() | |
| 789 | 681 | } else { | |
| 790 | 682 | if ret.Get(0) != nil { | |
| 791 | - | r0 = ret.Get(0).([]ports.RemoteCatalogCacheEntry) | |
| 683 | + | r0 = ret.Get(0).([]catalogue.RemoteCatalogCacheEntry) | |
| 792 | 684 | } | |
| 793 | 685 | } | |
| 794 | 686 | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| @@ -816,18 +708,18 @@ func (_c *MockRemoteCatalogCache_Load_Call) Run(run func()) *MockRemoteCatalogCa | |||
| 816 | 708 | return _c | |
| 817 | 709 | } | |
| 818 | 710 | ||
| 819 | - | func (_c *MockRemoteCatalogCache_Load_Call) Return(remoteCatalogCacheEntrys []ports.RemoteCatalogCacheEntry, err error) *MockRemoteCatalogCache_Load_Call { | |
| 711 | + | func (_c *MockRemoteCatalogCache_Load_Call) Return(remoteCatalogCacheEntrys []catalogue.RemoteCatalogCacheEntry, err error) *MockRemoteCatalogCache_Load_Call { | |
| 820 | 712 | _c.Call.Return(remoteCatalogCacheEntrys, err) | |
| 821 | 713 | return _c | |
| 822 | 714 | } | |
| 823 | 715 | ||
| 824 | - | func (_c *MockRemoteCatalogCache_Load_Call) RunAndReturn(run func() ([]ports.RemoteCatalogCacheEntry, error)) *MockRemoteCatalogCache_Load_Call { | |
| 716 | + | func (_c *MockRemoteCatalogCache_Load_Call) RunAndReturn(run func() ([]catalogue.RemoteCatalogCacheEntry, error)) *MockRemoteCatalogCache_Load_Call { | |
| 825 | 717 | _c.Call.Return(run) | |
| 826 | 718 | return _c | |
| 827 | 719 | } | |
| 828 | 720 | ||
| 829 | 721 | // Store provides a mock function for the type MockRemoteCatalogCache | |
| 830 | - | func (_mock *MockRemoteCatalogCache) Store(remoteCatalogCacheEntrys []ports.RemoteCatalogCacheEntry) error { | |
| 722 | + | func (_mock *MockRemoteCatalogCache) Store(remoteCatalogCacheEntrys []catalogue.RemoteCatalogCacheEntry) error { | |
| 831 | 723 | ret := _mock.Called(remoteCatalogCacheEntrys) | |
| 832 | 724 | ||
| 833 | 725 | if len(ret) == 0 { | |
| @@ -835,7 +727,7 @@ func (_mock *MockRemoteCatalogCache) Store(remoteCatalogCacheEntrys []ports.Remo | |||
| 835 | 727 | } | |
| 836 | 728 | ||
| 837 | 729 | var r0 error | |
| 838 | - | if returnFunc, ok := ret.Get(0).(func([]ports.RemoteCatalogCacheEntry) error); ok { | |
| 730 | + | if returnFunc, ok := ret.Get(0).(func([]catalogue.RemoteCatalogCacheEntry) error); ok { | |
| 839 | 731 | r0 = returnFunc(remoteCatalogCacheEntrys) | |
| 840 | 732 | } else { | |
| 841 | 733 | r0 = ret.Error(0) | |
| @@ -849,16 +741,16 @@ type MockRemoteCatalogCache_Store_Call struct { | |||
| 849 | 741 | } | |
| 850 | 742 | ||
| 851 | 743 | // Store is a helper method to define mock.On call | |
| 852 | - | // - remoteCatalogCacheEntrys []ports.RemoteCatalogCacheEntry | |
| 744 | + | // - remoteCatalogCacheEntrys []catalogue.RemoteCatalogCacheEntry | |
| 853 | 745 | func (_e *MockRemoteCatalogCache_Expecter) Store(remoteCatalogCacheEntrys any) *MockRemoteCatalogCache_Store_Call { | |
| 854 | 746 | return &MockRemoteCatalogCache_Store_Call{Call: _e.mock.On("Store", remoteCatalogCacheEntrys)} | |
| 855 | 747 | } | |
| 856 | 748 | ||
| 857 | - | func (_c *MockRemoteCatalogCache_Store_Call) Run(run func(remoteCatalogCacheEntrys []ports.RemoteCatalogCacheEntry)) *MockRemoteCatalogCache_Store_Call { | |
| 749 | + | func (_c *MockRemoteCatalogCache_Store_Call) Run(run func(remoteCatalogCacheEntrys []catalogue.RemoteCatalogCacheEntry)) *MockRemoteCatalogCache_Store_Call { | |
| 858 | 750 | _c.Call.Run(func(args mock.Arguments) { | |
| 859 | - | var arg0 []ports.RemoteCatalogCacheEntry | |
| 751 | + | var arg0 []catalogue.RemoteCatalogCacheEntry | |
| 860 | 752 | if args[0] != nil { | |
| 861 | - | arg0 = args[0].([]ports.RemoteCatalogCacheEntry) | |
| 753 | + | arg0 = args[0].([]catalogue.RemoteCatalogCacheEntry) | |
| 862 | 754 | } | |
| 863 | 755 | run( | |
| 864 | 756 | arg0, | |
| @@ -872,7 +764,7 @@ func (_c *MockRemoteCatalogCache_Store_Call) Return(err error) *MockRemoteCatalo | |||
| 872 | 764 | return _c | |
| 873 | 765 | } | |
| 874 | 766 | ||
| 875 | - | func (_c *MockRemoteCatalogCache_Store_Call) RunAndReturn(run func(remoteCatalogCacheEntrys []ports.RemoteCatalogCacheEntry) error) *MockRemoteCatalogCache_Store_Call { | |
| 767 | + | func (_c *MockRemoteCatalogCache_Store_Call) RunAndReturn(run func(remoteCatalogCacheEntrys []catalogue.RemoteCatalogCacheEntry) error) *MockRemoteCatalogCache_Store_Call { | |
| 876 | 768 | _c.Call.Return(run) | |
| 877 | 769 | return _c | |
| 878 | 770 | } | |
M internal/ports/remote.go +8 -306
| @@ -2,338 +2,40 @@ package ports | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "errors" | |
| 6 | - | "fmt" | |
| 7 | - | "log/slog" | |
| 8 | - | "math" | |
| 9 | - | "slices" | |
| 10 | - | "time" | |
| 11 | - | "unicode" | |
| 12 | - | "unicode/utf8" | |
| 13 | 5 | ||
| 14 | 6 | "github.com/bnema/vev/internal/domain" | |
| 15 | 7 | "github.com/bnema/vev/internal/protocol" | |
| 8 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 16 | 9 | ) | |
| 17 | 10 | ||
| 18 | - | // RemoteTransportMode selects the transport used for remote attach after CLI parsing. | |
| 19 | - | type RemoteTransportMode string | |
| 20 | - | ||
| 21 | - | const ( | |
| 22 | - | RemoteTransportUDP RemoteTransportMode = "udp" | |
| 23 | - | RemoteTransportStdio RemoteTransportMode = "stdio" | |
| 24 | - | ) | |
| 25 | - | ||
| 26 | - | // RemoteDialerFactory builds the transport-specific dialer for a remote target. | |
| 27 | - | // Implementations live in adapters; app wiring consumes this interface. | |
| 28 | - | type RemoteDialerFactory interface { | |
| 29 | - | DialerForRemote(target string, session string, mode RemoteTransportMode, log *slog.Logger) (Dialer, error) | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | // RemoteHostStore persists pinned and learned remote host targets in one | |
| 33 | - | // versioned state file across processes. | |
| 11 | + | // RemoteHostStore persists pinned and learned remote host targets. | |
| 34 | 12 | type RemoteHostStore interface { | |
| 35 | - | // Hosts returns pinned hosts in stored order and learned hosts in lexical order. | |
| 36 | 13 | Hosts() (pinned, learned []string, err error) | |
| 37 | 14 | AddPinned(target string) error | |
| 38 | 15 | RemovePinned(target string) error | |
| 39 | 16 | Remember(target string) error | |
| 40 | 17 | Forget(target string) error | |
| 41 | - | // Remove deletes target from both pinned and learned lists atomically and | |
| 42 | - | // reports whether the target existed in either list. | |
| 43 | 18 | Remove(target string) (deleted bool, err error) | |
| 44 | 19 | } | |
| 45 | 20 | ||
| 46 | - | // RemoteHostLearner records the remote target after a successful attach. | |
| 47 | - | // It deliberately has no arguments because app captures the validated target | |
| 48 | - | // while constructing the client dependency. | |
| 21 | + | // RemoteHostLearner records the validated remote target after an attach. | |
| 49 | 22 | type RemoteHostLearner interface { | |
| 50 | 23 | RememberRemoteHost() error | |
| 51 | 24 | } | |
| 52 | 25 | ||
| 53 | - | // RemoteCatalogClient fetches a versioned session catalog from a remote host. | |
| 26 | + | // RemoteCatalogClient fetches a versioned session catalogue from a remote host. | |
| 54 | 27 | type RemoteCatalogClient interface { | |
| 55 | - | List(ctx context.Context, target string) (RemoteCatalog, error) | |
| 28 | + | List(ctx context.Context, target string) (catalogue.RemoteCatalog, error) | |
| 56 | 29 | } | |
| 57 | 30 | ||
| 58 | - | // RemotePreviewClient fetches one bounded, exact-target viewport without | |
| 59 | - | // attaching to or mutating the remote session. | |
| 31 | + | // RemotePreviewClient fetches one bounded, exact-target viewport. | |
| 60 | 32 | type RemotePreviewClient interface { | |
| 61 | 33 | Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) | |
| 62 | 34 | } | |
| 63 | 35 | ||
| 64 | - | const ( | |
| 65 | - | // RemoteCatalogSchemaVersion is independent from the IPC protocol. A | |
| 66 | - | // catalogue can be rejected without changing the attachment wire layout. | |
| 67 | - | RemoteCatalogSchemaVersion uint16 = 3 | |
| 68 | - | ||
| 69 | - | RemoteCatalogMaxHosts = 64 | |
| 70 | - | RemoteCatalogMaxSessions = 256 | |
| 71 | - | RemoteCatalogMaxTabsPerSess = 128 | |
| 72 | - | RemoteCatalogMaxNameBytes = 256 | |
| 73 | - | RemoteCatalogMaxDetailBytes = 512 | |
| 74 | - | RemoteCatalogMaxIDBytes = 128 | |
| 75 | - | RemoteCatalogMaxCatalogBytes = 1 << 20 | |
| 76 | - | ) | |
| 77 | - | ||
| 78 | - | // RemoteCatalogCacheEntry is one immutable exact-schema host snapshot persisted | |
| 79 | - | // for immediate remote discovery at daemon startup. Cache writers strip dynamic | |
| 80 | - | // Detail and Attention fields before persistence. | |
| 81 | - | type RemoteCatalogCacheEntry struct { | |
| 82 | - | Host string | |
| 83 | - | FetchedAt time.Time | |
| 84 | - | Sessions []RemoteCatalogSession | |
| 85 | - | } | |
| 86 | - | ||
| 87 | 36 | // RemoteCatalogCache persists complete remote discovery snapshots independently | |
| 88 | 37 | // from the remote host registry. | |
| 89 | 38 | type RemoteCatalogCache interface { | |
| 90 | - | Load() ([]RemoteCatalogCacheEntry, error) | |
| 91 | - | Store([]RemoteCatalogCacheEntry) error | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | // RemoteCatalogTab is one bounded, untrusted tab presentation hint. Detail | |
| 95 | - | // and Attention are deliberately omitted from the durable cache. | |
| 96 | - | type RemoteCatalogTab struct { | |
| 97 | - | ID string `json:"id"` | |
| 98 | - | Index uint16 `json:"index"` | |
| 99 | - | Name string `json:"name"` | |
| 100 | - | Detail string `json:"detail,omitempty"` | |
| 101 | - | Attention bool `json:"attention,omitempty"` | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | // RemoteCatalogSessionState is the JSON catalogue lifecycle contract. It is | |
| 105 | - | // deliberately independent from SessionState, whose numeric values are part of | |
| 106 | - | // the binary client/daemon protocol. | |
| 107 | - | type RemoteCatalogSessionState string | |
| 108 | - | ||
| 109 | - | const ( | |
| 110 | - | RemoteCatalogSessionUp RemoteCatalogSessionState = "up" | |
| 111 | - | RemoteCatalogSessionDown RemoteCatalogSessionState = "down" | |
| 112 | - | RemoteCatalogSessionBroken RemoteCatalogSessionState = "broken" | |
| 113 | - | ) | |
| 114 | - | ||
| 115 | - | // Valid reports whether the state is one of the bounded catalogue values. | |
| 116 | - | func (s RemoteCatalogSessionState) Valid() bool { | |
| 117 | - | switch s { | |
| 118 | - | case RemoteCatalogSessionUp, RemoteCatalogSessionDown, RemoteCatalogSessionBroken: | |
| 119 | - | return true | |
| 120 | - | default: | |
| 121 | - | return false | |
| 122 | - | } | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | // RemoteCatalogSession is one exact-identity session in the remote discovery | |
| 126 | - | // catalogue. Tabs is always an ordered typed snapshot; older count-only peers | |
| 127 | - | // are rejected at the schema seam. | |
| 128 | - | type RemoteCatalogSession struct { | |
| 129 | - | LifecycleID domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 130 | - | Name string `json:"name"` | |
| 131 | - | State RemoteCatalogSessionState `json:"state"` | |
| 132 | - | Ephemeral bool `json:"ephemeral"` | |
| 133 | - | Tabs []RemoteCatalogTab `json:"tabs"` | |
| 134 | - | Attached bool `json:"attached"` | |
| 135 | - | LastUsedSeq uint64 `json:"last_used_seq,omitempty"` | |
| 136 | - | ActiveTabID string `json:"active_tab_id,omitempty"` | |
| 137 | - | Reason string `json:"reason,omitempty"` | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | // CatalogTabs returns an independent copy of the ordered tab snapshot. | |
| 141 | - | func CatalogTabs(session RemoteCatalogSession) []RemoteCatalogTab { | |
| 142 | - | return slices.Clone(session.Tabs) | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | // CatalogTabCount returns the typed tab count. | |
| 146 | - | func CatalogTabCount(session RemoteCatalogSession) int { | |
| 147 | - | return len(session.Tabs) | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | // SaturateUint16 clamps a catalogue tab count to the list protocol range. | |
| 151 | - | func SaturateUint16(n int) uint16 { | |
| 152 | - | if n <= 0 { | |
| 153 | - | return 0 | |
| 154 | - | } | |
| 155 | - | if n >= 1<<16 { | |
| 156 | - | return math.MaxUint16 | |
| 157 | - | } | |
| 158 | - | return uint16(n) | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | // RemoteCatalog is the independently versioned JSON envelope returned by | |
| 162 | - | // remote-catalog --json. Both protocol and catalogue schema versions are | |
| 163 | - | // mandatory and must match exactly. | |
| 164 | - | type RemoteCatalog struct { | |
| 165 | - | ProtocolVersion uint16 `json:"protocol_version"` | |
| 166 | - | SchemaVersion uint16 `json:"schema_version"` | |
| 167 | - | Sessions []RemoteCatalogSession `json:"sessions"` | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | // RemoteCatalogVersionMismatchError is returned when a remote catalog envelope | |
| 171 | - | // reports an incompatible protocol or catalogue schema version. | |
| 172 | - | type RemoteCatalogVersionMismatchError struct { | |
| 173 | - | Got uint16 | |
| 174 | - | Want uint16 | |
| 175 | - | Kind string | |
| 176 | - | } | |
| 177 | - | ||
| 178 | - | func (e *RemoteCatalogVersionMismatchError) Error() string { | |
| 179 | - | kind := e.Kind | |
| 180 | - | if kind == "" { | |
| 181 | - | kind = "protocol" | |
| 182 | - | } | |
| 183 | - | return fmt.Sprintf("remote catalog: %s version mismatch: got %d, want %d", kind, e.Got, e.Want) | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | var ( | |
| 187 | - | ErrInvalidRemoteCatalog = errors.New("ports: invalid remote catalog") | |
| 188 | - | ErrRemoteCatalogTooLarge = errors.New("ports: remote catalog exceeds size limit") | |
| 189 | - | ErrRemoteCatalogUnknownState = errors.New("ports: remote catalog has unknown state") | |
| 190 | - | ErrRemoteCatalogInvalidReason = errors.New("ports: remote catalog has unknown reason") | |
| 191 | - | ) | |
| 192 | - | ||
| 193 | - | // ValidateRemoteCatalog applies the exact current schema and all bounds before | |
| 194 | - | // a catalogue is cached or rendered. | |
| 195 | - | func ValidateRemoteCatalog(c RemoteCatalog) error { | |
| 196 | - | if c.ProtocolVersion != protocol.Version { | |
| 197 | - | return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: protocol.Version, Kind: "protocol"} | |
| 198 | - | } | |
| 199 | - | if c.SchemaVersion != RemoteCatalogSchemaVersion { | |
| 200 | - | return &RemoteCatalogVersionMismatchError{Got: c.SchemaVersion, Want: RemoteCatalogSchemaVersion, Kind: "catalog"} | |
| 201 | - | } | |
| 202 | - | if len(c.Sessions) > RemoteCatalogMaxSessions { | |
| 203 | - | return fmt.Errorf("%w: too many sessions", ErrRemoteCatalogTooLarge) | |
| 204 | - | } | |
| 205 | - | seenLifecycles := make(map[domain.SessionLifecycleID]string, len(c.Sessions)) | |
| 206 | - | seenNames := make(map[string]struct{}, len(c.Sessions)) | |
| 207 | - | bytes := 0 | |
| 208 | - | for _, session := range c.Sessions { | |
| 209 | - | if !validRemoteCatalogText(session.Name) || !validRemoteCatalogText(string(session.State)) || !validRemoteCatalogText(session.Reason) { | |
| 210 | - | return fmt.Errorf("%w: invalid session text", ErrInvalidRemoteCatalog) | |
| 211 | - | } | |
| 212 | - | if len(session.Name) > RemoteCatalogMaxNameBytes || len(session.Reason) > RemoteCatalogMaxDetailBytes { | |
| 213 | - | return fmt.Errorf("%w: session text too long", ErrRemoteCatalogTooLarge) | |
| 214 | - | } | |
| 215 | - | if err := domain.ValidateSessionName(session.Name); err != nil { | |
| 216 | - | return fmt.Errorf("%w: session name: %v", ErrInvalidRemoteCatalog, err) | |
| 217 | - | } | |
| 218 | - | if !session.State.Valid() { | |
| 219 | - | return fmt.Errorf("%w: %q", ErrRemoteCatalogUnknownState, session.State) | |
| 220 | - | } | |
| 221 | - | if session.ActiveTabID != "" { | |
| 222 | - | if err := domain.ValidateTabStableID(domain.TabStableID(session.ActiveTabID)); err != nil { | |
| 223 | - | return fmt.Errorf("%w: invalid active tab ID", ErrInvalidRemoteCatalog) | |
| 224 | - | } | |
| 225 | - | } | |
| 226 | - | if session.Reason != "" && !validRemoteCatalogReason(session.Reason) { | |
| 227 | - | return fmt.Errorf("%w: %q", ErrRemoteCatalogInvalidReason, session.Reason) | |
| 228 | - | } | |
| 229 | - | if session.LifecycleID == (domain.SessionLifecycleID{}) { | |
| 230 | - | return fmt.Errorf("%w: zero lifecycle ID", ErrInvalidRemoteCatalog) | |
| 231 | - | } | |
| 232 | - | if _, exists := seenNames[session.Name]; exists { | |
| 233 | - | return fmt.Errorf("%w: duplicate session name %q", ErrInvalidRemoteCatalog, session.Name) | |
| 234 | - | } | |
| 235 | - | seenNames[session.Name] = struct{}{} | |
| 236 | - | if prior, exists := seenLifecycles[session.LifecycleID]; exists { | |
| 237 | - | return fmt.Errorf("%w: lifecycle ID reused by %q and %q", ErrInvalidRemoteCatalog, prior, session.Name) | |
| 238 | - | } | |
| 239 | - | seenLifecycles[session.LifecycleID] = session.Name | |
| 240 | - | if session.Tabs == nil { | |
| 241 | - | return fmt.Errorf("%w: missing tab list", ErrInvalidRemoteCatalog) | |
| 242 | - | } | |
| 243 | - | tabs := CatalogTabs(session) | |
| 244 | - | if len(tabs) == 0 { | |
| 245 | - | if session.ActiveTabID != "" { | |
| 246 | - | return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog) | |
| 247 | - | } | |
| 248 | - | } else { | |
| 249 | - | if len(tabs) > RemoteCatalogMaxTabsPerSess { | |
| 250 | - | return fmt.Errorf("%w: too many tabs", ErrRemoteCatalogTooLarge) | |
| 251 | - | } | |
| 252 | - | tabIDs := make(map[string]struct{}, len(tabs)) | |
| 253 | - | for i, tab := range tabs { | |
| 254 | - | if !validRemoteCatalogText(tab.ID) || !validRemoteCatalogText(tab.Name) || !validRemoteCatalogText(tab.Detail) { | |
| 255 | - | return fmt.Errorf("%w: invalid tab text", ErrInvalidRemoteCatalog) | |
| 256 | - | } | |
| 257 | - | if len(tab.ID) > RemoteCatalogMaxIDBytes || len(tab.Name) > RemoteCatalogMaxNameBytes || len(tab.Detail) > RemoteCatalogMaxDetailBytes { | |
| 258 | - | return fmt.Errorf("%w: tab text too long", ErrRemoteCatalogTooLarge) | |
| 259 | - | } | |
| 260 | - | if tab.Index != uint16(i) { | |
| 261 | - | return fmt.Errorf("%w: tab indexes are not ordered", ErrInvalidRemoteCatalog) | |
| 262 | - | } | |
| 263 | - | if session.State == RemoteCatalogSessionUp && tab.ID == "" { | |
| 264 | - | return fmt.Errorf("%w: up tab has zero ID", ErrInvalidRemoteCatalog) | |
| 265 | - | } | |
| 266 | - | if tab.ID != "" { | |
| 267 | - | if err := domain.ValidateTabStableID(domain.TabStableID(tab.ID)); err != nil { | |
| 268 | - | return fmt.Errorf("%w: invalid tab ID", ErrInvalidRemoteCatalog) | |
| 269 | - | } | |
| 270 | - | if _, duplicate := tabIDs[tab.ID]; duplicate { | |
| 271 | - | return fmt.Errorf("%w: duplicate tab ID", ErrInvalidRemoteCatalog) | |
| 272 | - | } | |
| 273 | - | tabIDs[tab.ID] = struct{}{} | |
| 274 | - | } | |
| 275 | - | bytes += len(tab.ID) + len(tab.Name) + len(tab.Detail) | |
| 276 | - | } | |
| 277 | - | if session.ActiveTabID != "" { | |
| 278 | - | if session.State != RemoteCatalogSessionUp { | |
| 279 | - | return fmt.Errorf("%w: down or broken session has an active tab", ErrInvalidRemoteCatalog) | |
| 280 | - | } | |
| 281 | - | if _, ok := tabIDs[session.ActiveTabID]; !ok { | |
| 282 | - | return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog) | |
| 283 | - | } | |
| 284 | - | } | |
| 285 | - | } | |
| 286 | - | bytes += len(session.Name) + len(session.State) + len(session.Reason) | |
| 287 | - | } | |
| 288 | - | if bytes > RemoteCatalogMaxCatalogBytes { | |
| 289 | - | return ErrRemoteCatalogTooLarge | |
| 290 | - | } | |
| 291 | - | return nil | |
| 292 | - | } | |
| 293 | - | ||
| 294 | - | // ValidateRemoteCatalogCacheEntries validates the bounded exact-schema cache | |
| 295 | - | // DTO before it is loaded into picker state or written to disk. | |
| 296 | - | func ValidateRemoteCatalogCacheEntries(entries []RemoteCatalogCacheEntry) error { | |
| 297 | - | if len(entries) > RemoteCatalogMaxHosts { | |
| 298 | - | return fmt.Errorf("%w: too many hosts", ErrRemoteCatalogTooLarge) | |
| 299 | - | } | |
| 300 | - | seenHosts := make(map[string]struct{}, len(entries)) | |
| 301 | - | for _, entry := range entries { | |
| 302 | - | if err := domain.ValidateRemoteHostTarget(entry.Host); err != nil || len(entry.Host) > RemoteCatalogMaxNameBytes { | |
| 303 | - | return fmt.Errorf("%w: invalid host", ErrInvalidRemoteCatalog) | |
| 304 | - | } | |
| 305 | - | if entry.FetchedAt.IsZero() || entry.FetchedAt.UnixNano() <= 0 { | |
| 306 | - | return fmt.Errorf("%w: invalid fetched time", ErrInvalidRemoteCatalog) | |
| 307 | - | } | |
| 308 | - | if _, exists := seenHosts[entry.Host]; exists { | |
| 309 | - | return fmt.Errorf("%w: duplicate host", ErrInvalidRemoteCatalog) | |
| 310 | - | } | |
| 311 | - | seenHosts[entry.Host] = struct{}{} | |
| 312 | - | catalog := RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions} | |
| 313 | - | if err := ValidateRemoteCatalog(catalog); err != nil { | |
| 314 | - | return fmt.Errorf("%w: host %q: %v", ErrInvalidRemoteCatalog, entry.Host, err) | |
| 315 | - | } | |
| 316 | - | } | |
| 317 | - | return nil | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | func validRemoteCatalogText(value string) bool { | |
| 321 | - | if !utf8.ValidString(value) { | |
| 322 | - | return false | |
| 323 | - | } | |
| 324 | - | for _, r := range value { | |
| 325 | - | if unicode.IsControl(r) { | |
| 326 | - | return false | |
| 327 | - | } | |
| 328 | - | } | |
| 329 | - | return true | |
| 330 | - | } | |
| 331 | - | ||
| 332 | - | func validRemoteCatalogReason(reason string) bool { | |
| 333 | - | switch reason { | |
| 334 | - | case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_down", "session_broken", "identity_changed", "not_found", "timeout", "malformed": | |
| 335 | - | return true | |
| 336 | - | default: | |
| 337 | - | return false | |
| 338 | - | } | |
| 39 | + | Load() ([]catalogue.RemoteCatalogCacheEntry, error) | |
| 40 | + | Store([]catalogue.RemoteCatalogCacheEntry) error | |
| 339 | 41 | } | |
M internal/ports/runtime_observer.go +0 -138
| @@ -2,7 +2,6 @@ package ports | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | - | "sync" | |
| 6 | 5 | "sync/atomic" | |
| 7 | 6 | ) | |
| 8 | 7 | ||
| @@ -75,143 +74,6 @@ type SerializedRuntimeObserver interface { | |||
| 75 | 74 | Close() | |
| 76 | 75 | } | |
| 77 | 76 | ||
| 78 | - | // NewSerializedRuntimeObserver isolates producers from a potentially slow | |
| 79 | - | // observer with one ordered worker. capacity bounds queued marks. If the queue | |
| 80 | - | // fills, dropped marks are represented by one ordered diagnostic mark before | |
| 81 | - | // reporting resumes, so a trace can never silently claim completeness. | |
| 82 | - | func NewSerializedRuntimeObserver(observer RuntimeObserver, capacity int) SerializedRuntimeObserver { | |
| 83 | - | if observer == nil { | |
| 84 | - | return nil | |
| 85 | - | } | |
| 86 | - | if capacity < 1 { | |
| 87 | - | capacity = 1 | |
| 88 | - | } | |
| 89 | - | o := &serializedRuntimeObserver{observer: observer, capacity: capacity, done: make(chan struct{})} | |
| 90 | - | o.cond = sync.NewCond(&o.mu) | |
| 91 | - | go o.run() | |
| 92 | - | return o | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | // EnsureSerializedRuntimeObserver returns observer's existing serialized | |
| 96 | - | // owner when it has one. Otherwise it creates an owned worker. Callers close | |
| 97 | - | // the result only when owned is true, preventing nested workers and duplicate | |
| 98 | - | // lifecycle ownership when application wiring already serialized the process | |
| 99 | - | // observer. | |
| 100 | - | func EnsureSerializedRuntimeObserver(observer RuntimeObserver, capacity int) (serialized SerializedRuntimeObserver, owned bool) { | |
| 101 | - | if observer == nil { | |
| 102 | - | return nil, false | |
| 103 | - | } | |
| 104 | - | if serialized, ok := observer.(SerializedRuntimeObserver); ok { | |
| 105 | - | return serialized, false | |
| 106 | - | } | |
| 107 | - | return NewSerializedRuntimeObserver(observer, capacity), true | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | type serializedRuntimeObserver struct { | |
| 111 | - | observer RuntimeObserver | |
| 112 | - | capacity int | |
| 113 | - | ||
| 114 | - | mu sync.Mutex | |
| 115 | - | cond *sync.Cond | |
| 116 | - | marks []RuntimeMark | |
| 117 | - | dropped uint64 | |
| 118 | - | closed bool | |
| 119 | - | active bool | |
| 120 | - | done chan struct{} | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | func (o *serializedRuntimeObserver) ObserveRuntime(mark RuntimeMark) { | |
| 124 | - | if o == nil { | |
| 125 | - | return | |
| 126 | - | } | |
| 127 | - | o.mu.Lock() | |
| 128 | - | defer o.mu.Unlock() | |
| 129 | - | if o.closed { | |
| 130 | - | return | |
| 131 | - | } | |
| 132 | - | if o.dropped != 0 { | |
| 133 | - | // Reserve space for both the gap marker and the first post-gap mark. | |
| 134 | - | // This preserves the producer order around a bounded-queue loss. | |
| 135 | - | if len(o.marks)+2 > o.capacity { | |
| 136 | - | o.dropped++ | |
| 137 | - | return | |
| 138 | - | } | |
| 139 | - | o.marks = append(o.marks, runtimeObserverGapMark(o.dropped), mark) | |
| 140 | - | o.dropped = 0 | |
| 141 | - | o.cond.Signal() | |
| 142 | - | return | |
| 143 | - | } | |
| 144 | - | if len(o.marks) == o.capacity { | |
| 145 | - | o.dropped = 1 | |
| 146 | - | return | |
| 147 | - | } | |
| 148 | - | o.marks = append(o.marks, mark) | |
| 149 | - | o.cond.Signal() | |
| 150 | - | } | |
| 151 | - | ||
| 152 | - | func runtimeObserverGapMark(dropped uint64) RuntimeMark { | |
| 153 | - | return NewRuntimeMark("observer", RuntimeTransportDiagnostic, dropped, false) | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | func (o *serializedRuntimeObserver) Flush() { | |
| 157 | - | if o == nil { | |
| 158 | - | return | |
| 159 | - | } | |
| 160 | - | o.mu.Lock() | |
| 161 | - | for len(o.marks) != 0 || o.active || o.dropped != 0 { | |
| 162 | - | o.cond.Signal() | |
| 163 | - | o.cond.Wait() | |
| 164 | - | } | |
| 165 | - | closed := o.closed | |
| 166 | - | o.mu.Unlock() | |
| 167 | - | if closed { | |
| 168 | - | <-o.done | |
| 169 | - | } | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | func (o *serializedRuntimeObserver) Close() { | |
| 173 | - | if o == nil { | |
| 174 | - | return | |
| 175 | - | } | |
| 176 | - | o.mu.Lock() | |
| 177 | - | if !o.closed { | |
| 178 | - | o.closed = true | |
| 179 | - | o.cond.Broadcast() | |
| 180 | - | } | |
| 181 | - | o.mu.Unlock() | |
| 182 | - | <-o.done | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | func (o *serializedRuntimeObserver) run() { | |
| 186 | - | defer close(o.done) | |
| 187 | - | for { | |
| 188 | - | o.mu.Lock() | |
| 189 | - | for len(o.marks) == 0 && o.dropped == 0 && !o.closed { | |
| 190 | - | o.cond.Wait() | |
| 191 | - | } | |
| 192 | - | if len(o.marks) == 0 && o.dropped != 0 { | |
| 193 | - | o.marks = append(o.marks, runtimeObserverGapMark(o.dropped)) | |
| 194 | - | o.dropped = 0 | |
| 195 | - | } | |
| 196 | - | if len(o.marks) == 0 && o.closed { | |
| 197 | - | o.cond.Broadcast() | |
| 198 | - | o.mu.Unlock() | |
| 199 | - | return | |
| 200 | - | } | |
| 201 | - | mark := o.marks[0] | |
| 202 | - | o.marks = o.marks[1:] | |
| 203 | - | o.active = true | |
| 204 | - | o.mu.Unlock() | |
| 205 | - | ||
| 206 | - | o.observer.ObserveRuntime(mark) | |
| 207 | - | ||
| 208 | - | o.mu.Lock() | |
| 209 | - | o.active = false | |
| 210 | - | o.cond.Broadcast() | |
| 211 | - | o.mu.Unlock() | |
| 212 | - | } | |
| 213 | - | } | |
| 214 | - | ||
| 215 | 77 | // RuntimeMarkValid reports whether a mark has all correlation fields required | |
| 216 | 78 | // by the JSONL schema. Tick is checked separately by the timestamp owner. | |
| 217 | 79 | func RuntimeMarkValid(mark RuntimeMark) bool { | |
M internal/ports/runtime_observer_test.go +0 -66
| @@ -2,9 +2,7 @@ package ports | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "reflect" | |
| 5 | - | "sync" | |
| 6 | 5 | "testing" | |
| 7 | - | "time" | |
| 8 | 6 | ) | |
| 9 | 7 | ||
| 10 | 8 | func TestRuntimeObserverContract(t *testing.T) { | |
| @@ -44,70 +42,6 @@ func TestRuntimeObserverContract(t *testing.T) { | |||
| 44 | 42 | var _ RuntimeObserver = runtimeObserverFunc(func(RuntimeMark) {}) | |
| 45 | 43 | } | |
| 46 | 44 | ||
| 47 | - | func TestSerializedRuntimeObserverFlushesInOrderAndCloses(t *testing.T) { | |
| 48 | - | var mu sync.Mutex | |
| 49 | - | var got []RuntimeMark | |
| 50 | - | observer := NewSerializedRuntimeObserver(runtimeObserverFunc(func(mark RuntimeMark) { | |
| 51 | - | mu.Lock() | |
| 52 | - | got = append(got, mark) | |
| 53 | - | mu.Unlock() | |
| 54 | - | }), 2) | |
| 55 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeEmitStart, 1, true)) | |
| 56 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeEmitEnd, 1, true)) | |
| 57 | - | observer.Flush() | |
| 58 | - | observer.Close() | |
| 59 | - | observer.Close() | |
| 60 | - | ||
| 61 | - | mu.Lock() | |
| 62 | - | defer mu.Unlock() | |
| 63 | - | if len(got) != 2 || got[0].Kind != RuntimeEmitStart || got[1].Kind != RuntimeEmitEnd { | |
| 64 | - | t.Fatalf("serialized marks = %#v", got) | |
| 65 | - | } | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | func TestEnsureSerializedRuntimeObserverReusesExistingOwner(t *testing.T) { | |
| 69 | - | existing := NewSerializedRuntimeObserver(runtimeObserverFunc(func(RuntimeMark) {}), 1) | |
| 70 | - | reporter, owned := EnsureSerializedRuntimeObserver(existing, 1) | |
| 71 | - | if owned || reporter != existing { | |
| 72 | - | t.Fatalf("EnsureSerializedRuntimeObserver() = (%T, %t), want existing unowned reporter", reporter, owned) | |
| 73 | - | } | |
| 74 | - | existing.Close() | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | func TestSerializedRuntimeObserverReportsBoundedQueueLoss(t *testing.T) { | |
| 78 | - | entered := make(chan struct{}) | |
| 79 | - | release := make(chan struct{}) | |
| 80 | - | var mu sync.Mutex | |
| 81 | - | var got []RuntimeMark | |
| 82 | - | observer := NewSerializedRuntimeObserver(runtimeObserverFunc(func(mark RuntimeMark) { | |
| 83 | - | if mark.Kind == RuntimeEmitStart { | |
| 84 | - | close(entered) | |
| 85 | - | <-release | |
| 86 | - | } | |
| 87 | - | mu.Lock() | |
| 88 | - | got = append(got, mark) | |
| 89 | - | mu.Unlock() | |
| 90 | - | }), 2) | |
| 91 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeEmitStart, 1, true)) | |
| 92 | - | select { | |
| 93 | - | case <-entered: | |
| 94 | - | case <-time.After(time.Second): | |
| 95 | - | t.Fatal("observer did not start") | |
| 96 | - | } | |
| 97 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeComposeStart, 1, true)) | |
| 98 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeComposeEnd, 1, true)) | |
| 99 | - | observer.ObserveRuntime(NewRuntimeMark("client", RuntimeEmitEnd, 1, true)) // bounded loss | |
| 100 | - | close(release) | |
| 101 | - | observer.Flush() | |
| 102 | - | observer.Close() | |
| 103 | - | ||
| 104 | - | mu.Lock() | |
| 105 | - | defer mu.Unlock() | |
| 106 | - | if len(got) != 4 || got[0].Kind != RuntimeEmitStart || got[1].Kind != RuntimeComposeStart || got[2].Kind != RuntimeComposeEnd || got[3].Kind != RuntimeTransportDiagnostic || got[3].Bytes != 1 || got[3].Valid { | |
| 107 | - | t.Fatalf("bounded-loss marks = %#v", got) | |
| 108 | - | } | |
| 109 | - | } | |
| 110 | - | ||
| 111 | 45 | func TestP4RuntimeScreenMarkKindsValidateSchema1(t *testing.T) { | |
| 112 | 46 | if RuntimeMarkSchema != 1 { | |
| 113 | 47 | t.Fatalf("RuntimeMarkSchema = %d, want 1", RuntimeMarkSchema) | |
M internal/ports/terminal.go +1 -110
| @@ -2,12 +2,11 @@ package ports | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "io" | |
| 5 | - | "strings" | |
| 6 | 5 | ||
| 7 | 6 | "github.com/bnema/vev/internal/domain" | |
| 8 | 7 | ) | |
| 9 | 8 | ||
| 10 | - | // Terminal is the CLIENT-side controlling terminal. | |
| 9 | + | // Terminal is the client-side controlling terminal. | |
| 11 | 10 | type Terminal interface { | |
| 12 | 11 | EnterRaw() (restore func() error, err error) | |
| 13 | 12 | Geometry() (domain.Geometry, error) | |
| @@ -16,111 +15,3 @@ type Terminal interface { | |||
| 16 | 15 | Out() io.Writer | |
| 17 | 16 | Flush() error | |
| 18 | 17 | } | |
| 19 | - | ||
| 20 | - | // TerminalColorMode is the color output mode selected for one attachment. | |
| 21 | - | type TerminalColorMode uint8 | |
| 22 | - | ||
| 23 | - | const ( | |
| 24 | - | // TrueColor is the zero value so manually constructed attachments retain the | |
| 25 | - | // historical renderer behavior; live Hello paths always use the detector. | |
| 26 | - | TerminalColorTrueColor TerminalColorMode = iota | |
| 27 | - | TerminalColorIndexed256 | |
| 28 | - | ) | |
| 29 | - | ||
| 30 | - | // TerminalCapabilitySource records how confidently a terminal capability was | |
| 31 | - | // selected. Environment observations are advisory because nested multiplexers | |
| 32 | - | // can retain values from an outer terminal. | |
| 33 | - | type TerminalCapabilitySource uint8 | |
| 34 | - | ||
| 35 | - | const ( | |
| 36 | - | TerminalCapabilityUnknown TerminalCapabilitySource = iota | |
| 37 | - | TerminalCapabilityHeuristic | |
| 38 | - | TerminalCapabilityDeclared | |
| 39 | - | ) | |
| 40 | - | ||
| 41 | - | // TerminalApplication identifies a known terminal application when its | |
| 42 | - | // environment provides a trustworthy origin signal. It does not imply support | |
| 43 | - | // for any application-specific protocol on the current output path. | |
| 44 | - | type TerminalApplication uint8 | |
| 45 | - | ||
| 46 | - | const ( | |
| 47 | - | TerminalApplicationUnknown TerminalApplication = iota | |
| 48 | - | TerminalApplicationKitty | |
| 49 | - | ) | |
| 50 | - | ||
| 51 | - | // TerminalCapabilities describes the output features selected for one client | |
| 52 | - | // attachment. Pane processes must not derive their terminal environment from | |
| 53 | - | // these capabilities. | |
| 54 | - | type TerminalCapabilities struct { | |
| 55 | - | ColorMode TerminalColorMode | |
| 56 | - | ColorSource TerminalCapabilitySource | |
| 57 | - | Application TerminalApplication | |
| 58 | - | KittyGraphics bool | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // SupportsKittyGraphics reports whether this attachment's active outer-terminal | |
| 62 | - | // probe accepted the Kitty graphics protocol. Environment detection never sets it. | |
| 63 | - | func (c TerminalCapabilities) SupportsKittyGraphics() bool { | |
| 64 | - | return c.KittyGraphics | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // TrueColor reports whether this attachment can receive RGB ANSI output. | |
| 68 | - | func (c TerminalCapabilities) TrueColor() bool { | |
| 69 | - | return c.ColorMode == TerminalColorTrueColor | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | // DetectTerminalCapabilities derives conservative attachment capabilities from | |
| 73 | - | // a client environment. Missing evidence selects indexed color; it never proves | |
| 74 | - | // the physical terminal cannot display direct color. | |
| 75 | - | func DetectTerminalCapabilities(env []string) TerminalCapabilities { | |
| 76 | - | values := environmentValues(env) | |
| 77 | - | term := strings.ToLower(strings.TrimSpace(values["TERM"])) | |
| 78 | - | colorTerm := strings.ToLower(strings.TrimSpace(values["COLORTERM"])) | |
| 79 | - | caps := TerminalCapabilities{ColorMode: TerminalColorIndexed256} | |
| 80 | - | ||
| 81 | - | if values["KITTY_WINDOW_ID"] != "" || values["KITTY_PID"] != "" || values["KITTY_LISTEN_ON"] != "" { | |
| 82 | - | caps.Application = TerminalApplicationKitty | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | kittyIdentity := term == "xterm-kitty" && caps.Application == TerminalApplicationKitty | |
| 86 | - | switch colorTerm { | |
| 87 | - | case "truecolor", "24bit": | |
| 88 | - | caps.ColorMode = TerminalColorTrueColor | |
| 89 | - | caps.ColorSource = TerminalCapabilityDeclared | |
| 90 | - | } | |
| 91 | - | if term == "xterm-direct" || strings.HasSuffix(term, "-direct") { | |
| 92 | - | caps.ColorMode = TerminalColorTrueColor | |
| 93 | - | caps.ColorSource = TerminalCapabilityDeclared | |
| 94 | - | } | |
| 95 | - | if kittyIdentity { | |
| 96 | - | if caps.ColorSource == TerminalCapabilityUnknown { | |
| 97 | - | caps.ColorSource = TerminalCapabilityHeuristic | |
| 98 | - | } | |
| 99 | - | caps.ColorMode = TerminalColorTrueColor | |
| 100 | - | return caps | |
| 101 | - | } | |
| 102 | - | if caps.ColorSource == TerminalCapabilityDeclared { | |
| 103 | - | return caps | |
| 104 | - | } | |
| 105 | - | if strings.Contains(term, "256color") || term == "dumb" { | |
| 106 | - | caps.ColorSource = TerminalCapabilityDeclared | |
| 107 | - | } | |
| 108 | - | return caps | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | func environmentValues(env []string) map[string]string { | |
| 112 | - | values := make(map[string]string, len(env)) | |
| 113 | - | for _, entry := range env { | |
| 114 | - | key, value, ok := strings.Cut(entry, "=") | |
| 115 | - | if !ok { | |
| 116 | - | continue | |
| 117 | - | } | |
| 118 | - | values[key] = value | |
| 119 | - | } | |
| 120 | - | return values | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | |
| 124 | - | func DetectTrueColor(termEnv, colorTerm string) bool { | |
| 125 | - | return DetectTerminalCapabilities([]string{"TERM=" + termEnv, "COLORTERM=" + colorTerm}).TrueColor() | |
| 126 | - | } | |
D internal/ports/terminal_test.go +0 -68
| @@ -1,68 +0,0 @@ | |||
| 1 | - | package ports | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | "github.com/stretchr/testify/require" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | func TestDetectTerminalCapabilities(t *testing.T) { | |
| 10 | - | tests := []struct { | |
| 11 | - | name string | |
| 12 | - | env []string | |
| 13 | - | wantMode TerminalColorMode | |
| 14 | - | wantSource TerminalCapabilitySource | |
| 15 | - | wantApp TerminalApplication | |
| 16 | - | wantGraphics bool | |
| 17 | - | }{ | |
| 18 | - | { | |
| 19 | - | name: "declared truecolor", | |
| 20 | - | env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, | |
| 21 | - | wantMode: TerminalColorTrueColor, | |
| 22 | - | wantSource: TerminalCapabilityDeclared, | |
| 23 | - | }, | |
| 24 | - | { | |
| 25 | - | name: "kitty signals infer truecolor", | |
| 26 | - | env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, | |
| 27 | - | wantMode: TerminalColorTrueColor, | |
| 28 | - | wantSource: TerminalCapabilityHeuristic, | |
| 29 | - | wantApp: TerminalApplicationKitty, | |
| 30 | - | }, | |
| 31 | - | { | |
| 32 | - | name: "kitty environment with declared truecolor remains color-only", | |
| 33 | - | env: []string{"TERM=xterm-kitty", "COLORTERM=truecolor", "KITTY_WINDOW_ID=1"}, | |
| 34 | - | wantMode: TerminalColorTrueColor, | |
| 35 | - | wantSource: TerminalCapabilityDeclared, | |
| 36 | - | wantApp: TerminalApplicationKitty, | |
| 37 | - | }, | |
| 38 | - | { | |
| 39 | - | name: "256 color terminal remains a constrained attachment", | |
| 40 | - | env: []string{"TERM=xterm-256color"}, | |
| 41 | - | wantMode: TerminalColorIndexed256, | |
| 42 | - | wantSource: TerminalCapabilityDeclared, | |
| 43 | - | }, | |
| 44 | - | { | |
| 45 | - | name: "unknown terminal is conservatively indexed", | |
| 46 | - | env: []string{"TERM=unknown"}, | |
| 47 | - | wantMode: TerminalColorIndexed256, | |
| 48 | - | wantSource: TerminalCapabilityUnknown, | |
| 49 | - | }, | |
| 50 | - | { | |
| 51 | - | name: "kitty environment behind a multiplexer is not graphics evidence", | |
| 52 | - | env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, | |
| 53 | - | wantMode: TerminalColorIndexed256, | |
| 54 | - | wantSource: TerminalCapabilityDeclared, | |
| 55 | - | wantApp: TerminalApplicationKitty, | |
| 56 | - | }, | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | for _, tt := range tests { | |
| 60 | - | t.Run(tt.name, func(t *testing.T) { | |
| 61 | - | got := DetectTerminalCapabilities(tt.env) | |
| 62 | - | require.Equal(t, tt.wantMode, got.ColorMode) | |
| 63 | - | require.Equal(t, tt.wantSource, got.ColorSource) | |
| 64 | - | require.Equal(t, tt.wantApp, got.Application) | |
| 65 | - | require.Equal(t, tt.wantGraphics, got.SupportsKittyGraphics()) | |
| 66 | - | }) | |
| 67 | - | } | |
| 68 | - | } | |
A internal/protocol/catalogue/catalogue.go +263 -0
| @@ -0,0 +1,263 @@ | |||
| 1 | + | // Package catalogue owns the independently versioned remote discovery schema. | |
| 2 | + | package catalogue | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 7 | + | "math" | |
| 8 | + | "slices" | |
| 9 | + | "time" | |
| 10 | + | "unicode" | |
| 11 | + | "unicode/utf8" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/vev/internal/domain" | |
| 14 | + | "github.com/bnema/vev/internal/protocol" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | const ( | |
| 18 | + | // RemoteCatalogSchemaVersion is independent from the binary session protocol. | |
| 19 | + | RemoteCatalogSchemaVersion uint16 = 3 | |
| 20 | + | ||
| 21 | + | RemoteCatalogMaxHosts = 64 | |
| 22 | + | RemoteCatalogMaxSessions = 256 | |
| 23 | + | RemoteCatalogMaxTabsPerSess = 128 | |
| 24 | + | RemoteCatalogMaxNameBytes = 256 | |
| 25 | + | RemoteCatalogMaxDetailBytes = 512 | |
| 26 | + | RemoteCatalogMaxIDBytes = 128 | |
| 27 | + | RemoteCatalogMaxCatalogBytes = 1 << 20 | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // RemoteCatalogCacheEntry is one immutable exact-schema host snapshot. | |
| 31 | + | type RemoteCatalogCacheEntry struct { | |
| 32 | + | Host string | |
| 33 | + | FetchedAt time.Time | |
| 34 | + | Sessions []RemoteCatalogSession | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // RemoteCatalogTab is one bounded, untrusted tab presentation hint. | |
| 38 | + | type RemoteCatalogTab struct { | |
| 39 | + | ID string `json:"id"` | |
| 40 | + | Index uint16 `json:"index"` | |
| 41 | + | Name string `json:"name"` | |
| 42 | + | Detail string `json:"detail,omitempty"` | |
| 43 | + | Attention bool `json:"attention,omitempty"` | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // RemoteCatalogSessionState is the JSON catalogue lifecycle contract. | |
| 47 | + | type RemoteCatalogSessionState string | |
| 48 | + | ||
| 49 | + | const ( | |
| 50 | + | RemoteCatalogSessionUp RemoteCatalogSessionState = "up" | |
| 51 | + | RemoteCatalogSessionDown RemoteCatalogSessionState = "down" | |
| 52 | + | RemoteCatalogSessionBroken RemoteCatalogSessionState = "broken" | |
| 53 | + | ) | |
| 54 | + | ||
| 55 | + | func (s RemoteCatalogSessionState) Valid() bool { | |
| 56 | + | switch s { | |
| 57 | + | case RemoteCatalogSessionUp, RemoteCatalogSessionDown, RemoteCatalogSessionBroken: | |
| 58 | + | return true | |
| 59 | + | default: | |
| 60 | + | return false | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // RemoteCatalogSession is one exact-identity session in the remote catalogue. | |
| 65 | + | type RemoteCatalogSession struct { | |
| 66 | + | LifecycleID domain.SessionLifecycleID `json:"lifecycle_id"` | |
| 67 | + | Name string `json:"name"` | |
| 68 | + | State RemoteCatalogSessionState `json:"state"` | |
| 69 | + | Ephemeral bool `json:"ephemeral"` | |
| 70 | + | Tabs []RemoteCatalogTab `json:"tabs"` | |
| 71 | + | Attached bool `json:"attached"` | |
| 72 | + | LastUsedSeq uint64 `json:"last_used_seq,omitempty"` | |
| 73 | + | ActiveTabID string `json:"active_tab_id,omitempty"` | |
| 74 | + | Reason string `json:"reason,omitempty"` | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func CatalogTabs(session RemoteCatalogSession) []RemoteCatalogTab { return slices.Clone(session.Tabs) } | |
| 78 | + | func CatalogTabCount(session RemoteCatalogSession) int { return len(session.Tabs) } | |
| 79 | + | ||
| 80 | + | func SaturateUint16(n int) uint16 { | |
| 81 | + | if n <= 0 { | |
| 82 | + | return 0 | |
| 83 | + | } | |
| 84 | + | if n >= 1<<16 { | |
| 85 | + | return math.MaxUint16 | |
| 86 | + | } | |
| 87 | + | return uint16(n) | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // RemoteCatalog is the versioned JSON envelope returned by remote-catalog. | |
| 91 | + | type RemoteCatalog struct { | |
| 92 | + | ProtocolVersion uint16 `json:"protocol_version"` | |
| 93 | + | SchemaVersion uint16 `json:"schema_version"` | |
| 94 | + | Sessions []RemoteCatalogSession `json:"sessions"` | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // RemoteCatalogVersionMismatchError reports an incompatible protocol or schema version. | |
| 98 | + | type RemoteCatalogVersionMismatchError struct { | |
| 99 | + | Got uint16 | |
| 100 | + | Want uint16 | |
| 101 | + | Kind string | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (e *RemoteCatalogVersionMismatchError) Error() string { | |
| 105 | + | kind := e.Kind | |
| 106 | + | if kind == "" { | |
| 107 | + | kind = "protocol" | |
| 108 | + | } | |
| 109 | + | return fmt.Sprintf("remote catalog: %s version mismatch: got %d, want %d", kind, e.Got, e.Want) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | var ( | |
| 113 | + | ErrInvalidRemoteCatalog = errors.New("ports: invalid remote catalog") | |
| 114 | + | ErrRemoteCatalogTooLarge = errors.New("ports: remote catalog exceeds size limit") | |
| 115 | + | ErrRemoteCatalogUnknownState = errors.New("ports: remote catalog has unknown state") | |
| 116 | + | ErrRemoteCatalogInvalidReason = errors.New("ports: remote catalog has unknown reason") | |
| 117 | + | ) | |
| 118 | + | ||
| 119 | + | // ValidateRemoteCatalog applies the exact current schema and all bounds before use. | |
| 120 | + | func ValidateRemoteCatalog(c RemoteCatalog) error { | |
| 121 | + | if c.ProtocolVersion != protocol.Version { | |
| 122 | + | return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: protocol.Version, Kind: "protocol"} | |
| 123 | + | } | |
| 124 | + | if c.SchemaVersion != RemoteCatalogSchemaVersion { | |
| 125 | + | return &RemoteCatalogVersionMismatchError{Got: c.SchemaVersion, Want: RemoteCatalogSchemaVersion, Kind: "catalog"} | |
| 126 | + | } | |
| 127 | + | if len(c.Sessions) > RemoteCatalogMaxSessions { | |
| 128 | + | return fmt.Errorf("%w: too many sessions", ErrRemoteCatalogTooLarge) | |
| 129 | + | } | |
| 130 | + | seenLifecycles := make(map[domain.SessionLifecycleID]string, len(c.Sessions)) | |
| 131 | + | seenNames := make(map[string]struct{}, len(c.Sessions)) | |
| 132 | + | bytes := 0 | |
| 133 | + | for _, session := range c.Sessions { | |
| 134 | + | if !validText(session.Name) || !validText(string(session.State)) || !validText(session.Reason) { | |
| 135 | + | return fmt.Errorf("%w: invalid session text", ErrInvalidRemoteCatalog) | |
| 136 | + | } | |
| 137 | + | if len(session.Name) > RemoteCatalogMaxNameBytes || len(session.Reason) > RemoteCatalogMaxDetailBytes { | |
| 138 | + | return fmt.Errorf("%w: session text too long", ErrRemoteCatalogTooLarge) | |
| 139 | + | } | |
| 140 | + | if err := domain.ValidateSessionName(session.Name); err != nil { | |
| 141 | + | return fmt.Errorf("%w: session name: %v", ErrInvalidRemoteCatalog, err) | |
| 142 | + | } | |
| 143 | + | if !session.State.Valid() { | |
| 144 | + | return fmt.Errorf("%w: %q", ErrRemoteCatalogUnknownState, session.State) | |
| 145 | + | } | |
| 146 | + | if session.ActiveTabID != "" { | |
| 147 | + | if err := domain.ValidateTabStableID(domain.TabStableID(session.ActiveTabID)); err != nil { | |
| 148 | + | return fmt.Errorf("%w: invalid active tab ID", ErrInvalidRemoteCatalog) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | if session.Reason != "" && !validReason(session.Reason) { | |
| 152 | + | return fmt.Errorf("%w: %q", ErrRemoteCatalogInvalidReason, session.Reason) | |
| 153 | + | } | |
| 154 | + | if session.LifecycleID == (domain.SessionLifecycleID{}) { | |
| 155 | + | return fmt.Errorf("%w: zero lifecycle ID", ErrInvalidRemoteCatalog) | |
| 156 | + | } | |
| 157 | + | if _, exists := seenNames[session.Name]; exists { | |
| 158 | + | return fmt.Errorf("%w: duplicate session name %q", ErrInvalidRemoteCatalog, session.Name) | |
| 159 | + | } | |
| 160 | + | seenNames[session.Name] = struct{}{} | |
| 161 | + | if prior, exists := seenLifecycles[session.LifecycleID]; exists { | |
| 162 | + | return fmt.Errorf("%w: lifecycle ID reused by %q and %q", ErrInvalidRemoteCatalog, prior, session.Name) | |
| 163 | + | } | |
| 164 | + | seenLifecycles[session.LifecycleID] = session.Name | |
| 165 | + | if session.Tabs == nil { | |
| 166 | + | return fmt.Errorf("%w: missing tab list", ErrInvalidRemoteCatalog) | |
| 167 | + | } | |
| 168 | + | tabs := CatalogTabs(session) | |
| 169 | + | if len(tabs) == 0 { | |
| 170 | + | if session.ActiveTabID != "" { | |
| 171 | + | return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog) | |
| 172 | + | } | |
| 173 | + | } else { | |
| 174 | + | if len(tabs) > RemoteCatalogMaxTabsPerSess { | |
| 175 | + | return fmt.Errorf("%w: too many tabs", ErrRemoteCatalogTooLarge) | |
| 176 | + | } | |
| 177 | + | tabIDs := make(map[string]struct{}, len(tabs)) | |
| 178 | + | for i, tab := range tabs { | |
| 179 | + | if !validText(tab.ID) || !validText(tab.Name) || !validText(tab.Detail) { | |
| 180 | + | return fmt.Errorf("%w: invalid tab text", ErrInvalidRemoteCatalog) | |
| 181 | + | } | |
| 182 | + | if len(tab.ID) > RemoteCatalogMaxIDBytes || len(tab.Name) > RemoteCatalogMaxNameBytes || len(tab.Detail) > RemoteCatalogMaxDetailBytes { | |
| 183 | + | return fmt.Errorf("%w: tab text too long", ErrRemoteCatalogTooLarge) | |
| 184 | + | } | |
| 185 | + | if tab.Index != uint16(i) { | |
| 186 | + | return fmt.Errorf("%w: tab indexes are not ordered", ErrInvalidRemoteCatalog) | |
| 187 | + | } | |
| 188 | + | if session.State == RemoteCatalogSessionUp && tab.ID == "" { | |
| 189 | + | return fmt.Errorf("%w: up tab has zero ID", ErrInvalidRemoteCatalog) | |
| 190 | + | } | |
| 191 | + | if tab.ID != "" { | |
| 192 | + | if err := domain.ValidateTabStableID(domain.TabStableID(tab.ID)); err != nil { | |
| 193 | + | return fmt.Errorf("%w: invalid tab ID", ErrInvalidRemoteCatalog) | |
| 194 | + | } | |
| 195 | + | if _, duplicate := tabIDs[tab.ID]; duplicate { | |
| 196 | + | return fmt.Errorf("%w: duplicate tab ID", ErrInvalidRemoteCatalog) | |
| 197 | + | } | |
| 198 | + | tabIDs[tab.ID] = struct{}{} | |
| 199 | + | } | |
| 200 | + | bytes += len(tab.ID) + len(tab.Name) + len(tab.Detail) | |
| 201 | + | } | |
| 202 | + | if session.ActiveTabID != "" { | |
| 203 | + | if session.State != RemoteCatalogSessionUp { | |
| 204 | + | return fmt.Errorf("%w: down or broken session has an active tab", ErrInvalidRemoteCatalog) | |
| 205 | + | } | |
| 206 | + | if _, ok := tabIDs[session.ActiveTabID]; !ok { | |
| 207 | + | return fmt.Errorf("%w: active tab is absent", ErrInvalidRemoteCatalog) | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | bytes += len(session.Name) + len(session.State) + len(session.Reason) | |
| 212 | + | } | |
| 213 | + | if bytes > RemoteCatalogMaxCatalogBytes { | |
| 214 | + | return ErrRemoteCatalogTooLarge | |
| 215 | + | } | |
| 216 | + | return nil | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // ValidateRemoteCatalogCacheEntries validates the bounded exact-schema cache DTO. | |
| 220 | + | func ValidateRemoteCatalogCacheEntries(entries []RemoteCatalogCacheEntry) error { | |
| 221 | + | if len(entries) > RemoteCatalogMaxHosts { | |
| 222 | + | return fmt.Errorf("%w: too many hosts", ErrRemoteCatalogTooLarge) | |
| 223 | + | } | |
| 224 | + | seenHosts := make(map[string]struct{}, len(entries)) | |
| 225 | + | for _, entry := range entries { | |
| 226 | + | if err := domain.ValidateRemoteHostTarget(entry.Host); err != nil || len(entry.Host) > RemoteCatalogMaxNameBytes { | |
| 227 | + | return fmt.Errorf("%w: invalid host", ErrInvalidRemoteCatalog) | |
| 228 | + | } | |
| 229 | + | if entry.FetchedAt.IsZero() || entry.FetchedAt.UnixNano() <= 0 { | |
| 230 | + | return fmt.Errorf("%w: invalid fetched time", ErrInvalidRemoteCatalog) | |
| 231 | + | } | |
| 232 | + | if _, exists := seenHosts[entry.Host]; exists { | |
| 233 | + | return fmt.Errorf("%w: duplicate host", ErrInvalidRemoteCatalog) | |
| 234 | + | } | |
| 235 | + | seenHosts[entry.Host] = struct{}{} | |
| 236 | + | catalog := RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions} | |
| 237 | + | if err := ValidateRemoteCatalog(catalog); err != nil { | |
| 238 | + | return fmt.Errorf("%w: host %q: %v", ErrInvalidRemoteCatalog, entry.Host, err) | |
| 239 | + | } | |
| 240 | + | } | |
| 241 | + | return nil | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | func validText(value string) bool { | |
| 245 | + | if !utf8.ValidString(value) { | |
| 246 | + | return false | |
| 247 | + | } | |
| 248 | + | for _, r := range value { | |
| 249 | + | if unicode.IsControl(r) { | |
| 250 | + | return false | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | return true | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | func validReason(reason string) bool { | |
| 257 | + | switch reason { | |
| 258 | + | case "", "refreshing", "catalog_stale", "host_unreachable", "version_mismatch", "session_down", "session_broken", "identity_changed", "not_found", "timeout", "malformed": | |
| 259 | + | return true | |
| 260 | + | default: | |
| 261 | + | return false | |
| 262 | + | } | |
| 263 | + | } | |
R internal/ports/remote_state_test.go → internal/protocol/catalogue/catalogue_test.go +1 -1 renamed, 99% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package ports | |
| 1 | + | package catalogue | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
R internal/ports/mocks/transport_mock.go → internal/protocol/wire/mocks/transport_mock.go +23 -24 renamed, 93% similar
| @@ -2,12 +2,11 @@ | |||
| 2 | 2 | // github.com/vektra/mockery | |
| 3 | 3 | // template: testify | |
| 4 | 4 | ||
| 5 | - | package portsmocks | |
| 5 | + | package wiremocks | |
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | 9 | ||
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol/wire" | |
| 12 | 11 | mock "github.com/stretchr/testify/mock" | |
| 13 | 12 | ) | |
| @@ -579,27 +578,27 @@ func (_m *MockDialer) EXPECT() *MockDialer_Expecter { | |||
| 579 | 578 | } | |
| 580 | 579 | ||
| 581 | 580 | // Dial provides a mock function for the type MockDialer | |
| 582 | - | func (_mock *MockDialer) Dial(ctx context.Context) (ports.Transport, error) { | |
| 583 | - | ret := _mock.Called(ctx) | |
| 581 | + | func (_mock *MockDialer) Dial(context1 context.Context) (wire.Transport, error) { | |
| 582 | + | ret := _mock.Called(context1) | |
| 584 | 583 | ||
| 585 | 584 | if len(ret) == 0 { | |
| 586 | 585 | panic("no return value specified for Dial") | |
| 587 | 586 | } | |
| 588 | 587 | ||
| 589 | - | var r0 ports.Transport | |
| 588 | + | var r0 wire.Transport | |
| 590 | 589 | var r1 error | |
| 591 | - | if returnFunc, ok := ret.Get(0).(func(context.Context) (ports.Transport, error)); ok { | |
| 592 | - | return returnFunc(ctx) | |
| 590 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (wire.Transport, error)); ok { | |
| 591 | + | return returnFunc(context1) | |
| 593 | 592 | } | |
| 594 | - | if returnFunc, ok := ret.Get(0).(func(context.Context) ports.Transport); ok { | |
| 595 | - | r0 = returnFunc(ctx) | |
| 593 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) wire.Transport); ok { | |
| 594 | + | r0 = returnFunc(context1) | |
| 596 | 595 | } else { | |
| 597 | 596 | if ret.Get(0) != nil { | |
| 598 | - | r0 = ret.Get(0).(ports.Transport) | |
| 597 | + | r0 = ret.Get(0).(wire.Transport) | |
| 599 | 598 | } | |
| 600 | 599 | } | |
| 601 | 600 | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 602 | - | r1 = returnFunc(ctx) | |
| 601 | + | r1 = returnFunc(context1) | |
| 603 | 602 | } else { | |
| 604 | 603 | r1 = ret.Error(1) | |
| 605 | 604 | } | |
| @@ -612,12 +611,12 @@ type MockDialer_Dial_Call struct { | |||
| 612 | 611 | } | |
| 613 | 612 | ||
| 614 | 613 | // Dial is a helper method to define mock.On call | |
| 615 | - | // - ctx context.Context | |
| 616 | - | func (_e *MockDialer_Expecter) Dial(ctx any) *MockDialer_Dial_Call { | |
| 617 | - | return &MockDialer_Dial_Call{Call: _e.mock.On("Dial", ctx)} | |
| 614 | + | // - context1 context.Context | |
| 615 | + | func (_e *MockDialer_Expecter) Dial(context1 any) *MockDialer_Dial_Call { | |
| 616 | + | return &MockDialer_Dial_Call{Call: _e.mock.On("Dial", context1)} | |
| 618 | 617 | } | |
| 619 | 618 | ||
| 620 | - | func (_c *MockDialer_Dial_Call) Run(run func(ctx context.Context)) *MockDialer_Dial_Call { | |
| 619 | + | func (_c *MockDialer_Dial_Call) Run(run func(context1 context.Context)) *MockDialer_Dial_Call { | |
| 621 | 620 | _c.Call.Run(func(args mock.Arguments) { | |
| 622 | 621 | var arg0 context.Context | |
| 623 | 622 | if args[0] != nil { | |
| @@ -630,12 +629,12 @@ func (_c *MockDialer_Dial_Call) Run(run func(ctx context.Context)) *MockDialer_D | |||
| 630 | 629 | return _c | |
| 631 | 630 | } | |
| 632 | 631 | ||
| 633 | - | func (_c *MockDialer_Dial_Call) Return(transport ports.Transport, err error) *MockDialer_Dial_Call { | |
| 632 | + | func (_c *MockDialer_Dial_Call) Return(transport wire.Transport, err error) *MockDialer_Dial_Call { | |
| 634 | 633 | _c.Call.Return(transport, err) | |
| 635 | 634 | return _c | |
| 636 | 635 | } | |
| 637 | 636 | ||
| 638 | - | func (_c *MockDialer_Dial_Call) RunAndReturn(run func(ctx context.Context) (ports.Transport, error)) *MockDialer_Dial_Call { | |
| 637 | + | func (_c *MockDialer_Dial_Call) RunAndReturn(run func(context1 context.Context) (wire.Transport, error)) *MockDialer_Dial_Call { | |
| 639 | 638 | _c.Call.Return(run) | |
| 640 | 639 | return _c | |
| 641 | 640 | } | |
| @@ -668,23 +667,23 @@ func (_m *MockListener) EXPECT() *MockListener_Expecter { | |||
| 668 | 667 | } | |
| 669 | 668 | ||
| 670 | 669 | // Accept provides a mock function for the type MockListener | |
| 671 | - | func (_mock *MockListener) Accept() (ports.Transport, error) { | |
| 670 | + | func (_mock *MockListener) Accept() (wire.Transport, error) { | |
| 672 | 671 | ret := _mock.Called() | |
| 673 | 672 | ||
| 674 | 673 | if len(ret) == 0 { | |
| 675 | 674 | panic("no return value specified for Accept") | |
| 676 | 675 | } | |
| 677 | 676 | ||
| 678 | - | var r0 ports.Transport | |
| 677 | + | var r0 wire.Transport | |
| 679 | 678 | var r1 error | |
| 680 | - | if returnFunc, ok := ret.Get(0).(func() (ports.Transport, error)); ok { | |
| 679 | + | if returnFunc, ok := ret.Get(0).(func() (wire.Transport, error)); ok { | |
| 681 | 680 | return returnFunc() | |
| 682 | 681 | } | |
| 683 | - | if returnFunc, ok := ret.Get(0).(func() ports.Transport); ok { | |
| 682 | + | if returnFunc, ok := ret.Get(0).(func() wire.Transport); ok { | |
| 684 | 683 | r0 = returnFunc() | |
| 685 | 684 | } else { | |
| 686 | 685 | if ret.Get(0) != nil { | |
| 687 | - | r0 = ret.Get(0).(ports.Transport) | |
| 686 | + | r0 = ret.Get(0).(wire.Transport) | |
| 688 | 687 | } | |
| 689 | 688 | } | |
| 690 | 689 | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| @@ -712,12 +711,12 @@ func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call { | |||
| 712 | 711 | return _c | |
| 713 | 712 | } | |
| 714 | 713 | ||
| 715 | - | func (_c *MockListener_Accept_Call) Return(transport ports.Transport, err error) *MockListener_Accept_Call { | |
| 714 | + | func (_c *MockListener_Accept_Call) Return(transport wire.Transport, err error) *MockListener_Accept_Call { | |
| 716 | 715 | _c.Call.Return(transport, err) | |
| 717 | 716 | return _c | |
| 718 | 717 | } | |
| 719 | 718 | ||
| 720 | - | func (_c *MockListener_Accept_Call) RunAndReturn(run func() (ports.Transport, error)) *MockListener_Accept_Call { | |
| 719 | + | func (_c *MockListener_Accept_Call) RunAndReturn(run func() (wire.Transport, error)) *MockListener_Accept_Call { | |
| 721 | 720 | _c.Call.Return(run) | |
| 722 | 721 | return _c | |
| 723 | 722 | } | |
R internal/ports/transport.go → internal/protocol/wire/transport.go +11 -15 renamed, 66% similar
| @@ -1,22 +1,18 @@ | |||
| 1 | - | package ports | |
| 1 | + | package wire | |
| 2 | 2 | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | ||
| 6 | - | "github.com/bnema/vev/internal/protocol/wire" | |
| 7 | - | ) | |
| 3 | + | import "context" | |
| 8 | 4 | ||
| 9 | 5 | // Transport is a framed message channel over a single connection. Close must | |
| 10 | 6 | // be safe to call concurrently with Send and Recv, and must unblock active | |
| 11 | 7 | // Send and Recv calls. | |
| 12 | 8 | type Transport interface { | |
| 13 | - | Send(wire.Frame) error | |
| 14 | - | Recv() (wire.Frame, error) // blocking; io.EOF on close | |
| 9 | + | Send(Frame) error | |
| 10 | + | Recv() (Frame, error) // blocking; io.EOF on close | |
| 15 | 11 | Close() error | |
| 16 | 12 | } | |
| 17 | 13 | ||
| 18 | - | // DatagramTransport marks transports backed by a datagram link. It lets | |
| 19 | - | // usecases negotiate a conservative output window without importing adapters. | |
| 14 | + | // DatagramTransport marks transports backed by a datagram link. Typed session | |
| 15 | + | // adapters translate this carriage detail into semantic capabilities. | |
| 20 | 16 | type DatagramTransport interface { | |
| 21 | 17 | Transport | |
| 22 | 18 | DatagramTransport() | |
| @@ -26,21 +22,21 @@ type DatagramTransport interface { | |||
| 26 | 22 | // returns once the adapter owns the frame; Send retains its synchronous wire- | |
| 27 | 23 | // attempt contract. Daemon paint output may use this capability to pipeline. | |
| 28 | 24 | type AsyncTransport interface { | |
| 29 | - | SendAsync(wire.Frame) error | |
| 25 | + | SendAsync(Frame) error | |
| 30 | 26 | } | |
| 31 | 27 | ||
| 32 | 28 | // OwnedSynchronousTransport owns the complete bounded synchronous operation, | |
| 33 | 29 | // including adapter queues, pacing, write deadlines, and close cancellation. | |
| 34 | 30 | type OwnedSynchronousTransport interface { | |
| 35 | - | SendSynchronous(wire.Frame) error | |
| 31 | + | SendSynchronous(Frame) error | |
| 36 | 32 | } | |
| 37 | 33 | ||
| 38 | - | // Dialer establishes outbound Transport connections. | |
| 34 | + | // Dialer establishes outbound raw carriage connections. | |
| 39 | 35 | type Dialer interface { | |
| 40 | - | Dial(ctx context.Context) (Transport, error) | |
| 36 | + | Dial(context.Context) (Transport, error) | |
| 41 | 37 | } | |
| 42 | 38 | ||
| 43 | - | // Listener accepts incoming Transport connections. | |
| 39 | + | // Listener accepts incoming raw carriage connections. | |
| 44 | 40 | type Listener interface { | |
| 45 | 41 | Accept() (Transport, error) | |
| 46 | 42 | Close() error | |
M internal/usecase/client/client.go +2 -1
| @@ -22,6 +22,7 @@ import ( | |||
| 22 | 22 | renderer "github.com/bnema/vev-vt" | |
| 23 | 23 | "github.com/bnema/vev-vt/protocol/terminalquery" | |
| 24 | 24 | "github.com/bnema/vev/internal/domain" | |
| 25 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 25 | 26 | "github.com/bnema/vev/internal/ports" | |
| 26 | 27 | "github.com/bnema/vev/internal/protocol" | |
| 27 | 28 | "github.com/bnema/vev/internal/usecase/theme" | |
| @@ -1121,7 +1122,7 @@ func paletteSlot(slot int) (uint8, bool) { | |||
| 1121 | 1122 | ||
| 1122 | 1123 | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | |
| 1123 | 1124 | func DetectTrueColor(termEnv, colorTerm string) bool { | |
| 1124 | - | return ports.DetectTrueColor(termEnv, colorTerm) | |
| 1125 | + | return terminalcap.DetectTrueColor(termEnv, colorTerm) | |
| 1125 | 1126 | } | |
| 1126 | 1127 | ||
| 1127 | 1128 | func requestedOutputWindow(connection ports.ClientConnection) uint8 { | |
M internal/usecase/client/client_test.go +27 -27
| @@ -123,7 +123,7 @@ type recvItem struct { | |||
| 123 | 123 | wait <-chan struct{} | |
| 124 | 124 | } | |
| 125 | 125 | ||
| 126 | - | type markedDatagramTransport struct{ ports.Transport } | |
| 126 | + | type markedDatagramTransport struct{ wire.Transport } | |
| 127 | 127 | ||
| 128 | 128 | func (markedDatagramTransport) DatagramTransport() {} | |
| 129 | 129 | ||
| @@ -240,7 +240,7 @@ func isType(typ wire.MsgType) any { | |||
| 240 | 240 | } | |
| 241 | 241 | ||
| 242 | 242 | // transportDialer adapts one already-open test transport to the Runner API. | |
| 243 | - | type transportDialer struct{ transport ports.Transport } | |
| 243 | + | type transportDialer struct{ transport wire.Transport } | |
| 244 | 244 | ||
| 245 | 245 | func (d transportDialer) Dial(context.Context) (ports.ClientConnection, error) { | |
| 246 | 246 | return &rawClientConnection{raw: d.transport}, nil | |
| @@ -262,7 +262,7 @@ func testDependencies(dialer ports.ClientDialer, terminal ports.Terminal, clock | |||
| 262 | 262 | } | |
| 263 | 263 | } | |
| 264 | 264 | ||
| 265 | - | func attachTestDependencies(transport ports.Transport, terminal ports.Terminal, clock ports.Clock) client.Dependencies { | |
| 265 | + | func attachTestDependencies(transport wire.Transport, terminal ports.Terminal, clock ports.Clock) client.Dependencies { | |
| 266 | 266 | return testDependencies(transportDialer{transport: transport}, terminal, clock, nil, nil) | |
| 267 | 267 | } | |
| 268 | 268 | ||
| @@ -287,7 +287,7 @@ func TestRunBoundsBlockedDial(t *testing.T) { | |||
| 287 | 287 | clk, createdTimers := newHandshakeClock(t, 1) | |
| 288 | 288 | started := make(chan struct{}) | |
| 289 | 289 | dialer := newMockClientDialer(t) | |
| 290 | - | dialer.EXPECT().Dial(mock.Anything).RunAndReturn(func(ctx context.Context) (ports.Transport, error) { | |
| 290 | + | dialer.EXPECT().Dial(mock.Anything).RunAndReturn(func(ctx context.Context) (wire.Transport, error) { | |
| 291 | 291 | close(started) | |
| 292 | 292 | <-ctx.Done() | |
| 293 | 293 | return nil, ctx.Err() | |
| @@ -503,7 +503,7 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) { | |||
| 503 | 503 | {f: frameOf(wire.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))}, | |
| 504 | 504 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 505 | 505 | }} | |
| 506 | - | dialer := &sequenceDialer{trs: []ports.Transport{first}} | |
| 506 | + | dialer := &sequenceDialer{trs: []wire.Transport{first}} | |
| 507 | 507 | ||
| 508 | 508 | err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{ | |
| 509 | 509 | Intent: protocol.IntentAttach, SessionName: "first", | |
| @@ -570,7 +570,7 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) { | |||
| 570 | 570 | {f: welcome("stopped", targetLifecycle)}, | |
| 571 | 571 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 572 | 572 | }} | |
| 573 | - | dialer := &sequenceDialer{trs: []ports.Transport{first, second}} | |
| 573 | + | dialer := &sequenceDialer{trs: []wire.Transport{first, second}} | |
| 574 | 574 | ||
| 575 | 575 | err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{ | |
| 576 | 576 | Intent: protocol.IntentAttach, SessionName: "source", | |
| @@ -613,8 +613,8 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin | |||
| 613 | 613 | {f: welcome("local", localLifecycle)}, | |
| 614 | 614 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 615 | 615 | }} | |
| 616 | - | localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}} | |
| 617 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}} | |
| 616 | + | localDialer := &sequenceDialer{trs: []wire.Transport{local1, local2}} | |
| 617 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{remote}} | |
| 618 | 618 | deps := testDependencies(localDialer, term, realClock{}, nil, nil) | |
| 619 | 619 | deps.AttachHandoff = func(protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) { | |
| 620 | 620 | target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"} | |
| @@ -741,8 +741,8 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) { | |||
| 741 | 741 | resizeOnce.Do(func() { term.setSize(domain.Size{Cols: 100, Rows: 40}) }) | |
| 742 | 742 | } | |
| 743 | 743 | } | |
| 744 | - | localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}} | |
| 745 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}} | |
| 744 | + | localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker}} | |
| 745 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: remote}}} | |
| 746 | 746 | deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer}) | |
| 747 | 747 | ||
| 748 | 748 | err := runTestClient(context.Background(), deps, client.AttachRequest{ | |
| @@ -822,8 +822,8 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) { | |||
| 822 | 822 | {f: hybridWelcomeFrame("target", targetLifecycle)}, | |
| 823 | 823 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 824 | 824 | }} | |
| 825 | - | localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}} | |
| 826 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{ | |
| 825 | + | localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker}} | |
| 826 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{ | |
| 827 | 827 | markedDatagramTransport{Transport: sourceRemote}, targetRemote, | |
| 828 | 828 | }} | |
| 829 | 829 | deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer}) | |
| @@ -888,8 +888,8 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T) | |||
| 888 | 888 | protocol.ParkedRoutePrepare: prepareSent, | |
| 889 | 889 | }) | |
| 890 | 890 | ||
| 891 | - | localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}} | |
| 892 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}} | |
| 891 | + | localDialer := &sequenceDialer{trs: []wire.Transport{localInitial}} | |
| 892 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: remote}}} | |
| 893 | 893 | deps := hybridPickerDependencies(localDialer, term, clock, map[string]ports.ClientDialer{"remote": remoteDialer}) | |
| 894 | 894 | ||
| 895 | 895 | ctx, cancel := context.WithCancel(context.Background()) | |
| @@ -965,8 +965,8 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) { | |||
| 965 | 965 | {f: hybridWelcomeFrame("local", localLifecycle)}, | |
| 966 | 966 | {f: navigationDirectiveFrame(protocol.NavigationBack)}, | |
| 967 | 967 | }} | |
| 968 | - | localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}} | |
| 969 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}} | |
| 968 | + | localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker}} | |
| 969 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: remote}}} | |
| 970 | 970 | deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer}) | |
| 971 | 971 | ||
| 972 | 972 | require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{ | |
| @@ -1020,9 +1020,9 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) { | |||
| 1020 | 1020 | {f: hybridWelcomeFrame("target", targetLifecycle)}, | |
| 1021 | 1021 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 1022 | 1022 | }} | |
| 1023 | - | localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}} | |
| 1024 | - | sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}} | |
| 1025 | - | targetDialer := &sequenceDialer{trs: []ports.Transport{targetRemote}} | |
| 1023 | + | localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker}} | |
| 1024 | + | sourceDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: sourceRemote}}} | |
| 1025 | + | targetDialer := &sequenceDialer{trs: []wire.Transport{targetRemote}} | |
| 1026 | 1026 | deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{ | |
| 1027 | 1027 | sourceTarget.Endpoint: sourceDialer, | |
| 1028 | 1028 | targetTarget.Endpoint: targetDialer, | |
| @@ -1105,8 +1105,8 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T) | |||
| 1105 | 1105 | {f: welcome("remote-new", remoteNewLifecycle, 22)}, | |
| 1106 | 1106 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 1107 | 1107 | }} | |
| 1108 | - | localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}} | |
| 1109 | - | remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}} | |
| 1108 | + | localDialer := &sequenceDialer{trs: []wire.Transport{local1, local2, local3}} | |
| 1109 | + | remoteDialer := &sequenceDialer{trs: []wire.Transport{remote1, remote2, remote3}} | |
| 1110 | 1110 | ||
| 1111 | 1111 | deps := testDependencies(localDialer, term, realClock{}, nil, nil) | |
| 1112 | 1112 | deps.AttachHandoff = func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) { | |
| @@ -1586,7 +1586,7 @@ func TestRunPhaseASingleAttempt(t *testing.T) { | |||
| 1586 | 1586 | } | |
| 1587 | 1587 | ||
| 1588 | 1588 | type sequenceDialer struct { | |
| 1589 | - | trs []ports.Transport | |
| 1589 | + | trs []wire.Transport | |
| 1590 | 1590 | errs []error | |
| 1591 | 1591 | calls atomic.Int32 | |
| 1592 | 1592 | } | |
| @@ -1709,7 +1709,7 @@ func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) { | |||
| 1709 | 1709 | tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}} | |
| 1710 | 1710 | tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {err: io.EOF}}} | |
| 1711 | 1711 | tr3 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(33)}, {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}} | |
| 1712 | - | d := &sequenceDialer{trs: []ports.Transport{tr1, tr2, tr3}} | |
| 1712 | + | d := &sequenceDialer{trs: []wire.Transport{tr1, tr2, tr3}} | |
| 1713 | 1713 | clock := &reconnectTestClock{} | |
| 1714 | 1714 | result := make(chan error, 1) | |
| 1715 | 1715 | go func() { | |
| @@ -1757,7 +1757,7 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) { | |||
| 1757 | 1757 | {f: welcome(22)}, | |
| 1758 | 1758 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 1759 | 1759 | }} | |
| 1760 | - | dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}} | |
| 1760 | + | dialer := &sequenceDialer{trs: []wire.Transport{tr1, tr2}} | |
| 1761 | 1761 | clock := &reconnectTestClock{} | |
| 1762 | 1762 | result := make(chan error, 1) | |
| 1763 | 1763 | go func() { | |
| @@ -1800,7 +1800,7 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin | |||
| 1800 | 1800 | {f: welcome("new", newLifecycle, 22)}, | |
| 1801 | 1801 | {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}, | |
| 1802 | 1802 | }} | |
| 1803 | - | dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}} | |
| 1803 | + | dialer := &sequenceDialer{trs: []wire.Transport{tr1, tr2}} | |
| 1804 | 1804 | clock := &reconnectTestClock{} | |
| 1805 | 1805 | result := make(chan error, 1) | |
| 1806 | 1806 | go func() { | |
| @@ -1986,7 +1986,7 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) { | |||
| 1986 | 1986 | ||
| 1987 | 1987 | tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}} | |
| 1988 | 1988 | tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}} | |
| 1989 | - | d := &sequenceDialer{trs: []ports.Transport{tr1, tr2}} | |
| 1989 | + | d := &sequenceDialer{trs: []wire.Transport{tr1, tr2}} | |
| 1990 | 1990 | ||
| 1991 | 1991 | deps := testDependencies(d, term, realClock{}, nil, nil) | |
| 1992 | 1992 | learner := portsmocks.NewMockRemoteHostLearner(t) | |
| @@ -2269,7 +2269,7 @@ func TestRunDoesNotRetryTerminalDetachedError(t *testing.T) { | |||
| 2269 | 2269 | term := newRunTerminal() | |
| 2270 | 2270 | defer term.in.unblock() | |
| 2271 | 2271 | tr := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonSessionKilled}))}}} | |
| 2272 | - | d := &sequenceDialer{trs: []ports.Transport{tr}} | |
| 2272 | + | d := &sequenceDialer{trs: []wire.Transport{tr}} | |
| 2273 | 2273 | ||
| 2274 | 2274 | err := runTestClient(context.Background(), testDependencies(d, term, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false}) | |
| 2275 | 2275 | require.Error(t, err) | |
M internal/usecase/client/clipboard_intercept_test.go +1 -1
| @@ -155,7 +155,7 @@ func (t *clipboardToastLifecycleTransport) Send(f wire.Frame) error { | |||
| 155 | 155 | func (t *clipboardToastLifecycleTransport) Recv() (wire.Frame, error) { return <-t.recv, nil } | |
| 156 | 156 | func (*clipboardToastLifecycleTransport) Close() error { return nil } | |
| 157 | 157 | ||
| 158 | - | type clipboardToastLifecycleDialer struct{ transport ports.Transport } | |
| 158 | + | type clipboardToastLifecycleDialer struct{ transport wire.Transport } | |
| 159 | 159 | ||
| 160 | 160 | func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.ClientConnection, error) { | |
| 161 | 161 | return &rawClientConnection{raw: d.transport}, nil | |
M internal/usecase/client/reconnect_toast_test.go +8 -8
| @@ -148,7 +148,7 @@ type reconnectToastRecv struct { | |||
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | 150 | type reconnectToastSequenceDialer struct { | |
| 151 | - | transports []ports.Transport | |
| 151 | + | transports []wire.Transport | |
| 152 | 152 | calls int | |
| 153 | 153 | } | |
| 154 | 154 | ||
| @@ -1375,7 +1375,7 @@ func TestReconnectRetryFailuresPreserveAttachError(t *testing.T) { | |||
| 1375 | 1375 | reconnectToastRecv{frame: reconnectToastWelcome(44)}, | |
| 1376 | 1376 | reconnectToastRecv{err: attachErr}, | |
| 1377 | 1377 | ) | |
| 1378 | - | dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}} | |
| 1378 | + | dialer := &reconnectToastSequenceDialer{transports: []wire.Transport{tr}} | |
| 1379 | 1379 | ||
| 1380 | 1380 | err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}) | |
| 1381 | 1381 | require.ErrorIs(t, err, attachErr) | |
| @@ -1462,7 +1462,7 @@ func TestRemoteReconnectToastFailedDrawDoesNotBlankBounds(t *testing.T) { | |||
| 1462 | 1462 | {frame: reconnectToastWelcome(44)}, | |
| 1463 | 1463 | {err: io.EOF}, | |
| 1464 | 1464 | }} | |
| 1465 | - | dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}} | |
| 1465 | + | dialer := &reconnectToastSequenceDialer{transports: []wire.Transport{tr}} | |
| 1466 | 1466 | ||
| 1467 | 1467 | err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}) | |
| 1468 | 1468 | require.ErrorContains(t, err, "reconnect toast write failed") | |
| @@ -1484,12 +1484,12 @@ func (w *reconnectStatusClearFailWriter) Write(p []byte) (int, error) { | |||
| 1484 | 1484 | func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) { | |
| 1485 | 1485 | tests := []struct { | |
| 1486 | 1486 | name string | |
| 1487 | - | configure func(*mockClientDialer, ports.Transport) | |
| 1487 | + | configure func(*mockClientDialer, wire.Transport) | |
| 1488 | 1488 | sleep func(context.CancelFunc) bool | |
| 1489 | 1489 | }{ | |
| 1490 | 1490 | { | |
| 1491 | 1491 | name: "after attach failure", | |
| 1492 | - | configure: func(dialer *mockClientDialer, tr ports.Transport) { | |
| 1492 | + | configure: func(dialer *mockClientDialer, tr wire.Transport) { | |
| 1493 | 1493 | dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once() | |
| 1494 | 1494 | }, | |
| 1495 | 1495 | sleep: func(cancel context.CancelFunc) bool { | |
| @@ -1499,7 +1499,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) | |||
| 1499 | 1499 | }, | |
| 1500 | 1500 | { | |
| 1501 | 1501 | name: "after reconnect dial failure", | |
| 1502 | - | configure: func(dialer *mockClientDialer, tr ports.Transport) { | |
| 1502 | + | configure: func(dialer *mockClientDialer, tr wire.Transport) { | |
| 1503 | 1503 | dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once() | |
| 1504 | 1504 | dialer.EXPECT().Dial(mock.Anything).Return(nil, errors.New("redial failed")).Once() | |
| 1505 | 1505 | }, | |
| @@ -1575,7 +1575,7 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) { | |||
| 1575 | 1575 | {frame: reconnectToastWelcome(55)}, | |
| 1576 | 1576 | {frame: reconnectToastDetach(protocol.ReasonDetach)}, | |
| 1577 | 1577 | }} | |
| 1578 | - | dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}} | |
| 1578 | + | dialer := &reconnectToastSequenceDialer{transports: []wire.Transport{tr1, tr2}} | |
| 1579 | 1579 | ||
| 1580 | 1580 | err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}) | |
| 1581 | 1581 | require.NoError(t, err) | |
| @@ -1619,7 +1619,7 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) { | |||
| 1619 | 1619 | {frame: reconnectToastWelcomeNamed("0", 55)}, | |
| 1620 | 1620 | {frame: reconnectToastDetach(protocol.ReasonDetach)}, | |
| 1621 | 1621 | }} | |
| 1622 | - | dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}} | |
| 1622 | + | dialer := &reconnectToastSequenceDialer{transports: []wire.Transport{tr1, tr2}} | |
| 1623 | 1623 | ||
| 1624 | 1624 | err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: true}) | |
| 1625 | 1625 | require.NoError(t, err) | |
M internal/usecase/client/transport_observability_test.go +3 -2
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/stretchr/testify/require" | |
| 13 | 13 | ||
| 14 | + | "github.com/bnema/vev/internal/adapters/observability" | |
| 14 | 15 | "github.com/bnema/vev/internal/domain" | |
| 15 | 16 | "github.com/bnema/vev/internal/ports" | |
| 16 | 17 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| @@ -43,7 +44,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | |||
| 43 | 44 | term.EXPECT().ResizeEvents().Return(resizeCh).Maybe() | |
| 44 | 45 | ||
| 45 | 46 | observer := &blockingClientRuntimeObserver{entered: make(chan struct{}), release: make(chan struct{})} | |
| 46 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 1) | |
| 47 | + | reporter := observability.NewSerialized(observer, 1) | |
| 47 | 48 | defer reporter.Close() | |
| 48 | 49 | acked := make(chan struct{}) | |
| 49 | 50 | transport := newMockClientConnection(t) | |
| @@ -85,7 +86,7 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) { | |||
| 85 | 86 | defer in.unblock() | |
| 86 | 87 | ||
| 87 | 88 | observer := &clientRuntimeObserver{} | |
| 88 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 89 | + | reporter := observability.NewSerialized(observer, 64) | |
| 89 | 90 | defer reporter.Close() | |
| 90 | 91 | transport := newMockClientConnection(t) | |
| 91 | 92 | transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once() | |
M internal/usecase/client/typed_connection_external_test.go +8 -8
| @@ -7,9 +7,9 @@ import ( | |||
| 7 | 7 | "github.com/stretchr/testify/mock" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/ports" | |
| 10 | - | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol" | |
| 12 | 11 | "github.com/bnema/vev/internal/protocol/wire" | |
| 12 | + | wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | 15 | type mockTestingT interface { | |
| @@ -17,10 +17,10 @@ type mockTestingT interface { | |||
| 17 | 17 | Cleanup(func()) | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | type mockClientConnection struct{ *portsmocks.MockTransport } | |
| 20 | + | type mockClientConnection struct{ *wiremocks.MockTransport } | |
| 21 | 21 | ||
| 22 | 22 | func newMockClientConnection(t mockTestingT) *mockClientConnection { | |
| 23 | - | return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)} | |
| 23 | + | return &mockClientConnection{MockTransport: wiremocks.NewMockTransport(t)} | |
| 24 | 24 | } | |
| 25 | 25 | func (c *mockClientConnection) SendClient(m protocol.ClientMessage) error { | |
| 26 | 26 | f, e := externalClientFrame(m) | |
| @@ -42,7 +42,7 @@ func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities { | |||
| 42 | 42 | func (c *mockClientConnection) LinkState() ports.LinkState { return ports.LinkStateConnected } | |
| 43 | 43 | func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent { return nil } | |
| 44 | 44 | ||
| 45 | - | type rawClientConnection struct{ raw ports.Transport } | |
| 45 | + | type rawClientConnection struct{ raw wire.Transport } | |
| 46 | 46 | ||
| 47 | 47 | func (c *rawClientConnection) SendClient(m protocol.ClientMessage) error { | |
| 48 | 48 | f, e := externalClientFrame(m) | |
| @@ -75,10 +75,10 @@ func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { | |||
| 75 | 75 | } | |
| 76 | 76 | func (c *rawClientConnection) Close() error { return c.raw.Close() } | |
| 77 | 77 | ||
| 78 | - | type mockClientDialer struct{ *portsmocks.MockDialer } | |
| 78 | + | type mockClientDialer struct{ *wiremocks.MockDialer } | |
| 79 | 79 | ||
| 80 | 80 | func newMockClientDialer(t mockTestingT) *mockClientDialer { | |
| 81 | - | return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)} | |
| 81 | + | return &mockClientDialer{MockDialer: wiremocks.NewMockDialer(t)} | |
| 82 | 82 | } | |
| 83 | 83 | func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) { | |
| 84 | 84 | raw, e := d.MockDialer.Dial(ctx) | |
| @@ -88,8 +88,8 @@ func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, er | |||
| 88 | 88 | return &rawClientConnection{raw: raw}, nil | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | - | func externalCapabilities(raw ports.Transport) protocol.ConnectionCapabilities { | |
| 92 | - | _, dgram := raw.(ports.DatagramTransport) | |
| 91 | + | func externalCapabilities(raw wire.Transport) protocol.ConnectionCapabilities { | |
| 92 | + | _, dgram := raw.(wire.DatagramTransport) | |
| 93 | 93 | _, link := raw.(ports.LinkStateReporter) | |
| 94 | 94 | window := uint8(protocol.MaxOutputWindow) | |
| 95 | 95 | if dgram { | |
M internal/usecase/client/typed_connection_test.go +10 -9
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 16 | "github.com/bnema/vev/internal/protocol" | |
| 17 | 17 | "github.com/bnema/vev/internal/protocol/wire" | |
| 18 | + | wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks" | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| 20 | 21 | type mockTestingT interface { | |
| @@ -22,10 +23,10 @@ type mockTestingT interface { | |||
| 22 | 23 | Cleanup(func()) | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 25 | - | type mockClientConnection struct{ *portsmocks.MockTransport } | |
| 26 | + | type mockClientConnection struct{ *wiremocks.MockTransport } | |
| 26 | 27 | ||
| 27 | 28 | func newMockClientConnection(t mockTestingT) *mockClientConnection { | |
| 28 | - | return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)} | |
| 29 | + | return &mockClientConnection{MockTransport: wiremocks.NewMockTransport(t)} | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | 32 | func (c *mockClientConnection) SendClient(message protocol.ClientMessage) error { | |
| @@ -52,7 +53,7 @@ func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent { | |||
| 52 | 53 | return testClientLinkEvents(c.MockTransport) | |
| 53 | 54 | } | |
| 54 | 55 | ||
| 55 | - | type rawClientConnection struct{ raw ports.Transport } | |
| 56 | + | type rawClientConnection struct{ raw wire.Transport } | |
| 56 | 57 | ||
| 57 | 58 | func (c *rawClientConnection) SendClient(message protocol.ClientMessage) error { | |
| 58 | 59 | frame, err := testClientFrame(message) | |
| @@ -75,10 +76,10 @@ func (c *rawClientConnection) LinkState() ports.LinkState { return testC | |||
| 75 | 76 | func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { return testClientLinkEvents(c.raw) } | |
| 76 | 77 | func (c *rawClientConnection) Close() error { return c.raw.Close() } | |
| 77 | 78 | ||
| 78 | - | type mockClientDialer struct{ *portsmocks.MockDialer } | |
| 79 | + | type mockClientDialer struct{ *wiremocks.MockDialer } | |
| 79 | 80 | ||
| 80 | 81 | func newMockClientDialer(t mockTestingT) *mockClientDialer { | |
| 81 | - | return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)} | |
| 82 | + | return &mockClientDialer{MockDialer: wiremocks.NewMockDialer(t)} | |
| 82 | 83 | } | |
| 83 | 84 | func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) { | |
| 84 | 85 | raw, err := d.MockDialer.Dial(ctx) | |
| @@ -179,8 +180,8 @@ func testServerMessage(frame wire.Frame) (protocol.ServerMessage, error) { | |||
| 179 | 180 | } | |
| 180 | 181 | } | |
| 181 | 182 | ||
| 182 | - | func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities { | |
| 183 | - | _, dgram := raw.(ports.DatagramTransport) | |
| 183 | + | func testClientCapabilities(raw wire.Transport) protocol.ConnectionCapabilities { | |
| 184 | + | _, dgram := raw.(wire.DatagramTransport) | |
| 184 | 185 | _, link := raw.(ports.LinkStateReporter) | |
| 185 | 186 | window := uint8(protocol.MaxOutputWindow) | |
| 186 | 187 | if dgram { | |
| @@ -188,13 +189,13 @@ func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities | |||
| 188 | 189 | } | |
| 189 | 190 | return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, LinkState: link} | |
| 190 | 191 | } | |
| 191 | - | func testClientLinkState(raw ports.Transport) ports.LinkState { | |
| 192 | + | func testClientLinkState(raw wire.Transport) ports.LinkState { | |
| 192 | 193 | if r, ok := raw.(ports.LinkStateReporter); ok { | |
| 193 | 194 | return r.LinkState() | |
| 194 | 195 | } | |
| 195 | 196 | return ports.LinkStateConnected | |
| 196 | 197 | } | |
| 197 | - | func testClientLinkEvents(raw ports.Transport) <-chan ports.LinkEvent { | |
| 198 | + | func testClientLinkEvents(raw wire.Transport) <-chan ports.LinkEvent { | |
| 198 | 199 | if r, ok := raw.(ports.LinkStateReporter); ok { | |
| 199 | 200 | return r.LinkEvents() | |
| 200 | 201 | } | |
M internal/usecase/daemon/attachment_output.go +2 -2
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | renderer "github.com/bnema/vev-vt/ansi" | |
| 10 | 10 | "github.com/bnema/vev/internal/domain" | |
| 11 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 12 | 12 | "github.com/bnema/vev/internal/protocol" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -95,7 +95,7 @@ func newOutputStateStream(windowSize ...uint8) *attachmentOutput { | |||
| 95 | 95 | return newOutputStateStreamForProfile(renderer.ColorProfileTrueColor, windowSize...) | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | - | func newOutputStateStreamForCapabilities(capabilities ports.TerminalCapabilities, windowSize ...uint8) *attachmentOutput { | |
| 98 | + | func newOutputStateStreamForCapabilities(capabilities terminalcap.Capabilities, windowSize ...uint8) *attachmentOutput { | |
| 99 | 99 | profile := renderer.ColorProfileANSI256 | |
| 100 | 100 | if capabilities.TrueColor() { | |
| 101 | 101 | profile = renderer.ColorProfileTrueColor | |
M internal/usecase/daemon/attachment_output_test.go +5 -5
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | renderer "github.com/bnema/vev-vt/ansi" | |
| 10 | 10 | "github.com/bnema/vev-vt/graphics" | |
| 11 | 11 | "github.com/bnema/vev/internal/domain" | |
| 12 | - | "github.com/bnema/vev/internal/ports" | |
| 12 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 13 | 13 | "github.com/bnema/vev/internal/protocol" | |
| 14 | 14 | "github.com/bnema/vev/internal/protocol/wire" | |
| 15 | 15 | "github.com/bnema/vev/internal/usecase/layout" | |
| @@ -56,17 +56,17 @@ func TestAttachmentOutputUsesTerminalColorProfile(t *testing.T) { | |||
| 56 | 56 | ||
| 57 | 57 | cases := []struct { | |
| 58 | 58 | name string | |
| 59 | - | capabilities ports.TerminalCapabilities | |
| 59 | + | capabilities terminalcap.Capabilities | |
| 60 | 60 | want string | |
| 61 | 61 | }{ | |
| 62 | 62 | { | |
| 63 | 63 | name: "truecolor", | |
| 64 | - | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor}, | |
| 64 | + | capabilities: terminalcap.Capabilities{ColorMode: terminalcap.TrueColor}, | |
| 65 | 65 | want: "\x1b[1;1H\x1b[0;38;2;255;0;0;48;2;0;0;255;58;2;0;255;0mX\x1b[0m", | |
| 66 | 66 | }, | |
| 67 | 67 | { | |
| 68 | 68 | name: "indexed 256", | |
| 69 | - | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorIndexed256}, | |
| 69 | + | capabilities: terminalcap.Capabilities{ColorMode: terminalcap.Indexed256}, | |
| 70 | 70 | want: "\x1b[1;1H\x1b[0;38;5;196;48;5;21;58;5;46mX\x1b[0m", | |
| 71 | 71 | }, | |
| 72 | 72 | } | |
| @@ -451,7 +451,7 @@ func TestAttachmentOutputFailedSendKeepsTextCursorAndGraphicsSpeculative(t *test | |||
| 451 | 451 | graphicsState := newGraphicsOutputState() | |
| 452 | 452 | output := attachmentOutputWithGraphics(graphicsState) | |
| 453 | 453 | output.lastCursor = cursorOut{valid: true, row: 1, col: 1} | |
| 454 | - | ac := &attachedClient{output: output, terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true}} | |
| 454 | + | ac := &attachedClient{output: output, terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}} | |
| 455 | 455 | output.attachment = ac | |
| 456 | 456 | frame := renderer.NewFrame(1, 1) | |
| 457 | 457 | prepared, err := output.prepareFrame(nil, state, frame, []renderer.Damage{renderer.FullRedraw()}, true, cursorOut{row: 2, col: 3}) | |
M internal/usecase/daemon/client.go +5 -4
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/vev/internal/domain" | |
| 15 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 15 | 16 | "github.com/bnema/vev/internal/ports" | |
| 16 | 17 | "github.com/bnema/vev/internal/protocol" | |
| 17 | 18 | "github.com/bnema/vev/internal/usecase/keys" | |
| @@ -40,7 +41,7 @@ type attachedClient struct { | |||
| 40 | 41 | overlays *overlayRuntime | |
| 41 | 42 | overlayOnce sync.Once | |
| 42 | 43 | clientID [16]byte | |
| 43 | - | terminalCapabilities ports.TerminalCapabilities | |
| 44 | + | terminalCapabilities terminalcap.Capabilities | |
| 44 | 45 | navigationCapabilities protocol.NavigationCapabilities | |
| 45 | 46 | startupOverlay protocol.StartupOverlay | |
| 46 | 47 | // lifecycle is the sole authority for attachment capability publication, | |
| @@ -612,7 +613,7 @@ type attachClientOptions struct { | |||
| 612 | 613 | clientID [16]byte | |
| 613 | 614 | resumeCapable bool | |
| 614 | 615 | maxOutputInFlight uint8 | |
| 615 | - | terminalCapabilities ports.TerminalCapabilities | |
| 616 | + | terminalCapabilities terminalcap.Capabilities | |
| 616 | 617 | capabilitiesSet bool | |
| 617 | 618 | navigationCapabilities protocol.NavigationCapabilities | |
| 618 | 619 | startupOverlay protocol.StartupOverlay | |
| @@ -644,7 +645,7 @@ func (d *Daemon) attachClient(sess *session, tr ports.ServerConnection, sz domai | |||
| 644 | 645 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | |
| 645 | 646 | d.touchMRU(sess) | |
| 646 | 647 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | |
| 647 | - | if ac.terminalCapabilities.ColorSource == ports.TerminalCapabilityDeclared && !ac.terminalCapabilities.TrueColor() { | |
| 648 | + | if ac.terminalCapabilities.ColorSource == terminalcap.SourceDeclared && !ac.terminalCapabilities.TrueColor() { | |
| 648 | 649 | d.publishToast(ac, domain.Notification{ | |
| 649 | 650 | Code: domain.NoticeUser, | |
| 650 | 651 | Severity: domain.NoticeWarn, | |
| @@ -669,7 +670,7 @@ func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.ServerConne | |||
| 669 | 670 | resumeToken = d.nextResumeTokenLocked() | |
| 670 | 671 | } | |
| 671 | 672 | if !opts.capabilitiesSet { | |
| 672 | - | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | |
| 673 | + | opts.terminalCapabilities = terminalcap.Capabilities{ColorMode: terminalcap.TrueColor} | |
| 673 | 674 | } | |
| 674 | 675 | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) | |
| 675 | 676 | geometry = geometry.NormalizePixels() | |
M internal/usecase/daemon/control.go +18 -17
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/bnema/vev/internal/domain" | |
| 14 | 14 | "github.com/bnema/vev/internal/ports" | |
| 15 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 16 | 17 | "github.com/bnema/vev/internal/usecase/command" | |
| 17 | 18 | "github.com/bnema/vev/internal/usecase/layout" | |
| 18 | 19 | ) | |
| @@ -711,7 +712,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) { | |||
| 711 | 712 | } | |
| 712 | 713 | e.d.mu.Unlock() | |
| 713 | 714 | ||
| 714 | - | rows := make([]ports.RemoteCatalogSession, 0, len(sessions)+len(stopped)) | |
| 715 | + | rows := make([]catalogue.RemoteCatalogSession, 0, len(sessions)+len(stopped)) | |
| 715 | 716 | liveNames := make(map[string]struct{}, len(sessions)) | |
| 716 | 717 | for _, sess := range sessions { | |
| 717 | 718 | e.d.refreshSessionFocusedTitles(sess) | |
| @@ -720,10 +721,10 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) { | |||
| 720 | 721 | if err != nil { | |
| 721 | 722 | return "", err | |
| 722 | 723 | } | |
| 723 | - | row := ports.RemoteCatalogSession{ | |
| 724 | + | row := catalogue.RemoteCatalogSession{ | |
| 724 | 725 | LifecycleID: snap.incarnation, | |
| 725 | 726 | Name: snap.name, | |
| 726 | - | State: ports.RemoteCatalogSessionUp, | |
| 727 | + | State: catalogue.RemoteCatalogSessionUp, | |
| 727 | 728 | Ephemeral: snap.ephemeral, | |
| 728 | 729 | Tabs: tabs, | |
| 729 | 730 | Attached: snap.attached, | |
| @@ -743,13 +744,13 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) { | |||
| 743 | 744 | if err != nil { | |
| 744 | 745 | return "", err | |
| 745 | 746 | } | |
| 746 | - | state := ports.RemoteCatalogSessionDown | |
| 747 | + | state := catalogue.RemoteCatalogSessionDown | |
| 747 | 748 | reason := "" | |
| 748 | 749 | if entry.broken() { | |
| 749 | - | state = ports.RemoteCatalogSessionBroken | |
| 750 | + | state = catalogue.RemoteCatalogSessionBroken | |
| 750 | 751 | reason = "session_broken" | |
| 751 | 752 | } | |
| 752 | - | rows = append(rows, ports.RemoteCatalogSession{ | |
| 753 | + | rows = append(rows, catalogue.RemoteCatalogSession{ | |
| 753 | 754 | LifecycleID: entry.incarnation, | |
| 754 | 755 | Name: entry.name, | |
| 755 | 756 | State: state, | |
| @@ -765,24 +766,24 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) { | |||
| 765 | 766 | } | |
| 766 | 767 | return rows[i].LifecycleID.String() < rows[j].LifecycleID.String() | |
| 767 | 768 | }) | |
| 768 | - | catalog := ports.RemoteCatalog{ | |
| 769 | + | catalog := catalogue.RemoteCatalog{ | |
| 769 | 770 | ProtocolVersion: protocol.Version, | |
| 770 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 771 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 771 | 772 | Sessions: rows, | |
| 772 | 773 | } | |
| 773 | - | if err := ports.ValidateRemoteCatalog(catalog); err != nil { | |
| 774 | + | if err := catalogue.ValidateRemoteCatalog(catalog); err != nil { | |
| 774 | 775 | return "", err | |
| 775 | 776 | } | |
| 776 | 777 | return marshalListing(catalog) | |
| 777 | 778 | } | |
| 778 | 779 | ||
| 779 | - | func remoteCatalogTabs(view sessionView) ([]ports.RemoteCatalogTab, error) { | |
| 780 | - | if len(view.tabs) > ports.RemoteCatalogMaxTabsPerSess { | |
| 780 | + | func remoteCatalogTabs(view sessionView) ([]catalogue.RemoteCatalogTab, error) { | |
| 781 | + | if len(view.tabs) > catalogue.RemoteCatalogMaxTabsPerSess { | |
| 781 | 782 | return nil, fmt.Errorf("remote catalog: session %q has too many tabs", view.name) | |
| 782 | 783 | } | |
| 783 | - | tabs := make([]ports.RemoteCatalogTab, 0, len(view.tabs)) | |
| 784 | + | tabs := make([]catalogue.RemoteCatalogTab, 0, len(view.tabs)) | |
| 784 | 785 | for i, tab := range view.tabs { | |
| 785 | - | tabs = append(tabs, ports.RemoteCatalogTab{ | |
| 786 | + | tabs = append(tabs, catalogue.RemoteCatalogTab{ | |
| 786 | 787 | ID: string(tab.id), | |
| 787 | 788 | Index: uint16(i), | |
| 788 | 789 | Name: tab.name, | |
| @@ -793,7 +794,7 @@ func remoteCatalogTabs(view sessionView) ([]ports.RemoteCatalogTab, error) { | |||
| 793 | 794 | return tabs, nil | |
| 794 | 795 | } | |
| 795 | 796 | ||
| 796 | - | func remoteCatalogStoppedTabs(entry inactiveSession) ([]ports.RemoteCatalogTab, error) { | |
| 797 | + | func remoteCatalogStoppedTabs(entry inactiveSession) ([]catalogue.RemoteCatalogTab, error) { | |
| 797 | 798 | records := entry.tabRecords | |
| 798 | 799 | if len(records) == 0 && len(entry.tabNames) != 0 { | |
| 799 | 800 | records = make([]domain.CatalogueTabRecord, len(entry.tabNames)) | |
| @@ -801,12 +802,12 @@ func remoteCatalogStoppedTabs(entry inactiveSession) ([]ports.RemoteCatalogTab, | |||
| 801 | 802 | records[i].Name = name | |
| 802 | 803 | } | |
| 803 | 804 | } | |
| 804 | - | if len(records) > ports.RemoteCatalogMaxTabsPerSess { | |
| 805 | + | if len(records) > catalogue.RemoteCatalogMaxTabsPerSess { | |
| 805 | 806 | return nil, fmt.Errorf("remote catalog: session %q has too many tabs", entry.name) | |
| 806 | 807 | } | |
| 807 | - | tabs := make([]ports.RemoteCatalogTab, 0, len(records)) | |
| 808 | + | tabs := make([]catalogue.RemoteCatalogTab, 0, len(records)) | |
| 808 | 809 | for i, record := range records { | |
| 809 | - | tabs = append(tabs, ports.RemoteCatalogTab{ID: string(record.StableID), Index: uint16(i), Name: record.Name}) | |
| 810 | + | tabs = append(tabs, catalogue.RemoteCatalogTab{ID: string(record.StableID), Index: uint16(i), Name: record.Name}) | |
| 810 | 811 | } | |
| 811 | 812 | return tabs, nil | |
| 812 | 813 | } | |
M internal/usecase/daemon/control_test.go +13 -12
| @@ -14,9 +14,10 @@ import ( | |||
| 14 | 14 | "time" | |
| 15 | 15 | ||
| 16 | 16 | "github.com/bnema/vev/internal/domain" | |
| 17 | - | "github.com/bnema/vev/internal/ports" | |
| 17 | + | appports "github.com/bnema/vev/internal/ports" | |
| 18 | 18 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 19 | 19 | "github.com/bnema/vev/internal/protocol" | |
| 20 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 20 | 21 | "github.com/bnema/vev/internal/protocol/wire" | |
| 21 | 22 | "github.com/bnema/vev/internal/usecase/command" | |
| 22 | 23 | "github.com/bnema/vev/internal/usecase/layout" | |
| @@ -774,7 +775,7 @@ func TestRemoteCatalogJSONOutput(t *testing.T) { | |||
| 774 | 775 | require.True(t, result.OK, result.Text) | |
| 775 | 776 | require.True(t, strings.HasSuffix(result.Output, "\n"), "catalog output must be newline-terminated") | |
| 776 | 777 | ||
| 777 | - | var catalog ports.RemoteCatalog | |
| 778 | + | var catalog catalogue.RemoteCatalog | |
| 778 | 779 | require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog)) | |
| 779 | 780 | require.Equal(t, protocol.Version, catalog.ProtocolVersion) | |
| 780 | 781 | ||
| @@ -786,13 +787,13 @@ func TestRemoteCatalogJSONOutput(t *testing.T) { | |||
| 786 | 787 | _, hasSessions := raw["sessions"] | |
| 787 | 788 | require.True(t, hasProtocol && hasSchema && hasSessions) | |
| 788 | 789 | ||
| 789 | - | require.Equal(t, []ports.RemoteCatalogSession{ | |
| 790 | - | {LifecycleID: build.incarnation, Name: "build", State: "up", Ephemeral: true, Tabs: []ports.RemoteCatalogTab{{ID: "t_build", Name: "1", Detail: " (sh)"}}, ActiveTabID: "t_build", LastUsedSeq: 1}, | |
| 791 | - | {LifecycleID: domain.IncarnationID{3}, Name: "old", State: "down", Tabs: []ports.RemoteCatalogTab{}}, | |
| 792 | - | {LifecycleID: work.incarnation, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "t_work", Name: "1", Detail: " (sh)"}, {ID: "t_work_2", Index: 1, Name: "2", Detail: " (sh)"}}, Attached: true, ActiveTabID: "t_work", LastUsedSeq: 2}, | |
| 790 | + | require.Equal(t, []catalogue.RemoteCatalogSession{ | |
| 791 | + | {LifecycleID: build.incarnation, Name: "build", State: "up", Ephemeral: true, Tabs: []catalogue.RemoteCatalogTab{{ID: "t_build", Name: "1", Detail: " (sh)"}}, ActiveTabID: "t_build", LastUsedSeq: 1}, | |
| 792 | + | {LifecycleID: domain.IncarnationID{3}, Name: "old", State: "down", Tabs: []catalogue.RemoteCatalogTab{}}, | |
| 793 | + | {LifecycleID: work.incarnation, Name: "work", State: "up", Tabs: []catalogue.RemoteCatalogTab{{ID: "t_work", Name: "1", Detail: " (sh)"}, {ID: "t_work_2", Index: 1, Name: "2", Detail: " (sh)"}}, Attached: true, ActiveTabID: "t_work", LastUsedSeq: 2}, | |
| 793 | 794 | }, catalog.Sessions) | |
| 794 | 795 | ||
| 795 | - | require.Equal(t, ports.RemoteCatalogSessionDown, catalog.Sessions[1].State) | |
| 796 | + | require.Equal(t, catalogue.RemoteCatalogSessionDown, catalog.Sessions[1].State) | |
| 796 | 797 | ||
| 797 | 798 | listAfter := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"}) | |
| 798 | 799 | require.True(t, listAfter.OK, listAfter.Text) | |
| @@ -818,14 +819,14 @@ func TestRemoteCatalogRefreshesFocusedTabTitle(t *testing.T) { | |||
| 818 | 819 | ||
| 819 | 820 | result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true}) | |
| 820 | 821 | require.True(t, result.OK, result.Text) | |
| 821 | - | var catalog ports.RemoteCatalog | |
| 822 | + | var catalog catalogue.RemoteCatalog | |
| 822 | 823 | require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog)) | |
| 823 | - | require.Equal(t, []ports.RemoteCatalogSession{{ | |
| 824 | + | require.Equal(t, []catalogue.RemoteCatalogSession{{ | |
| 824 | 825 | LifecycleID: sess.incarnation, | |
| 825 | 826 | Name: "work", | |
| 826 | 827 | State: "up", | |
| 827 | 828 | Ephemeral: true, | |
| 828 | - | Tabs: []ports.RemoteCatalogTab{{ | |
| 829 | + | Tabs: []catalogue.RemoteCatalogTab{{ | |
| 829 | 830 | ID: "t_work", | |
| 830 | 831 | Index: 0, | |
| 831 | 832 | Name: "shell", | |
| @@ -840,7 +841,7 @@ func TestRemoteCatalogRejectsTooManyTabs(t *testing.T) { | |||
| 840 | 841 | d := newTestDaemon(t, nil, stubClock{}) | |
| 841 | 842 | sess := addControlSession(d, "work", "t_work", "p_work") | |
| 842 | 843 | sess.mu.Lock() | |
| 843 | - | sess.tabs = make([]*tab, ports.RemoteCatalogMaxTabsPerSess+1) | |
| 844 | + | sess.tabs = make([]*tab, catalogue.RemoteCatalogMaxTabsPerSess+1) | |
| 844 | 845 | for i := range sess.tabs { | |
| 845 | 846 | sess.tabs[i] = newTabWithStableID( | |
| 846 | 847 | fmt.Sprintf("tab-%d", i), fmt.Sprintf("pane-%d", i), newQuietPTY(), domain.Size{Cols: 80, Rows: 22}, | |
| @@ -1345,7 +1346,7 @@ type controlPTYFactory struct { | |||
| 1345 | 1346 | once sync.Once | |
| 1346 | 1347 | } | |
| 1347 | 1348 | ||
| 1348 | - | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1349 | + | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (appports.PTY, error) { | |
| 1349 | 1350 | f.once.Do(func() { | |
| 1350 | 1351 | if f.entered != nil { | |
| 1351 | 1352 | close(f.entered) | |
M internal/usecase/daemon/daemon.go +5 -9
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "time" | |
| 17 | 17 | ||
| 18 | 18 | "github.com/bnema/vev/internal/domain" | |
| 19 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 19 | 20 | "github.com/bnema/vev/internal/ports" | |
| 20 | 21 | "github.com/bnema/vev/internal/protocol" | |
| 21 | 22 | "github.com/bnema/vev/internal/usecase/keys" | |
| @@ -304,8 +305,6 @@ type Daemon struct { | |||
| 304 | 305 | remoteCatalogClient ports.RemoteCatalogClient | |
| 305 | 306 | remoteCatalogCache ports.RemoteCatalogCache | |
| 306 | 307 | remotePreviewClient ports.RemotePreviewClient | |
| 307 | - | remoteDialerFactory ports.RemoteDialerFactory | |
| 308 | - | remoteTransportMode ports.RemoteTransportMode | |
| 309 | 308 | // tempDir overrides os.TempDir() for clipboard-image-transfer writes | |
| 310 | 309 | // (see clipboard.go); empty means use os.TempDir(). | |
| 311 | 310 | tempDir string | |
| @@ -416,14 +415,11 @@ func WithRuntimeObserver(observer ports.SerializedRuntimeObserver) Option { | |||
| 416 | 415 | } | |
| 417 | 416 | ||
| 418 | 417 | // WithRemoteDiscovery installs the remote discovery ports used by the daemon. | |
| 419 | - | // The composition root validates mode before constructing the daemon. | |
| 420 | - | func WithRemoteDiscovery(store ports.RemoteHostStore, catalog ports.RemoteCatalogClient, cache ports.RemoteCatalogCache, dialer ports.RemoteDialerFactory, mode ports.RemoteTransportMode) Option { | |
| 418 | + | func WithRemoteDiscovery(store ports.RemoteHostStore, catalog ports.RemoteCatalogClient, cache ports.RemoteCatalogCache) Option { | |
| 421 | 419 | return func(d *Daemon) { | |
| 422 | 420 | d.remoteHostStore = store | |
| 423 | 421 | d.remoteCatalogClient = catalog | |
| 424 | 422 | d.remoteCatalogCache = cache | |
| 425 | - | d.remoteDialerFactory = dialer | |
| 426 | - | d.remoteTransportMode = mode | |
| 427 | 423 | } | |
| 428 | 424 | } | |
| 429 | 425 | ||
| @@ -1351,14 +1347,14 @@ func (d *Daemon) finishAttach(sess *session, tr ports.ServerConnection, sz domai | |||
| 1351 | 1347 | sess.env = copyEnvironment(h.Env) | |
| 1352 | 1348 | } | |
| 1353 | 1349 | sess.mu.Unlock() | |
| 1354 | - | terminalCapabilities := ports.DetectTerminalCapabilities(h.Env) | |
| 1350 | + | terminalCapabilities := terminalcap.Detect(h.Env) | |
| 1355 | 1351 | // Kitty graphics are enabled only by the explicit direct-terminal | |
| 1356 | 1352 | // declaration in Hello. Environment values remain useful for color and | |
| 1357 | 1353 | // diagnostics, but cannot authorize terminal-global graphics side effects. | |
| 1358 | 1354 | terminalCapabilities.KittyGraphics = h.KittyDirectGraphics | |
| 1359 | 1355 | if h.TrueColor && !terminalCapabilities.TrueColor() { | |
| 1360 | - | terminalCapabilities.ColorMode = ports.TerminalColorTrueColor | |
| 1361 | - | terminalCapabilities.ColorSource = ports.TerminalCapabilityDeclared | |
| 1356 | + | terminalCapabilities.ColorMode = terminalcap.TrueColor | |
| 1357 | + | terminalCapabilities.ColorSource = terminalcap.SourceDeclared | |
| 1362 | 1358 | } | |
| 1363 | 1359 | opts := attachClientOptions{ | |
| 1364 | 1360 | clientID: h.ClientID, | |
M internal/usecase/daemon/graphics_output_test.go +6 -6
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | vt "github.com/bnema/vev-vt" | |
| 14 | 14 | "github.com/bnema/vev-vt/graphics" | |
| 15 | 15 | "github.com/bnema/vev/internal/domain" | |
| 16 | - | "github.com/bnema/vev/internal/ports" | |
| 16 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 17 | 17 | "github.com/bnema/vev/internal/protocol" | |
| 18 | 18 | "github.com/bnema/vev/internal/protocol/wire" | |
| 19 | 19 | "github.com/bnema/vev/internal/usecase/layout" | |
| @@ -295,7 +295,7 @@ func TestGraphicsOutputDefersCleanupThatDoesNotFitFrameBudget(t *testing.T) { | |||
| 295 | 295 | seed.commit() | |
| 296 | 296 | ac := &attachedClient{ | |
| 297 | 297 | output: attachmentOutputWithGraphics(graphicsState), | |
| 298 | - | terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true}, | |
| 298 | + | terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}, | |
| 299 | 299 | } | |
| 300 | 300 | ||
| 301 | 301 | deferred, err := graphicsOutputDataWithDaemonLimit(nil, &capturedRenderState{}, ac, false, 0) | |
| @@ -512,7 +512,7 @@ func TestGraphicsNamespacePoolExhaustionFallsBackToText(t *testing.T) { | |||
| 512 | 512 | domain.Geometry{Size: defaultSize}, | |
| 513 | 513 | attachClientOptions{ | |
| 514 | 514 | capabilitiesSet: true, | |
| 515 | - | terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true, ColorMode: ports.TerminalColorTrueColor}, | |
| 515 | + | terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true, ColorMode: terminalcap.TrueColor}, | |
| 516 | 516 | }, | |
| 517 | 517 | ) | |
| 518 | 518 | d.mu.Unlock() | |
| @@ -624,7 +624,7 @@ func TestGraphicsOutputOversizeSuppressesGraphicsWithoutCompositionError(t *test | |||
| 624 | 624 | }}} | |
| 625 | 625 | ac := &attachedClient{ | |
| 626 | 626 | output: attachmentOutputWithGraphics(newGraphicsOutputState()), | |
| 627 | - | terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true}, | |
| 627 | + | terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}, | |
| 628 | 628 | } | |
| 629 | 629 | prepared, err := graphicsOutputData(state, ac, true) | |
| 630 | 630 | require.NoError(t, err, "ANSI composition must continue when optional graphics exceed the bound") | |
| @@ -644,7 +644,7 @@ func TestGraphicsOutputClipsPlacementToPaneContent(t *testing.T) { | |||
| 644 | 644 | }}} | |
| 645 | 645 | ac := &attachedClient{ | |
| 646 | 646 | output: attachmentOutputWithGraphics(newGraphicsOutputState()), | |
| 647 | - | terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true}, | |
| 647 | + | terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}, | |
| 648 | 648 | } | |
| 649 | 649 | prepared, err := graphicsOutputData(state, ac, true) | |
| 650 | 650 | require.NoError(t, err) | |
| @@ -825,7 +825,7 @@ func graphicsPaneTestState(tab domain.TabStableID, panes ...capturedPaneRenderSt | |||
| 825 | 825 | func graphicsPaneTestClient() *attachedClient { | |
| 826 | 826 | return &attachedClient{ | |
| 827 | 827 | output: attachmentOutputWithGraphics(newGraphicsOutputState()), | |
| 828 | - | terminalCapabilities: ports.TerminalCapabilities{KittyGraphics: true}, | |
| 828 | + | terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}, | |
| 829 | 829 | } | |
| 830 | 830 | } | |
| 831 | 831 | ||
M internal/usecase/daemon/palette.go +3 -3
| @@ -7,8 +7,8 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | renderer "github.com/bnema/vev-vt" | |
| 9 | 9 | "github.com/bnema/vev/internal/domain" | |
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol" | |
| 11 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 12 | 12 | "github.com/bnema/vev/internal/usecase/command" | |
| 13 | 13 | "github.com/bnema/vev/internal/usecase/layout" | |
| 14 | 14 | "github.com/bnema/vev/internal/usecase/palette" | |
| @@ -105,8 +105,8 @@ func paletteRouteRepresentsDaemonSession(entry protocol.RecentRouteEntry, daemon | |||
| 105 | 105 | paletteRemoteDisplayOrigin(entry.HostLabel) == daemonDisplayOrigin | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | - | func remotePaletteUnavailableReason(status remoteHostStatus, session ports.RemoteCatalogSession) string { | |
| 109 | - | if session.State == ports.RemoteCatalogSessionBroken { | |
| 108 | + | func remotePaletteUnavailableReason(status remoteHostStatus, session catalogue.RemoteCatalogSession) string { | |
| 109 | + | if session.State == catalogue.RemoteCatalogSessionBroken { | |
| 110 | 110 | return "session_broken" | |
| 111 | 111 | } | |
| 112 | 112 | return remoteReasonForStatus(status) | |
M internal/usecase/daemon/palette_test.go +20 -20
| @@ -11,9 +11,9 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | renderer "github.com/bnema/vev-vt" | |
| 13 | 13 | "github.com/bnema/vev/internal/domain" | |
| 14 | - | "github.com/bnema/vev/internal/ports" | |
| 15 | 14 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 16 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 17 | 17 | "github.com/bnema/vev/internal/protocol/wire" | |
| 18 | 18 | "github.com/bnema/vev/internal/usecase/command" | |
| 19 | 19 | "github.com/bnema/vev/internal/usecase/palette" | |
| @@ -439,11 +439,11 @@ func TestPaletteIncludesExactRemoteCatalogTargetBesideSameNameLocalSession(t *te | |||
| 439 | 439 | local.ephemeral = false | |
| 440 | 440 | remoteLifecycle := domain.SessionLifecycleID{21} | |
| 441 | 441 | d.remoteCatalog.mu.Lock() | |
| 442 | - | d.remoteCatalog.cache["user@arch"] = ports.RemoteCatalogCacheEntry{ | |
| 442 | + | d.remoteCatalog.cache["user@arch"] = catalogue.RemoteCatalogCacheEntry{ | |
| 443 | 443 | Host: "user@arch", FetchedAt: now, | |
| 444 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 445 | - | LifecycleID: remoteLifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 446 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-remote", Index: 0, Name: "shell"}}, ActiveTabID: "tab-remote", | |
| 444 | + | Sessions: []catalogue.RemoteCatalogSession{{ | |
| 445 | + | LifecycleID: remoteLifecycle, Name: "vev", State: catalogue.RemoteCatalogSessionUp, | |
| 446 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-remote", Index: 0, Name: "shell"}}, ActiveTabID: "tab-remote", | |
| 447 | 447 | }}, | |
| 448 | 448 | } | |
| 449 | 449 | d.remoteCatalog.status["user@arch"] = remoteHostFresh | |
| @@ -527,11 +527,11 @@ func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) { | |||
| 527 | 527 | lifecycle := domain.SessionLifecycleID{22} | |
| 528 | 528 | d.remoteCatalog.mu.Lock() | |
| 529 | 529 | for _, endpoint := range test.catalogEndpoints { | |
| 530 | - | d.remoteCatalog.cache[endpoint] = ports.RemoteCatalogCacheEntry{ | |
| 530 | + | d.remoteCatalog.cache[endpoint] = catalogue.RemoteCatalogCacheEntry{ | |
| 531 | 531 | Host: endpoint, FetchedAt: now, | |
| 532 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 533 | - | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 534 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 532 | + | Sessions: []catalogue.RemoteCatalogSession{{ | |
| 533 | + | LifecycleID: lifecycle, Name: "vev", State: catalogue.RemoteCatalogSessionUp, | |
| 534 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 535 | 535 | }}, | |
| 536 | 536 | } | |
| 537 | 537 | d.remoteCatalog.status[endpoint] = remoteHostFresh | |
| @@ -577,11 +577,11 @@ func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) { | |||
| 577 | 577 | d.clock = fixedRemoteRefreshClock{now: now} | |
| 578 | 578 | remoteLifecycle := domain.SessionLifecycleID{31} | |
| 579 | 579 | d.remoteCatalog.mu.Lock() | |
| 580 | - | d.remoteCatalog.cache["user@arch"] = ports.RemoteCatalogCacheEntry{ | |
| 580 | + | d.remoteCatalog.cache["user@arch"] = catalogue.RemoteCatalogCacheEntry{ | |
| 581 | 581 | Host: "user@arch", FetchedAt: now, | |
| 582 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 583 | - | LifecycleID: remoteLifecycle, Name: "work", State: ports.RemoteCatalogSessionUp, | |
| 584 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-work", Index: 0, Name: "shell"}}, ActiveTabID: "tab-work", | |
| 582 | + | Sessions: []catalogue.RemoteCatalogSession{{ | |
| 583 | + | LifecycleID: remoteLifecycle, Name: "work", State: catalogue.RemoteCatalogSessionUp, | |
| 584 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-work", Index: 0, Name: "shell"}}, ActiveTabID: "tab-work", | |
| 585 | 585 | }}, | |
| 586 | 586 | } | |
| 587 | 587 | d.remoteCatalog.status["user@arch"] = remoteHostFresh | |
| @@ -616,11 +616,11 @@ func TestPaletteCachedRemoteSelectionFailsClosed(t *testing.T) { | |||
| 616 | 616 | now := time.Unix(1_000, 0) | |
| 617 | 617 | d.clock = fixedRemoteRefreshClock{now: now} | |
| 618 | 618 | d.remoteCatalog.mu.Lock() | |
| 619 | - | d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{ | |
| 619 | + | d.remoteCatalog.cache["arch"] = catalogue.RemoteCatalogCacheEntry{ | |
| 620 | 620 | Host: "arch", FetchedAt: now, | |
| 621 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 622 | - | LifecycleID: domain.SessionLifecycleID{32}, Name: "cached", State: ports.RemoteCatalogSessionUp, | |
| 623 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-cached", Index: 0}}, ActiveTabID: "tab-cached", | |
| 621 | + | Sessions: []catalogue.RemoteCatalogSession{{ | |
| 622 | + | LifecycleID: domain.SessionLifecycleID{32}, Name: "cached", State: catalogue.RemoteCatalogSessionUp, | |
| 623 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-cached", Index: 0}}, ActiveTabID: "tab-cached", | |
| 624 | 624 | }}, | |
| 625 | 625 | } | |
| 626 | 626 | d.remoteCatalog.status["arch"] = remoteHostCached | |
| @@ -660,9 +660,9 @@ func TestRemoteRefreshUpdatesOpenPaletteAndPreservesQuery(t *testing.T) { | |||
| 660 | 660 | model.Insert(r) | |
| 661 | 661 | } | |
| 662 | 662 | ac.overlays.paletteMu.Unlock() | |
| 663 | - | request.result <- remoteRefreshResult{catalog: remoteCatalogForTest(ports.RemoteCatalogSession{ | |
| 664 | - | LifecycleID: domain.SessionLifecycleID{41}, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 665 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 663 | + | request.result <- remoteRefreshResult{catalog: remoteCatalogForTest(catalogue.RemoteCatalogSession{ | |
| 664 | + | LifecycleID: domain.SessionLifecycleID{41}, Name: "vev", State: catalogue.RemoteCatalogSessionUp, | |
| 665 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 666 | 666 | })} | |
| 667 | 667 | receiveRemotePicker(t, cache.stores, "palette catalog cache store") | |
| 668 | 668 | d.sessWg.Wait() | |
M internal/usecase/daemon/picker.go +6 -6
| @@ -9,8 +9,8 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | renderer "github.com/bnema/vev-vt" | |
| 11 | 11 | "github.com/bnema/vev/internal/domain" | |
| 12 | - | "github.com/bnema/vev/internal/ports" | |
| 13 | 12 | "github.com/bnema/vev/internal/protocol" | |
| 13 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 14 | 14 | "github.com/bnema/vev/internal/usecase/layout" | |
| 15 | 15 | "github.com/bnema/vev/internal/usecase/picker" | |
| 16 | 16 | "github.com/bnema/vev/internal/usecase/ui" | |
| @@ -1005,14 +1005,14 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, t | |||
| 1005 | 1005 | return nil | |
| 1006 | 1006 | } | |
| 1007 | 1007 | ||
| 1008 | - | func (d *Daemon) remoteCatalogEntryLocked(host string) (ports.RemoteCatalogCacheEntry, bool) { | |
| 1008 | + | func (d *Daemon) remoteCatalogEntryLocked(host string) (catalogue.RemoteCatalogCacheEntry, bool) { | |
| 1009 | 1009 | if d.remoteCatalog.status[host] != remoteHostFresh { | |
| 1010 | - | return ports.RemoteCatalogCacheEntry{}, false | |
| 1010 | + | return catalogue.RemoteCatalogCacheEntry{}, false | |
| 1011 | 1011 | } | |
| 1012 | 1012 | entry, ok := d.remoteCatalog.cache[host] | |
| 1013 | 1013 | if !ok || remoteCatalogExpired(entry.FetchedAt, d.clock.Now()) { | |
| 1014 | 1014 | d.remoteCatalog.status[host] = remoteHostStale | |
| 1015 | - | return ports.RemoteCatalogCacheEntry{}, false | |
| 1015 | + | return catalogue.RemoteCatalogCacheEntry{}, false | |
| 1016 | 1016 | } | |
| 1017 | 1017 | return entry, true | |
| 1018 | 1018 | } | |
| @@ -1031,10 +1031,10 @@ func (d *Daemon) remoteCatalogTargetReady(target domain.RemoteSessionTarget) boo | |||
| 1031 | 1031 | if session.Name != target.SessionName || session.LifecycleID != target.LifecycleID { | |
| 1032 | 1032 | continue | |
| 1033 | 1033 | } | |
| 1034 | - | if target.Stopped != remoteSessionStateStopped(session.State) || session.State == ports.RemoteCatalogSessionBroken { | |
| 1034 | + | if target.Stopped != remoteSessionStateStopped(session.State) || session.State == catalogue.RemoteCatalogSessionBroken { | |
| 1035 | 1035 | return false | |
| 1036 | 1036 | } | |
| 1037 | - | tabs := ports.CatalogTabs(session) | |
| 1037 | + | tabs := catalogue.CatalogTabs(session) | |
| 1038 | 1038 | metadata := make([]domain.TabSelectorTab, 0, len(tabs)) | |
| 1039 | 1039 | for _, tab := range tabs { | |
| 1040 | 1040 | metadata = append(metadata, domain.TabSelectorTab{ID: domain.TabStableID(tab.ID), Name: tab.Name}) | |
M internal/usecase/daemon/remote_attach_test.go +11 -11
| @@ -7,8 +7,8 @@ import ( | |||
| 7 | 7 | "github.com/stretchr/testify/require" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/domain" | |
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol" | |
| 11 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 12 | 12 | "github.com/bnema/vev/internal/protocol/wire" | |
| 13 | 13 | "github.com/bnema/vev/internal/usecase/picker" | |
| 14 | 14 | ) | |
| @@ -76,9 +76,9 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) { | |||
| 76 | 76 | func TestRemotePickerRichHandoffCarriesLifecycleTabAndPolicy(t *testing.T) { | |
| 77 | 77 | d := newRemotePickerDaemon(nil) | |
| 78 | 78 | lifecycle := remoteLifecycleForTest() | |
| 79 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 80 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{ | |
| 81 | - | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 79 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 80 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 81 | + | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 82 | 82 | }}, | |
| 83 | 83 | }}) | |
| 84 | 84 | d.remoteCatalog.mu.Lock() | |
| @@ -117,10 +117,10 @@ func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) { | |||
| 117 | 117 | t.Run(test.name, func(t *testing.T) { | |
| 118 | 118 | d := newRemotePickerDaemon(nil) | |
| 119 | 119 | lifecycle := remoteLifecycleForTest() | |
| 120 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 121 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{ | |
| 122 | - | LifecycleID: lifecycle, Name: "work", State: ports.RemoteCatalogSessionUp, | |
| 123 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 120 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 121 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 122 | + | LifecycleID: lifecycle, Name: "work", State: catalogue.RemoteCatalogSessionUp, | |
| 123 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 124 | 124 | }}, | |
| 125 | 125 | }}) | |
| 126 | 126 | d.remoteCatalog.mu.Lock() | |
| @@ -155,9 +155,9 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||
| 155 | 155 | lifecycle := remoteLifecycleForTest() | |
| 156 | 156 | cachedLifecycle := lifecycle | |
| 157 | 157 | cachedLifecycle[0]++ | |
| 158 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 159 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{ | |
| 160 | - | LifecycleID: cachedLifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 158 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 159 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 160 | + | LifecycleID: cachedLifecycle, Name: "work", State: "up", Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 161 | 161 | }}, | |
| 162 | 162 | }}) | |
| 163 | 163 | d.remoteCatalog.mu.Lock() | |
M internal/usecase/daemon/remote_catalog_state.go +13 -16
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | "sync" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 10 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | type remoteHostStatus uint8 | |
| @@ -22,8 +22,6 @@ const ( | |||
| 22 | 22 | remoteHostMalformed | |
| 23 | 23 | ) | |
| 24 | 24 | ||
| 25 | - | // remoteCatalogState owns the cache-derived discovery state. Its mutex never | |
| 26 | - | // covers cache or transport I/O; writeMu only serializes complete cache writes. | |
| 27 | 25 | type remoteDiscoveryConsumerKind uint8 | |
| 28 | 26 | ||
| 29 | 27 | const ( | |
| @@ -31,9 +29,11 @@ const ( | |||
| 31 | 29 | remoteDiscoveryPalette | |
| 32 | 30 | ) | |
| 33 | 31 | ||
| 32 | + | // remoteCatalogState owns cache-derived discovery state. Its mutex never | |
| 33 | + | // covers cache or transport I/O; writeMu serializes complete cache writes. | |
| 34 | 34 | type remoteCatalogState struct { | |
| 35 | 35 | mu sync.Mutex | |
| 36 | - | cache map[string]ports.RemoteCatalogCacheEntry | |
| 36 | + | cache map[string]catalogue.RemoteCatalogCacheEntry | |
| 37 | 37 | status map[string]remoteHostStatus | |
| 38 | 38 | failure map[string]time.Time | |
| 39 | 39 | refresh uint64 | |
| @@ -44,35 +44,35 @@ type remoteCatalogState struct { | |||
| 44 | 44 | ||
| 45 | 45 | func newRemoteCatalogState() remoteCatalogState { | |
| 46 | 46 | return remoteCatalogState{ | |
| 47 | - | cache: make(map[string]ports.RemoteCatalogCacheEntry), | |
| 47 | + | cache: make(map[string]catalogue.RemoteCatalogCacheEntry), | |
| 48 | 48 | status: make(map[string]remoteHostStatus), | |
| 49 | 49 | failure: make(map[string]time.Time), | |
| 50 | 50 | consumers: make(map[*attachedClient]remoteDiscoveryConsumerKind), | |
| 51 | 51 | } | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | - | func cloneRemoteCatalogEntry(entry ports.RemoteCatalogCacheEntry) ports.RemoteCatalogCacheEntry { | |
| 55 | - | sessions := make([]ports.RemoteCatalogSession, len(entry.Sessions)) | |
| 54 | + | func cloneRemoteCatalogEntry(entry catalogue.RemoteCatalogCacheEntry) catalogue.RemoteCatalogCacheEntry { | |
| 55 | + | sessions := make([]catalogue.RemoteCatalogSession, len(entry.Sessions)) | |
| 56 | 56 | for i, session := range entry.Sessions { | |
| 57 | 57 | sessions[i] = session | |
| 58 | - | if tabs := ports.CatalogTabs(session); tabs != nil { | |
| 58 | + | if tabs := catalogue.CatalogTabs(session); tabs != nil { | |
| 59 | 59 | sessions[i].Tabs = slices.Clone(tabs) | |
| 60 | 60 | } | |
| 61 | 61 | } | |
| 62 | - | return ports.RemoteCatalogCacheEntry{ | |
| 62 | + | return catalogue.RemoteCatalogCacheEntry{ | |
| 63 | 63 | Host: entry.Host, | |
| 64 | 64 | FetchedAt: entry.FetchedAt, | |
| 65 | 65 | Sessions: sessions, | |
| 66 | 66 | } | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | - | func (s *remoteCatalogState) replaceCache(entries []ports.RemoteCatalogCacheEntry) { | |
| 69 | + | func (s *remoteCatalogState) replaceCache(entries []catalogue.RemoteCatalogCacheEntry) { | |
| 70 | 70 | if s == nil { | |
| 71 | 71 | return | |
| 72 | 72 | } | |
| 73 | 73 | s.mu.Lock() | |
| 74 | 74 | defer s.mu.Unlock() | |
| 75 | - | s.cache = make(map[string]ports.RemoteCatalogCacheEntry, len(entries)) | |
| 75 | + | s.cache = make(map[string]catalogue.RemoteCatalogCacheEntry, len(entries)) | |
| 76 | 76 | s.status = make(map[string]remoteHostStatus, len(entries)) | |
| 77 | 77 | s.failure = make(map[string]time.Time) | |
| 78 | 78 | for _, entry := range entries { | |
| @@ -81,13 +81,13 @@ func (s *remoteCatalogState) replaceCache(entries []ports.RemoteCatalogCacheEntr | |||
| 81 | 81 | } | |
| 82 | 82 | } | |
| 83 | 83 | ||
| 84 | - | func (s *remoteCatalogState) cacheSnapshot() []ports.RemoteCatalogCacheEntry { | |
| 84 | + | func (s *remoteCatalogState) cacheSnapshot() []catalogue.RemoteCatalogCacheEntry { | |
| 85 | 85 | if s == nil { | |
| 86 | 86 | return nil | |
| 87 | 87 | } | |
| 88 | 88 | s.mu.Lock() | |
| 89 | 89 | defer s.mu.Unlock() | |
| 90 | - | entries := make([]ports.RemoteCatalogCacheEntry, 0, len(s.cache)) | |
| 90 | + | entries := make([]catalogue.RemoteCatalogCacheEntry, 0, len(s.cache)) | |
| 91 | 91 | for _, entry := range s.cache { | |
| 92 | 92 | entries = append(entries, cloneRemoteCatalogEntry(entry)) | |
| 93 | 93 | } | |
| @@ -107,9 +107,6 @@ func (d *Daemon) loadRemoteCatalogCache() { | |||
| 107 | 107 | d.remoteCatalog.replaceCache(entries) | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | - | // persistRemoteCatalogCache writes the newest complete cache snapshot. Cache | |
| 111 | - | // I/O happens after releasing the state mutex and remains serialized with | |
| 112 | - | // other writes so an older snapshot cannot win a later atomic replacement. | |
| 113 | 110 | func (d *Daemon) persistRemoteCatalogCache() { | |
| 114 | 111 | if d == nil || d.remoteCatalogCache == nil { | |
| 115 | 112 | return | |
M internal/usecase/daemon/remote_catalog_state_test.go +17 -17
| @@ -8,23 +8,23 @@ import ( | |||
| 8 | 8 | "testing" | |
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 12 | 12 | "github.com/stretchr/testify/require" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | 15 | type catalogCacheStub struct { | |
| 16 | - | load func() ([]ports.RemoteCatalogCacheEntry, error) | |
| 17 | - | store func([]ports.RemoteCatalogCacheEntry) error | |
| 16 | + | load func() ([]catalogue.RemoteCatalogCacheEntry, error) | |
| 17 | + | store func([]catalogue.RemoteCatalogCacheEntry) error | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | func (s catalogCacheStub) Load() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 20 | + | func (s catalogCacheStub) Load() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 21 | 21 | if s.load == nil { | |
| 22 | 22 | return nil, nil | |
| 23 | 23 | } | |
| 24 | 24 | return s.load() | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | - | func (s catalogCacheStub) Store(entries []ports.RemoteCatalogCacheEntry) error { | |
| 27 | + | func (s catalogCacheStub) Store(entries []catalogue.RemoteCatalogCacheEntry) error { | |
| 28 | 28 | if s.store == nil { | |
| 29 | 29 | return nil | |
| 30 | 30 | } | |
| @@ -35,31 +35,31 @@ func TestRemoteCatalogCacheStartup(t *testing.T) { | |||
| 35 | 35 | now := time.Unix(0, 42) | |
| 36 | 36 | tests := []struct { | |
| 37 | 37 | name string | |
| 38 | - | load func() ([]ports.RemoteCatalogCacheEntry, error) | |
| 39 | - | wantCache map[string]ports.RemoteCatalogCacheEntry | |
| 38 | + | load func() ([]catalogue.RemoteCatalogCacheEntry, error) | |
| 39 | + | wantCache map[string]catalogue.RemoteCatalogCacheEntry | |
| 40 | 40 | wantStatus map[string]remoteHostStatus | |
| 41 | 41 | }{ | |
| 42 | 42 | { | |
| 43 | 43 | name: "loads cached entries", | |
| 44 | - | load: func() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 45 | - | return []ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}}, nil | |
| 44 | + | load: func() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 45 | + | return []catalogue.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: now, Sessions: []catalogue.RemoteCatalogSession{{Name: "work", State: "up"}}}}, nil | |
| 46 | 46 | }, | |
| 47 | - | wantCache: map[string]ports.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}}}, | |
| 47 | + | wantCache: map[string]catalogue.RemoteCatalogCacheEntry{"arch": {Host: "arch", FetchedAt: now, Sessions: []catalogue.RemoteCatalogSession{{Name: "work", State: "up"}}}}, | |
| 48 | 48 | wantStatus: map[string]remoteHostStatus{"arch": remoteHostCached}, | |
| 49 | 49 | }, | |
| 50 | 50 | { | |
| 51 | 51 | name: "invalid cache starts empty", | |
| 52 | - | load: func() ([]ports.RemoteCatalogCacheEntry, error) { | |
| 52 | + | load: func() ([]catalogue.RemoteCatalogCacheEntry, error) { | |
| 53 | 53 | return nil, errors.New("invalid cache") | |
| 54 | 54 | }, | |
| 55 | - | wantCache: map[string]ports.RemoteCatalogCacheEntry{}, | |
| 55 | + | wantCache: map[string]catalogue.RemoteCatalogCacheEntry{}, | |
| 56 | 56 | wantStatus: map[string]remoteHostStatus{}, | |
| 57 | 57 | }, | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | 60 | for _, tt := range tests { | |
| 61 | 61 | t.Run(tt.name, func(t *testing.T) { | |
| 62 | - | d := New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, catalogCacheStub{load: tt.load}, nil, ports.RemoteTransportUDP)) | |
| 62 | + | d := New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, catalogCacheStub{load: tt.load})) | |
| 63 | 63 | ||
| 64 | 64 | d.remoteCatalog.mu.Lock() | |
| 65 | 65 | defer d.remoteCatalog.mu.Unlock() | |
| @@ -76,7 +76,7 @@ func TestRemoteCatalogCacheStoreSerializesLockFreeIO(t *testing.T) { | |||
| 76 | 76 | var active, maxActive int | |
| 77 | 77 | var activeMu sync.Mutex | |
| 78 | 78 | ||
| 79 | - | cache := catalogCacheStub{store: func(entries []ports.RemoteCatalogCacheEntry) error { | |
| 79 | + | cache := catalogCacheStub{store: func(entries []catalogue.RemoteCatalogCacheEntry) error { | |
| 80 | 80 | if !d.mu.TryLock() { | |
| 81 | 81 | return errors.New("daemon lock held during cache I/O") | |
| 82 | 82 | } | |
| @@ -99,16 +99,16 @@ func TestRemoteCatalogCacheStoreSerializesLockFreeIO(t *testing.T) { | |||
| 99 | 99 | activeMu.Unlock() | |
| 100 | 100 | return nil | |
| 101 | 101 | }} | |
| 102 | - | d = New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, cache, nil, ports.RemoteTransportUDP)) | |
| 102 | + | d = New(nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(nil, nil, cache)) | |
| 103 | 103 | d.remoteCatalog.mu.Lock() | |
| 104 | - | d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []ports.RemoteCatalogSession{{Name: "one", State: "up"}}} | |
| 104 | + | d.remoteCatalog.cache["arch"] = catalogue.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 1), Sessions: []catalogue.RemoteCatalogSession{{Name: "one", State: "up"}}} | |
| 105 | 105 | d.remoteCatalog.mu.Unlock() | |
| 106 | 106 | ||
| 107 | 107 | done := make(chan struct{}, 2) | |
| 108 | 108 | go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }() | |
| 109 | 109 | <-entered | |
| 110 | 110 | d.remoteCatalog.mu.Lock() | |
| 111 | - | d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []ports.RemoteCatalogSession{{Name: "two", State: "up"}}} | |
| 111 | + | d.remoteCatalog.cache["arch"] = catalogue.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(0, 2), Sessions: []catalogue.RemoteCatalogSession{{Name: "two", State: "up"}}} | |
| 112 | 112 | d.remoteCatalog.mu.Unlock() | |
| 113 | 113 | go func() { d.persistRemoteCatalogCache(); done <- struct{}{} }() | |
| 114 | 114 | ||
M internal/usecase/daemon/remote_picker.go +19 -19
| @@ -10,13 +10,13 @@ import ( | |||
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | - | "github.com/bnema/vev/internal/ports" | |
| 13 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 14 | 14 | "github.com/bnema/vev/internal/usecase/palette" | |
| 15 | 15 | "github.com/bnema/vev/internal/usecase/picker" | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | 18 | type remoteCatalogPresentationEntry struct { | |
| 19 | - | entry ports.RemoteCatalogCacheEntry | |
| 19 | + | entry catalogue.RemoteCatalogCacheEntry | |
| 20 | 20 | status remoteHostStatus | |
| 21 | 21 | } | |
| 22 | 22 | ||
| @@ -72,7 +72,7 @@ func (d *Daemon) remoteCatalogSnapshot() []remoteCatalogPresentationEntry { | |||
| 72 | 72 | continue | |
| 73 | 73 | } | |
| 74 | 74 | entries = append(entries, remoteCatalogPresentationEntry{ | |
| 75 | - | entry: ports.RemoteCatalogCacheEntry{Host: host}, | |
| 75 | + | entry: catalogue.RemoteCatalogCacheEntry{Host: host}, | |
| 76 | 76 | status: status, | |
| 77 | 77 | }) | |
| 78 | 78 | } | |
| @@ -138,11 +138,11 @@ func remotePickerStatusDetail(status remoteHostStatus, fetchedAt time.Time) stri | |||
| 138 | 138 | } | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | - | func remoteSessionStateStopped(state ports.RemoteCatalogSessionState) bool { | |
| 142 | - | return state == ports.RemoteCatalogSessionDown | |
| 141 | + | func remoteSessionStateStopped(state catalogue.RemoteCatalogSessionState) bool { | |
| 142 | + | return state == catalogue.RemoteCatalogSessionDown | |
| 143 | 143 | } | |
| 144 | 144 | ||
| 145 | - | func remoteCatalogSessionTarget(key domain.RemoteSessionKey, session ports.RemoteCatalogSession) (domain.RemoteSessionKey, domain.RemoteSessionTarget) { | |
| 145 | + | func remoteCatalogSessionTarget(key domain.RemoteSessionKey, session catalogue.RemoteCatalogSession) (domain.RemoteSessionKey, domain.RemoteSessionTarget) { | |
| 146 | 146 | key.LifecycleID = session.LifecycleID | |
| 147 | 147 | key.DisplayOrigin = domain.RemoteDisplayOrigin(key.Host) | |
| 148 | 148 | target := domain.RemoteSessionTarget{ | |
| @@ -152,7 +152,7 @@ func remoteCatalogSessionTarget(key domain.RemoteSessionKey, session ports.Remot | |||
| 152 | 152 | SessionName: session.Name, | |
| 153 | 153 | Stopped: remoteSessionStateStopped(session.State), | |
| 154 | 154 | } | |
| 155 | - | tabs := ports.CatalogTabs(session) | |
| 155 | + | tabs := catalogue.CatalogTabs(session) | |
| 156 | 156 | if len(tabs) == 0 { | |
| 157 | 157 | return key, target | |
| 158 | 158 | } | |
| @@ -177,12 +177,12 @@ func remoteCatalogSessionTarget(key domain.RemoteSessionKey, session ports.Remot | |||
| 177 | 177 | return key, target | |
| 178 | 178 | } | |
| 179 | 179 | ||
| 180 | - | func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSession, status remoteHostStatus, fetchedAt time.Time) picker.SessionView { | |
| 180 | + | func remotePickerView(key domain.RemoteSessionKey, session catalogue.RemoteCatalogSession, status remoteHostStatus, fetchedAt time.Time) picker.SessionView { | |
| 181 | 181 | key, target := remoteCatalogSessionTarget(key, session) | |
| 182 | 182 | availability := remotePickerAvailability(status) | |
| 183 | 183 | stopped := remoteSessionStateStopped(session.State) | |
| 184 | - | broken := session.State == ports.RemoteCatalogSessionBroken | |
| 185 | - | tabs := ports.CatalogTabs(session) | |
| 184 | + | broken := session.State == catalogue.RemoteCatalogSessionBroken | |
| 185 | + | tabs := catalogue.CatalogTabs(session) | |
| 186 | 186 | viewTabs := make([]picker.TabEntry, 0, len(tabs)) | |
| 187 | 187 | active := 0 | |
| 188 | 188 | for i, tab := range tabs { | |
| @@ -236,7 +236,7 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe | |||
| 236 | 236 | detail = "identity changed" | |
| 237 | 237 | case activation == picker.RemoteRestart: | |
| 238 | 238 | detail = "stopped — Enter to restart" | |
| 239 | - | case session.State == ports.RemoteCatalogSessionUp: | |
| 239 | + | case session.State == catalogue.RemoteCatalogSessionUp: | |
| 240 | 240 | detail = "up" | |
| 241 | 241 | case stopped: | |
| 242 | 242 | detail = "stopped" | |
| @@ -573,7 +573,7 @@ func containsRemoteDiscoveryHost(hosts []string, target string) bool { | |||
| 573 | 573 | // applyRemoteRefreshResult publishes one host completion only if both its | |
| 574 | 574 | // refresh generation and current host-registry membership remain authoritative. | |
| 575 | 575 | // Its return reports whether durable cache state changed. | |
| 576 | - | func (d *Daemon) applyRemoteRefreshResult(generation uint64, host string, catalog ports.RemoteCatalog, listErr error) bool { | |
| 576 | + | func (d *Daemon) applyRemoteRefreshResult(generation uint64, host string, catalog catalogue.RemoteCatalog, listErr error) bool { | |
| 577 | 577 | if d == nil { | |
| 578 | 578 | return false | |
| 579 | 579 | } | |
| @@ -592,7 +592,7 @@ func (d *Daemon) applyRemoteRefreshResult(generation uint64, host string, catalo | |||
| 592 | 592 | stillKnown := containsRemoteDiscoveryHost(hosts, host) | |
| 593 | 593 | now := d.clock.Now() | |
| 594 | 594 | if listErr == nil { | |
| 595 | - | if err := ports.ValidateRemoteCatalog(catalog); err != nil { | |
| 595 | + | if err := catalogue.ValidateRemoteCatalog(catalog); err != nil { | |
| 596 | 596 | listErr = err | |
| 597 | 597 | } | |
| 598 | 598 | } | |
| @@ -616,13 +616,13 @@ func (d *Daemon) applyRemoteRefreshResult(generation uint64, host string, catalo | |||
| 616 | 616 | } | |
| 617 | 617 | if listErr != nil { | |
| 618 | 618 | status := remoteHostMalformed | |
| 619 | - | var mismatch *ports.RemoteCatalogVersionMismatchError | |
| 619 | + | var mismatch *catalogue.RemoteCatalogVersionMismatchError | |
| 620 | 620 | if errors.As(listErr, &mismatch) { | |
| 621 | 621 | status = remoteHostVersionMismatch | |
| 622 | - | } else if !errors.Is(listErr, ports.ErrInvalidRemoteCatalog) && | |
| 623 | - | !errors.Is(listErr, ports.ErrRemoteCatalogTooLarge) && | |
| 624 | - | !errors.Is(listErr, ports.ErrRemoteCatalogUnknownState) && | |
| 625 | - | !errors.Is(listErr, ports.ErrRemoteCatalogInvalidReason) { | |
| 622 | + | } else if !errors.Is(listErr, catalogue.ErrInvalidRemoteCatalog) && | |
| 623 | + | !errors.Is(listErr, catalogue.ErrRemoteCatalogTooLarge) && | |
| 624 | + | !errors.Is(listErr, catalogue.ErrRemoteCatalogUnknownState) && | |
| 625 | + | !errors.Is(listErr, catalogue.ErrRemoteCatalogInvalidReason) { | |
| 626 | 626 | status = remoteHostUnreachable | |
| 627 | 627 | } | |
| 628 | 628 | d.remoteCatalog.status[host] = status | |
| @@ -631,7 +631,7 @@ func (d *Daemon) applyRemoteRefreshResult(generation uint64, host string, catalo | |||
| 631 | 631 | d.refreshRemoteDiscoveryConsumers() | |
| 632 | 632 | return false | |
| 633 | 633 | } | |
| 634 | - | d.remoteCatalog.cache[host] = cloneRemoteCatalogEntry(ports.RemoteCatalogCacheEntry{ | |
| 634 | + | d.remoteCatalog.cache[host] = cloneRemoteCatalogEntry(catalogue.RemoteCatalogCacheEntry{ | |
| 635 | 635 | Host: host, | |
| 636 | 636 | FetchedAt: now, | |
| 637 | 637 | Sessions: catalog.Sessions, | |
M internal/usecase/daemon/remote_picker_test.go +100 -99
| @@ -15,9 +15,10 @@ import ( | |||
| 15 | 15 | "github.com/stretchr/testify/require" | |
| 16 | 16 | ||
| 17 | 17 | "github.com/bnema/vev/internal/domain" | |
| 18 | - | "github.com/bnema/vev/internal/ports" | |
| 18 | + | appports "github.com/bnema/vev/internal/ports" | |
| 19 | 19 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 20 | 20 | "github.com/bnema/vev/internal/protocol" | |
| 21 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 21 | 22 | "github.com/bnema/vev/internal/protocol/wire" | |
| 22 | 23 | "github.com/bnema/vev/internal/usecase/picker" | |
| 23 | 24 | ) | |
| @@ -56,16 +57,16 @@ func receiveRemotePickerClose(t *testing.T, ch <-chan struct{}, what string) { | |||
| 56 | 57 | require.NoError(t, waitRemotePickerClose(ch, what)) | |
| 57 | 58 | } | |
| 58 | 59 | ||
| 59 | - | func newRemotePickerDaemon(store ports.RemoteHostStore) *Daemon { | |
| 60 | - | return New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(store, nil, nil, nil, ports.RemoteTransportUDP)) | |
| 60 | + | func newRemotePickerDaemon(store appports.RemoteHostStore) *Daemon { | |
| 61 | + | return New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(store, nil, nil)) | |
| 61 | 62 | } | |
| 62 | 63 | ||
| 63 | 64 | func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T) { | |
| 64 | 65 | lifecycle := remoteLifecycleForTest() | |
| 65 | 66 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle} | |
| 66 | - | session := ports.RemoteCatalogSession{ | |
| 67 | - | LifecycleID: lifecycle, Name: "work", State: ports.RemoteCatalogSessionDown, | |
| 68 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 67 | + | session := catalogue.RemoteCatalogSession{ | |
| 68 | + | LifecycleID: lifecycle, Name: "work", State: catalogue.RemoteCatalogSessionDown, | |
| 69 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 69 | 70 | } | |
| 70 | 71 | view := remotePickerView(key, session, remoteHostFresh, time.Unix(100, 0)) | |
| 71 | 72 | require.True(t, view.Stopped) | |
| @@ -77,9 +78,9 @@ func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T) | |||
| 77 | 78 | } | |
| 78 | 79 | ||
| 79 | 80 | func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) { | |
| 80 | - | tabs := make([]ports.RemoteCatalogTab, math.MaxUint16+1) | |
| 81 | - | tabs[0] = ports.RemoteCatalogTab{Name: "main"} | |
| 82 | - | view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, ports.RemoteCatalogSession{ | |
| 81 | + | tabs := make([]catalogue.RemoteCatalogTab, math.MaxUint16+1) | |
| 82 | + | tabs[0] = catalogue.RemoteCatalogTab{Name: "main"} | |
| 83 | + | view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, catalogue.RemoteCatalogSession{ | |
| 83 | 84 | LifecycleID: remoteLifecycleForTest(), Name: "work", State: "down", Tabs: tabs, | |
| 84 | 85 | }, remoteHostFresh, time.Unix(100, 0)) | |
| 85 | 86 | ||
| @@ -89,9 +90,9 @@ func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) { | |||
| 89 | 90 | ||
| 90 | 91 | func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) { | |
| 91 | 92 | for _, attached := range []bool{false, true} { | |
| 92 | - | view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{ | |
| 93 | + | view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, catalogue.RemoteCatalogSession{ | |
| 93 | 94 | LifecycleID: remoteLifecycleForTest(), Name: "work", State: "up", Attached: attached, | |
| 94 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0}}, | |
| 95 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0}}, | |
| 95 | 96 | }, remoteHostFresh, time.Now()) | |
| 96 | 97 | require.Equal(t, "up", view.RemoteDetail) | |
| 97 | 98 | } | |
| @@ -101,13 +102,13 @@ func TestRemotePickerScopesSameLifecycleBytesToEachEndpoint(t *testing.T) { | |||
| 101 | 102 | store := &remoteRefreshHostStore{hosts: []string{"remote", "vev@remote"}} | |
| 102 | 103 | d := newRemotePickerDaemon(store) | |
| 103 | 104 | lifecycle := remoteLifecycleForTest() | |
| 104 | - | session := ports.RemoteCatalogSession{ | |
| 105 | + | session := catalogue.RemoteCatalogSession{ | |
| 105 | 106 | LifecycleID: lifecycle, Name: "work", State: "up", | |
| 106 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 107 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 107 | 108 | } | |
| 108 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{ | |
| 109 | - | {Host: "remote", FetchedAt: time.Unix(100, 0), Sessions: []ports.RemoteCatalogSession{session}}, | |
| 110 | - | {Host: "vev@remote", FetchedAt: time.Unix(100, 0), Sessions: []ports.RemoteCatalogSession{session}}, | |
| 109 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{ | |
| 110 | + | {Host: "remote", FetchedAt: time.Unix(100, 0), Sessions: []catalogue.RemoteCatalogSession{session}}, | |
| 111 | + | {Host: "vev@remote", FetchedAt: time.Unix(100, 0), Sessions: []catalogue.RemoteCatalogSession{session}}, | |
| 111 | 112 | }) | |
| 112 | 113 | d.remoteCatalog.mu.Lock() | |
| 113 | 114 | d.remoteCatalog.status["remote"] = remoteHostFresh | |
| @@ -131,9 +132,9 @@ func TestRemotePickerCatalogExpiresAtAttachTTL(t *testing.T) { | |||
| 131 | 132 | clock := &remotePickerClock{now: now} | |
| 132 | 133 | d := newTestDaemon(t, nil, clock) | |
| 133 | 134 | lifecycle := remoteLifecycleForTest() | |
| 134 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 135 | - | Host: "arch", FetchedAt: now.Add(-remoteCatalogAttachTTL), Sessions: []ports.RemoteCatalogSession{{ | |
| 136 | - | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 135 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 136 | + | Host: "arch", FetchedAt: now.Add(-remoteCatalogAttachTTL), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 137 | + | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 137 | 138 | }}, | |
| 138 | 139 | }}) | |
| 139 | 140 | d.remoteCatalog.mu.Lock() | |
| @@ -152,9 +153,9 @@ func TestRemotePickerTargetReadyExpiresAfterPickerBuildWithoutSnapshot(t *testin | |||
| 152 | 153 | clock := &remotePickerClock{now: now} | |
| 153 | 154 | d := newTestDaemon(t, nil, clock) | |
| 154 | 155 | lifecycle := remoteLifecycleForTest() | |
| 155 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 156 | - | Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{{ | |
| 157 | - | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 156 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 157 | + | Host: "arch", FetchedAt: now, Sessions: []catalogue.RemoteCatalogSession{{ | |
| 158 | + | LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 158 | 159 | }}, | |
| 159 | 160 | }}) | |
| 160 | 161 | d.remoteCatalog.mu.Lock() | |
| @@ -177,14 +178,14 @@ func TestRemotePickerTargetReadyExpiresAfterPickerBuildWithoutSnapshot(t *testin | |||
| 177 | 178 | func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) { | |
| 178 | 179 | tests := []struct { | |
| 179 | 180 | name string | |
| 180 | - | tabs []ports.RemoteCatalogTab | |
| 181 | + | tabs []catalogue.RemoteCatalogTab | |
| 181 | 182 | activeTabID string | |
| 182 | 183 | wantActive int | |
| 183 | 184 | wantTabID domain.TabStableID | |
| 184 | 185 | }{ | |
| 185 | 186 | { | |
| 186 | 187 | name: "active first", | |
| 187 | - | tabs: []ports.RemoteCatalogTab{ | |
| 188 | + | tabs: []catalogue.RemoteCatalogTab{ | |
| 188 | 189 | {ID: "tab-active", Index: 0, Name: "active"}, | |
| 189 | 190 | {ID: "tab-last", Index: 1, Name: "last"}, | |
| 190 | 191 | }, | |
| @@ -194,7 +195,7 @@ func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) { | |||
| 194 | 195 | }, | |
| 195 | 196 | { | |
| 196 | 197 | name: "active last", | |
| 197 | - | tabs: []ports.RemoteCatalogTab{ | |
| 198 | + | tabs: []catalogue.RemoteCatalogTab{ | |
| 198 | 199 | {ID: "tab-first", Index: 0, Name: "first"}, | |
| 199 | 200 | {ID: "tab-active", Index: 1, Name: "active"}, | |
| 200 | 201 | }, | |
| @@ -204,7 +205,7 @@ func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) { | |||
| 204 | 205 | }, | |
| 205 | 206 | { | |
| 206 | 207 | name: "empty active tab ID falls back to first stable tab", | |
| 207 | - | tabs: []ports.RemoteCatalogTab{ | |
| 208 | + | tabs: []catalogue.RemoteCatalogTab{ | |
| 208 | 209 | {ID: "tab-first", Index: 0, Name: "first"}, | |
| 209 | 210 | {ID: "", Index: 1, Name: "malformed"}, | |
| 210 | 211 | }, | |
| @@ -217,8 +218,8 @@ func TestRemotePickerSelectsCatalogActiveTab(t *testing.T) { | |||
| 217 | 218 | t.Run(test.name, func(t *testing.T) { | |
| 218 | 219 | d := newRemotePickerDaemon(nil) | |
| 219 | 220 | lifecycle := remoteLifecycleForTest() | |
| 220 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 221 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{ | |
| 221 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 222 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 222 | 223 | LifecycleID: lifecycle, | |
| 223 | 224 | Name: "vive", | |
| 224 | 225 | State: "up", | |
| @@ -259,8 +260,8 @@ func TestRemotePickerAvailabilityUsesCachedRowsForFailures(t *testing.T) { | |||
| 259 | 260 | for _, test := range tests { | |
| 260 | 261 | t.Run(test.name, func(t *testing.T) { | |
| 261 | 262 | d := newRemotePickerDaemon(nil) | |
| 262 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 263 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}, | |
| 263 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 264 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "work", State: "up"}}, | |
| 264 | 265 | }}) | |
| 265 | 266 | d.remoteCatalog.mu.Lock() | |
| 266 | 267 | d.remoteCatalog.status["arch"] = test.status | |
| @@ -278,8 +279,8 @@ func TestRemotePickerStaleDetailUsesLastSuccessfulFetch(t *testing.T) { | |||
| 278 | 279 | d := newRemotePickerDaemon(nil) | |
| 279 | 280 | fetchedAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC) | |
| 280 | 281 | failedAt := fetchedAt.Add(6 * time.Hour) | |
| 281 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 282 | - | Host: "arch", FetchedAt: fetchedAt, Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}, | |
| 282 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 283 | + | Host: "arch", FetchedAt: fetchedAt, Sessions: []catalogue.RemoteCatalogSession{{Name: "work", State: "up"}}, | |
| 283 | 284 | }}) | |
| 284 | 285 | d.remoteCatalog.mu.Lock() | |
| 285 | 286 | d.remoteCatalog.status["arch"] = remoteHostUnreachable | |
| @@ -355,7 +356,7 @@ type remoteRefreshRequest struct { | |||
| 355 | 356 | } | |
| 356 | 357 | ||
| 357 | 358 | type remoteRefreshResult struct { | |
| 358 | - | catalog ports.RemoteCatalog | |
| 359 | + | catalog catalogue.RemoteCatalog | |
| 359 | 360 | err error | |
| 360 | 361 | } | |
| 361 | 362 | ||
| @@ -367,7 +368,7 @@ type channelRemoteCatalog struct { | |||
| 367 | 368 | violations []string | |
| 368 | 369 | } | |
| 369 | 370 | ||
| 370 | - | func (c *channelRemoteCatalog) List(ctx context.Context, host string) (ports.RemoteCatalog, error) { | |
| 371 | + | func (c *channelRemoteCatalog) List(ctx context.Context, host string) (catalogue.RemoteCatalog, error) { | |
| 371 | 372 | c.probeMu.Lock() | |
| 372 | 373 | if c.probes && !c.d.mu.TryLock() { | |
| 373 | 374 | c.violations = append(c.violations, "daemon lock held during remote catalog I/O") | |
| @@ -386,21 +387,21 @@ func (c *channelRemoteCatalog) List(ctx context.Context, host string) (ports.Rem | |||
| 386 | 387 | case result := <-request.result: | |
| 387 | 388 | return result.catalog, result.err | |
| 388 | 389 | case <-ctx.Done(): | |
| 389 | - | return ports.RemoteCatalog{}, ctx.Err() | |
| 390 | + | return catalogue.RemoteCatalog{}, ctx.Err() | |
| 390 | 391 | } | |
| 391 | 392 | } | |
| 392 | 393 | ||
| 393 | 394 | type recordingRemoteCache struct { | |
| 394 | 395 | d *Daemon | |
| 395 | - | stores chan []ports.RemoteCatalogCacheEntry | |
| 396 | + | stores chan []catalogue.RemoteCatalogCacheEntry | |
| 396 | 397 | probeMu sync.Mutex | |
| 397 | 398 | probes bool | |
| 398 | 399 | violations []string | |
| 399 | 400 | } | |
| 400 | 401 | ||
| 401 | - | func (*recordingRemoteCache) Load() ([]ports.RemoteCatalogCacheEntry, error) { return nil, nil } | |
| 402 | + | func (*recordingRemoteCache) Load() ([]catalogue.RemoteCatalogCacheEntry, error) { return nil, nil } | |
| 402 | 403 | ||
| 403 | - | func (c *recordingRemoteCache) Store(entries []ports.RemoteCatalogCacheEntry) error { | |
| 404 | + | func (c *recordingRemoteCache) Store(entries []catalogue.RemoteCatalogCacheEntry) error { | |
| 404 | 405 | c.probeMu.Lock() | |
| 405 | 406 | if c.probes && !c.d.mu.TryLock() { | |
| 406 | 407 | c.violations = append(c.violations, "daemon lock held during remote cache write") | |
| @@ -413,7 +414,7 @@ func (c *recordingRemoteCache) Store(entries []ports.RemoteCatalogCacheEntry) er | |||
| 413 | 414 | c.d.remoteCatalog.mu.Unlock() | |
| 414 | 415 | } | |
| 415 | 416 | c.probeMu.Unlock() | |
| 416 | - | copyEntries := make([]ports.RemoteCatalogCacheEntry, len(entries)) | |
| 417 | + | copyEntries := make([]catalogue.RemoteCatalogCacheEntry, len(entries)) | |
| 417 | 418 | for i, entry := range entries { | |
| 418 | 419 | copyEntries[i] = cloneRemoteCatalogEntry(entry) | |
| 419 | 420 | } | |
| @@ -423,8 +424,8 @@ func (c *recordingRemoteCache) Store(entries []ports.RemoteCatalogCacheEntry) er | |||
| 423 | 424 | ||
| 424 | 425 | type fixedRemoteRefreshClock struct{ now time.Time } | |
| 425 | 426 | ||
| 426 | - | func (c fixedRemoteRefreshClock) Now() time.Time { return c.now } | |
| 427 | - | func (fixedRemoteRefreshClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | |
| 427 | + | func (c fixedRemoteRefreshClock) Now() time.Time { return c.now } | |
| 428 | + | func (fixedRemoteRefreshClock) NewTimer(time.Duration) appports.Timer { return stubTimer{} } | |
| 428 | 429 | ||
| 429 | 430 | type remotePickerClock struct { | |
| 430 | 431 | mu sync.Mutex | |
| @@ -436,7 +437,7 @@ func (c *remotePickerClock) Now() time.Time { | |||
| 436 | 437 | defer c.mu.Unlock() | |
| 437 | 438 | return c.now | |
| 438 | 439 | } | |
| 439 | - | func (*remotePickerClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | |
| 440 | + | func (*remotePickerClock) NewTimer(time.Duration) appports.Timer { return stubTimer{} } | |
| 440 | 441 | func (c *remotePickerClock) Advance(d time.Duration) { | |
| 441 | 442 | c.mu.Lock() | |
| 442 | 443 | c.now = c.now.Add(d) | |
| @@ -457,19 +458,19 @@ func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog, | |||
| 457 | 458 | } | |
| 458 | 459 | } | |
| 459 | 460 | ||
| 460 | - | func remoteCatalogForTest(sessions ...ports.RemoteCatalogSession) ports.RemoteCatalog { | |
| 461 | - | return ports.RemoteCatalog{ | |
| 461 | + | func remoteCatalogForTest(sessions ...catalogue.RemoteCatalogSession) catalogue.RemoteCatalog { | |
| 462 | + | return catalogue.RemoteCatalog{ | |
| 462 | 463 | ProtocolVersion: protocol.Version, | |
| 463 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 464 | - | Sessions: append([]ports.RemoteCatalogSession{}, sessions...), | |
| 464 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 465 | + | Sessions: append([]catalogue.RemoteCatalogSession{}, sessions...), | |
| 465 | 466 | } | |
| 466 | 467 | } | |
| 467 | 468 | ||
| 468 | 469 | func newRemoteRefreshDaemon(t *testing.T, hosts *remoteRefreshHostStore, now time.Time) (*Daemon, *channelRemoteCatalog, *recordingRemoteCache) { | |
| 469 | 470 | t.Helper() | |
| 470 | 471 | catalog := &channelRemoteCatalog{requests: make(chan remoteRefreshRequest, 8)} | |
| 471 | - | cache := &recordingRemoteCache{stores: make(chan []ports.RemoteCatalogCacheEntry, 8)} | |
| 472 | - | d := New(nil, fixedRemoteRefreshClock{now: now}, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(hosts, catalog, cache, nil, ports.RemoteTransportUDP)) | |
| 472 | + | cache := &recordingRemoteCache{stores: make(chan []catalogue.RemoteCatalogCacheEntry, 8)} | |
| 473 | + | d := New(nil, fixedRemoteRefreshClock{now: now}, slog.New(slog.NewTextHandler(io.Discard, nil)), WithRemoteDiscovery(hosts, catalog, cache)) | |
| 473 | 474 | catalog.d, cache.d = d, d | |
| 474 | 475 | return d, catalog, cache | |
| 475 | 476 | } | |
| @@ -487,10 +488,10 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T) | |||
| 487 | 488 | ac.overlays.pickerMu.Unlock() | |
| 488 | 489 | require.True(t, d.registerRemoteDiscoveryConsumer(instance.discoveryInstance())) | |
| 489 | 490 | lifecycle := remoteLifecycleForTest() | |
| 490 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 491 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{ | |
| 491 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 492 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{ | |
| 492 | 493 | LifecycleID: lifecycle, Name: "cached", State: "up", | |
| 493 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 494 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 494 | 495 | }}, | |
| 495 | 496 | }}) | |
| 496 | 497 | ||
| @@ -504,9 +505,9 @@ func TestRemoteRefreshStartsPerHostAndCancelsSupersededGeneration(t *testing.T) | |||
| 504 | 505 | require.Contains(t, first, "mule") | |
| 505 | 506 | requireNoRemoteLockViolations(t, catalog, nil) | |
| 506 | 507 | d.remoteCatalog.mu.Lock() | |
| 507 | - | require.Equal(t, []ports.RemoteCatalogSession{{ | |
| 508 | + | require.Equal(t, []catalogue.RemoteCatalogSession{{ | |
| 508 | 509 | LifecycleID: lifecycle, Name: "cached", State: "up", | |
| 509 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 510 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}}, | |
| 510 | 511 | }}, d.remoteCatalog.cache["arch"].Sessions, "cached rows publish before any remote completion") | |
| 511 | 512 | require.Equal(t, remoteHostRefreshing, d.remoteCatalog.status["arch"]) | |
| 512 | 513 | d.remoteCatalog.mu.Unlock() | |
| @@ -581,26 +582,26 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes | |||
| 581 | 582 | now := time.Unix(200, 0) | |
| 582 | 583 | hosts := &remoteRefreshHostStore{hosts: []string{"arch", "mule"}} | |
| 583 | 584 | d, _, cache := newRemoteRefreshDaemon(t, hosts, now) | |
| 584 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{ | |
| 585 | - | {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}}}, | |
| 586 | - | {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}}, | |
| 585 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{ | |
| 586 | + | {Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "old", State: "up"}}}, | |
| 587 | + | {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "stale", State: "up"}}}, | |
| 587 | 588 | }) | |
| 588 | 589 | d.remoteCatalog.mu.Lock() | |
| 589 | 590 | d.remoteCatalog.refresh = 7 | |
| 590 | 591 | d.remoteCatalog.mu.Unlock() | |
| 591 | 592 | ||
| 592 | - | require.False(t, d.applyRemoteRefreshResult(7, "mule", ports.RemoteCatalog{}, errors.New("offline"))) | |
| 593 | + | require.False(t, d.applyRemoteRefreshResult(7, "mule", catalogue.RemoteCatalog{}, errors.New("offline"))) | |
| 593 | 594 | d.remoteCatalog.mu.Lock() | |
| 594 | - | require.Equal(t, []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}, d.remoteCatalog.cache["mule"].Sessions) | |
| 595 | + | require.Equal(t, []catalogue.RemoteCatalogSession{{Name: "stale", State: "up"}}, d.remoteCatalog.cache["mule"].Sessions) | |
| 595 | 596 | require.Equal(t, time.Unix(20, 0), d.remoteCatalog.cache["mule"].FetchedAt) | |
| 596 | 597 | require.Equal(t, remoteHostUnreachable, d.remoteCatalog.status["mule"]) | |
| 597 | 598 | d.remoteCatalog.mu.Unlock() | |
| 598 | 599 | ||
| 599 | 600 | require.True(t, d.applyRemoteRefreshResult(7, "arch", remoteCatalogForTest(), nil)) | |
| 600 | 601 | stored := receiveRemotePicker(t, cache.stores, "remote cache store") | |
| 601 | - | require.Equal(t, []ports.RemoteCatalogCacheEntry{ | |
| 602 | - | {Host: "arch", FetchedAt: now, Sessions: []ports.RemoteCatalogSession{}}, | |
| 603 | - | {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []ports.RemoteCatalogSession{{Name: "stale", State: "up"}}}, | |
| 602 | + | require.Equal(t, []catalogue.RemoteCatalogCacheEntry{ | |
| 603 | + | {Host: "arch", FetchedAt: now, Sessions: []catalogue.RemoteCatalogSession{}}, | |
| 604 | + | {Host: "mule", FetchedAt: time.Unix(20, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "stale", State: "up"}}}, | |
| 604 | 605 | }, stored, "every successful write persists the newest full cache atomically") | |
| 605 | 606 | requireNoRemoteLockViolations(t, nil, cache) | |
| 606 | 607 | d.remoteCatalog.mu.Lock() | |
| @@ -612,21 +613,21 @@ func TestRemoteRefreshResultPreservesFailuresAndEvictsSuccessfulOmissions(t *tes | |||
| 612 | 613 | func TestRemoteRefreshRejectsRemovedHostAndLateGeneration(t *testing.T) { | |
| 613 | 614 | hosts := &remoteRefreshHostStore{hosts: []string{"arch"}} | |
| 614 | 615 | d, _, cache := newRemoteRefreshDaemon(t, hosts, time.Unix(300, 0)) | |
| 615 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 616 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "old", State: "up"}}, | |
| 616 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 617 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "old", State: "up"}}, | |
| 617 | 618 | }}) | |
| 618 | 619 | d.remoteCatalog.mu.Lock() | |
| 619 | 620 | d.remoteCatalog.refresh = 9 | |
| 620 | 621 | d.remoteCatalog.mu.Unlock() | |
| 621 | 622 | ||
| 622 | - | require.False(t, d.applyRemoteRefreshResult(8, "arch", remoteCatalogForTest(ports.RemoteCatalogSession{ | |
| 623 | - | LifecycleID: remoteLifecycleForTest(), Name: "late", State: ports.RemoteCatalogSessionUp, | |
| 624 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 623 | + | require.False(t, d.applyRemoteRefreshResult(8, "arch", remoteCatalogForTest(catalogue.RemoteCatalogSession{ | |
| 624 | + | LifecycleID: remoteLifecycleForTest(), Name: "late", State: catalogue.RemoteCatalogSessionUp, | |
| 625 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 625 | 626 | }), nil)) | |
| 626 | 627 | hosts.set() | |
| 627 | - | require.True(t, d.applyRemoteRefreshResult(9, "arch", remoteCatalogForTest(ports.RemoteCatalogSession{ | |
| 628 | - | LifecycleID: remoteLifecycleForTest(), Name: "removed", State: ports.RemoteCatalogSessionUp, | |
| 629 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 628 | + | require.True(t, d.applyRemoteRefreshResult(9, "arch", remoteCatalogForTest(catalogue.RemoteCatalogSession{ | |
| 629 | + | LifecycleID: remoteLifecycleForTest(), Name: "removed", State: catalogue.RemoteCatalogSessionUp, | |
| 630 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 630 | 631 | }), nil)) | |
| 631 | 632 | require.Empty(t, receiveRemotePicker(t, cache.stores, "remote cache store")) | |
| 632 | 633 | requireNoRemoteLockViolations(t, nil, cache) | |
| @@ -640,16 +641,16 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) { | |||
| 640 | 641 | lifecycle := remoteLifecycleForTest() | |
| 641 | 642 | for _, test := range []struct { | |
| 642 | 643 | name string | |
| 643 | - | session ports.RemoteCatalogSession | |
| 644 | + | session catalogue.RemoteCatalogSession | |
| 644 | 645 | }{ | |
| 645 | - | {name: "unknown state", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "unknown"}}, | |
| 646 | - | {name: "invalid reason", session: ports.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "up", Reason: "bogus"}}, | |
| 646 | + | {name: "unknown state", session: catalogue.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "unknown"}}, | |
| 647 | + | {name: "invalid reason", session: catalogue.RemoteCatalogSession{LifecycleID: lifecycle, Name: "work", State: "up", Reason: "bogus"}}, | |
| 647 | 648 | } { | |
| 648 | 649 | t.Run(test.name, func(t *testing.T) { | |
| 649 | 650 | hosts := &remoteRefreshHostStore{hosts: []string{"arch"}} | |
| 650 | 651 | d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0)) | |
| 651 | 652 | d.remoteCatalog.refresh = 1 | |
| 652 | - | catalog := ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{test.session}} | |
| 653 | + | catalog := catalogue.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: catalogue.RemoteCatalogSchemaVersion, Sessions: []catalogue.RemoteCatalogSession{test.session}} | |
| 653 | 654 | require.False(t, d.applyRemoteRefreshResult(1, "arch", catalog, nil)) | |
| 654 | 655 | d.remoteCatalog.mu.Lock() | |
| 655 | 656 | require.Equal(t, remoteHostMalformed, d.remoteCatalog.status["arch"]) | |
| @@ -661,13 +662,13 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) { | |||
| 661 | 662 | func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) { | |
| 662 | 663 | hosts := &remoteRefreshHostStore{hosts: []string{"arch"}} | |
| 663 | 664 | d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0)) | |
| 664 | - | old := ports.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{Name: "work", State: "up"}}} | |
| 665 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{old}) | |
| 665 | + | old := catalogue.RemoteCatalogCacheEntry{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{{Name: "work", State: "up"}}} | |
| 666 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{old}) | |
| 666 | 667 | d.remoteCatalog.mu.Lock() | |
| 667 | 668 | d.remoteCatalog.refresh = 3 | |
| 668 | 669 | d.remoteCatalog.mu.Unlock() | |
| 669 | 670 | ||
| 670 | - | require.False(t, d.applyRemoteRefreshResult(3, "arch", ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{Got: 19, Want: protocol.Version})) | |
| 671 | + | require.False(t, d.applyRemoteRefreshResult(3, "arch", catalogue.RemoteCatalog{}, &catalogue.RemoteCatalogVersionMismatchError{Got: 19, Want: protocol.Version})) | |
| 671 | 672 | d.remoteCatalog.mu.Lock() | |
| 672 | 673 | require.Equal(t, old, d.remoteCatalog.cache["arch"]) | |
| 673 | 674 | require.Equal(t, remoteHostVersionMismatch, d.remoteCatalog.status["arch"]) | |
| @@ -676,13 +677,13 @@ func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) { | |||
| 676 | 677 | ||
| 677 | 678 | func TestRemotePickerHandoffSendsTargetAndLeavesNoShadowSession(t *testing.T) { | |
| 678 | 679 | lifecycle := remoteLifecycleForTest() | |
| 679 | - | remoteSession := ports.RemoteCatalogSession{ | |
| 680 | - | LifecycleID: lifecycle, Name: "work", State: ports.RemoteCatalogSessionUp, | |
| 681 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 680 | + | remoteSession := catalogue.RemoteCatalogSession{ | |
| 681 | + | LifecycleID: lifecycle, Name: "work", State: catalogue.RemoteCatalogSessionUp, | |
| 682 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 682 | 683 | } | |
| 683 | 684 | d := newRemotePickerDaemon(nil) | |
| 684 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 685 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{remoteSession}, | |
| 685 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 686 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{remoteSession}, | |
| 686 | 687 | }}) | |
| 687 | 688 | d.remoteCatalog.mu.Lock() | |
| 688 | 689 | d.remoteCatalog.status["arch"] = remoteHostFresh | |
| @@ -709,11 +710,11 @@ func TestRemotePickerHandoffSendsTargetAndLeavesNoShadowSession(t *testing.T) { | |||
| 709 | 710 | ||
| 710 | 711 | func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |
| 711 | 712 | lifecycle := remoteLifecycleForTest() | |
| 712 | - | remoteSession := ports.RemoteCatalogSession{ | |
| 713 | + | remoteSession := catalogue.RemoteCatalogSession{ | |
| 713 | 714 | LifecycleID: lifecycle, | |
| 714 | 715 | Name: "work", | |
| 715 | 716 | State: "down", | |
| 716 | - | Tabs: []ports.RemoteCatalogTab{ | |
| 717 | + | Tabs: []catalogue.RemoteCatalogTab{ | |
| 717 | 718 | {ID: "tab-a", Name: "alpha", Index: 0}, | |
| 718 | 719 | {ID: "tab-b", Name: "beta", Index: 1}, | |
| 719 | 720 | }, | |
| @@ -738,7 +739,7 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |||
| 738 | 739 | require.Equal(t, domain.NewStableTabSelector("tab-b"), selected.RemoteTarget.StoppedTab) | |
| 739 | 740 | ||
| 740 | 741 | local := newRemotePickerDaemon(nil) | |
| 741 | - | local.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{remoteSession}}}) | |
| 742 | + | local.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{remoteSession}}}) | |
| 742 | 743 | local.remoteCatalog.mu.Lock() | |
| 743 | 744 | local.remoteCatalog.status["arch"] = remoteHostFresh | |
| 744 | 745 | local.remoteCatalog.mu.Unlock() | |
| @@ -779,11 +780,11 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |||
| 779 | 780 | ||
| 780 | 781 | func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing.T) { | |
| 781 | 782 | lifecycle := remoteLifecycleForTest() | |
| 782 | - | remoteSession := ports.RemoteCatalogSession{ | |
| 783 | + | remoteSession := catalogue.RemoteCatalogSession{ | |
| 783 | 784 | LifecycleID: lifecycle, | |
| 784 | 785 | Name: "work", | |
| 785 | 786 | State: "down", | |
| 786 | - | Tabs: []ports.RemoteCatalogTab{}, | |
| 787 | + | Tabs: []catalogue.RemoteCatalogTab{}, | |
| 787 | 788 | } | |
| 788 | 789 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | |
| 789 | 790 | remoteView := remotePickerView(key, remoteSession, remoteHostFresh, time.Unix(10, 0)) | |
| @@ -807,8 +808,8 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing | |||
| 807 | 808 | require.Equal(t, domain.TabSelector{}, selected.RemoteTarget.StoppedTab) | |
| 808 | 809 | ||
| 809 | 810 | local := newRemotePickerDaemon(nil) | |
| 810 | - | local.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 811 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{remoteSession}, | |
| 811 | + | local.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 812 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{remoteSession}, | |
| 812 | 813 | }}) | |
| 813 | 814 | local.remoteCatalog.mu.Lock() | |
| 814 | 815 | local.remoteCatalog.status["arch"] = remoteHostFresh | |
| @@ -878,13 +879,13 @@ func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) { | |||
| 878 | 879 | ||
| 879 | 880 | func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) { | |
| 880 | 881 | lifecycle := remoteLifecycleForTest() | |
| 881 | - | remoteSession := ports.RemoteCatalogSession{ | |
| 882 | - | LifecycleID: lifecycle, Name: "work", State: ports.RemoteCatalogSessionUp, | |
| 883 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 882 | + | remoteSession := catalogue.RemoteCatalogSession{ | |
| 883 | + | LifecycleID: lifecycle, Name: "work", State: catalogue.RemoteCatalogSessionUp, | |
| 884 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}}, | |
| 884 | 885 | } | |
| 885 | 886 | d := newRemotePickerDaemon(nil) | |
| 886 | - | d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{ | |
| 887 | - | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{remoteSession}, | |
| 887 | + | d.remoteCatalog.replaceCache([]catalogue.RemoteCatalogCacheEntry{{ | |
| 888 | + | Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []catalogue.RemoteCatalogSession{remoteSession}, | |
| 888 | 889 | }}) | |
| 889 | 890 | d.remoteCatalog.mu.Lock() | |
| 890 | 891 | d.remoteCatalog.status["arch"] = remoteHostFresh | |
| @@ -921,9 +922,9 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) { | |||
| 921 | 922 | require.Same(t, sess, ac.currentAttachmentSession()) | |
| 922 | 923 | } | |
| 923 | 924 | ||
| 924 | - | func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...ports.ServerConnection) (*session, *attachedClient, chan wire.Frame) { | |
| 925 | + | func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...appports.ServerConnection) (*session, *attachedClient, chan wire.Frame) { | |
| 925 | 926 | t.Helper() | |
| 926 | - | var tr ports.ServerConnection | |
| 927 | + | var tr appports.ServerConnection | |
| 927 | 928 | var sends chan wire.Frame | |
| 928 | 929 | if len(transports) != 0 { | |
| 929 | 930 | tr = transports[0] | |
| @@ -962,12 +963,12 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) { | |||
| 962 | 963 | d.remoteCatalog.refresh = 4 | |
| 963 | 964 | d.remoteCatalog.mu.Unlock() | |
| 964 | 965 | ||
| 965 | - | require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{ | |
| 966 | + | require.True(t, d.applyRemoteRefreshResult(4, "arch", catalogue.RemoteCatalog{ | |
| 966 | 967 | ProtocolVersion: protocol.Version, | |
| 967 | - | SchemaVersion: ports.RemoteCatalogSchemaVersion, | |
| 968 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 968 | + | SchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 969 | + | Sessions: []catalogue.RemoteCatalogSession{{ | |
| 969 | 970 | LifecycleID: remoteLifecycleForTest(), Name: "work", State: "up", | |
| 970 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, | |
| 971 | + | Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, | |
| 971 | 972 | }}, | |
| 972 | 973 | }, nil)) | |
| 973 | 974 | receiveRemotePicker(t, cache.stores, "remote cache store") | |
M internal/usecase/daemon/render_pipeline_test.go +1 -1
| @@ -28,7 +28,7 @@ import ( | |||
| 28 | 28 | "github.com/stretchr/testify/require" | |
| 29 | 29 | ) | |
| 30 | 30 | ||
| 31 | - | // scriptedReplayTransport is intentionally a ports.Transport only: replay | |
| 31 | + | // scriptedReplayTransport is intentionally a wire.Transport only: replay | |
| 32 | 32 | // assertions must not depend on any adapter's framing implementation. | |
| 33 | 33 | type scriptedReplayTransport struct { | |
| 34 | 34 | t *testing.T | |
M internal/usecase/daemon/session_test.go +6 -6
| @@ -399,11 +399,11 @@ func TestServeReturnsWhenLastSessionExits(t *testing.T) { | |||
| 399 | 399 | tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24})) | |
| 400 | 400 | ||
| 401 | 401 | l := newMockServerListener(t) | |
| 402 | - | connCh := make(chan ports.Transport, 1) | |
| 402 | + | connCh := make(chan wire.Transport, 1) | |
| 403 | 403 | connCh <- tr | |
| 404 | 404 | closed := make(chan struct{}) | |
| 405 | 405 | var once sync.Once | |
| 406 | - | l.EXPECT().Accept().RunAndReturn(func() (ports.Transport, error) { | |
| 406 | + | l.EXPECT().Accept().RunAndReturn(func() (wire.Transport, error) { | |
| 407 | 407 | select { | |
| 408 | 408 | case c := <-connCh: | |
| 409 | 409 | return c, nil | |
| @@ -439,11 +439,11 @@ func TestServeGracefulShutdownOnContextCancel(t *testing.T) { | |||
| 439 | 439 | tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24})) | |
| 440 | 440 | ||
| 441 | 441 | l := newMockServerListener(t) | |
| 442 | - | connCh := make(chan ports.Transport, 1) | |
| 442 | + | connCh := make(chan wire.Transport, 1) | |
| 443 | 443 | connCh <- tr | |
| 444 | 444 | closed := make(chan struct{}) | |
| 445 | 445 | var once sync.Once | |
| 446 | - | l.EXPECT().Accept().RunAndReturn(func() (ports.Transport, error) { | |
| 446 | + | l.EXPECT().Accept().RunAndReturn(func() (wire.Transport, error) { | |
| 447 | 447 | select { | |
| 448 | 448 | case c := <-connCh: | |
| 449 | 449 | return c, nil | |
| @@ -601,11 +601,11 @@ func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) { | |||
| 601 | 601 | tr.EXPECT().Close().RunAndReturn(func() error { closeConn(); return nil }).Maybe() | |
| 602 | 602 | ||
| 603 | 603 | l := newMockServerListener(t) | |
| 604 | - | connCh := make(chan ports.Transport, 1) | |
| 604 | + | connCh := make(chan wire.Transport, 1) | |
| 605 | 605 | connCh <- tr | |
| 606 | 606 | lnClosed := make(chan struct{}) | |
| 607 | 607 | var lnOnce sync.Once | |
| 608 | - | l.EXPECT().Accept().RunAndReturn(func() (ports.Transport, error) { | |
| 608 | + | l.EXPECT().Accept().RunAndReturn(func() (wire.Transport, error) { | |
| 609 | 609 | select { | |
| 610 | 610 | case c := <-connCh: | |
| 611 | 611 | return c, nil | |
M internal/usecase/daemon/snapshot_scheduler_test.go +3 -3
| @@ -613,14 +613,14 @@ func (c *serveShutdownClock) nextFinalTimer(t *testing.T) *snapshotDeadlineTimer | |||
| 613 | 613 | } | |
| 614 | 614 | } | |
| 615 | 615 | ||
| 616 | - | func serveSnapshotListener(t *testing.T, tr ports.Transport) *mockServerListener { | |
| 616 | + | func serveSnapshotListener(t *testing.T, tr wire.Transport) *mockServerListener { | |
| 617 | 617 | t.Helper() | |
| 618 | 618 | l := newMockServerListener(t) | |
| 619 | - | connections := make(chan ports.Transport, 1) | |
| 619 | + | connections := make(chan wire.Transport, 1) | |
| 620 | 620 | connections <- tr | |
| 621 | 621 | closed := make(chan struct{}) | |
| 622 | 622 | var once sync.Once | |
| 623 | - | l.EXPECT().Accept().RunAndReturn(func() (ports.Transport, error) { | |
| 623 | + | l.EXPECT().Accept().RunAndReturn(func() (wire.Transport, error) { | |
| 624 | 624 | select { | |
| 625 | 625 | case conn := <-connections: | |
| 626 | 626 | return conn, nil | |
M internal/usecase/daemon/transport_observability_test.go +2 -1
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "testing" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/vev/internal/adapters/observability" | |
| 10 | 11 | "github.com/bnema/vev/internal/domain" | |
| 11 | 12 | "github.com/bnema/vev/internal/ports" | |
| 12 | 13 | "github.com/bnema/vev/internal/protocol/wire" | |
| @@ -233,7 +234,7 @@ func TestTransportObservabilityBlockedACKEndDoesNotDelayNotifyAck(t *testing.T) | |||
| 233 | 234 | endEntered: make(chan struct{}), | |
| 234 | 235 | releaseEnd: make(chan struct{}), | |
| 235 | 236 | } | |
| 236 | - | reporter := ports.NewSerializedRuntimeObserver(observer, 64) | |
| 237 | + | reporter := observability.NewSerialized(observer, 64) | |
| 237 | 238 | defer reporter.Close() | |
| 238 | 239 | d := New(nil, nil, nil, WithRuntimeObserver(reporter)) | |
| 239 | 240 | wakes := make(chan renderWake, 1) | |
M internal/usecase/daemon/typed_connection_test.go +23 -23
| @@ -7,9 +7,9 @@ import ( | |||
| 7 | 7 | "github.com/stretchr/testify/mock" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/ports" | |
| 10 | - | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 11 | 10 | "github.com/bnema/vev/internal/protocol" | |
| 12 | 11 | "github.com/bnema/vev/internal/protocol/wire" | |
| 12 | + | wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | 15 | type mockTestingT interface { | |
| @@ -17,10 +17,10 @@ type mockTestingT interface { | |||
| 17 | 17 | Cleanup(func()) | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | type mockServerConnection struct{ *portsmocks.MockTransport } | |
| 20 | + | type mockServerConnection struct{ *wiremocks.MockTransport } | |
| 21 | 21 | ||
| 22 | 22 | func newMockServerConnection(t mockTestingT) *mockServerConnection { | |
| 23 | - | return &mockServerConnection{MockTransport: portsmocks.NewMockTransport(t)} | |
| 23 | + | return &mockServerConnection{MockTransport: wiremocks.NewMockTransport(t)} | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | func (c *mockServerConnection) ReceiveClient() (protocol.ClientMessage, error) { | |
| @@ -52,7 +52,7 @@ func (c *mockServerConnection) LinkEvents() <-chan ports.LinkEvent { | |||
| 52 | 52 | return testLinkEvents(c.MockTransport) | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | - | type rawServerConnection struct{ raw ports.Transport } | |
| 55 | + | type rawServerConnection struct{ raw wire.Transport } | |
| 56 | 56 | ||
| 57 | 57 | func (c *rawServerConnection) ReceiveClient() (protocol.ClientMessage, error) { | |
| 58 | 58 | return testReceiveClient(c.raw) | |
| @@ -82,10 +82,10 @@ func (c *rawServerConnection) LinkState() ports.LinkState { return testL | |||
| 82 | 82 | func (c *rawServerConnection) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(c.raw) } | |
| 83 | 83 | func (c *rawServerConnection) Close() error { return c.raw.Close() } | |
| 84 | 84 | ||
| 85 | - | type mockServerListener struct{ *portsmocks.MockListener } | |
| 85 | + | type mockServerListener struct{ *wiremocks.MockListener } | |
| 86 | 86 | ||
| 87 | 87 | func newMockServerListener(t mockTestingT) *mockServerListener { | |
| 88 | - | return &mockServerListener{MockListener: portsmocks.NewMockListener(t)} | |
| 88 | + | return &mockServerListener{MockListener: wiremocks.NewMockListener(t)} | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | 91 | func (l *mockServerListener) Accept() (ports.ServerConnection, error) { | |
| @@ -96,7 +96,7 @@ func (l *mockServerListener) Accept() (ports.ServerConnection, error) { | |||
| 96 | 96 | return &rawServerConnection{raw: raw}, nil | |
| 97 | 97 | } | |
| 98 | 98 | ||
| 99 | - | func testReceiveClient(raw ports.Transport) (protocol.ClientMessage, error) { | |
| 99 | + | func testReceiveClient(raw wire.Transport) (protocol.ClientMessage, error) { | |
| 100 | 100 | frame, err := raw.Recv() | |
| 101 | 101 | if err != nil { | |
| 102 | 102 | return nil, err | |
| @@ -166,7 +166,7 @@ func testDecodeClientFrame(frame wire.Frame) (protocol.ClientMessage, error) { | |||
| 166 | 166 | } | |
| 167 | 167 | } | |
| 168 | 168 | ||
| 169 | - | func testSendServer(raw ports.Transport, message protocol.ServerMessage) error { | |
| 169 | + | func testSendServer(raw wire.Transport, message protocol.ServerMessage) error { | |
| 170 | 170 | frame, err := testServerFrame(message) | |
| 171 | 171 | if err != nil { | |
| 172 | 172 | return err | |
| @@ -174,8 +174,8 @@ func testSendServer(raw ports.Transport, message protocol.ServerMessage) error { | |||
| 174 | 174 | return raw.Send(frame) | |
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | - | func testSendServerAsync(raw ports.Transport, message protocol.ServerMessage) error { | |
| 178 | - | transport, ok := raw.(ports.AsyncTransport) | |
| 177 | + | func testSendServerAsync(raw wire.Transport, message protocol.ServerMessage) error { | |
| 178 | + | transport, ok := raw.(wire.AsyncTransport) | |
| 179 | 179 | if !ok { | |
| 180 | 180 | return errors.New("test server connection: async unsupported") | |
| 181 | 181 | } | |
| @@ -186,8 +186,8 @@ func testSendServerAsync(raw ports.Transport, message protocol.ServerMessage) er | |||
| 186 | 186 | return transport.SendAsync(frame) | |
| 187 | 187 | } | |
| 188 | 188 | ||
| 189 | - | func testSendServerSynchronous(raw ports.Transport, message protocol.ServerMessage) error { | |
| 190 | - | transport, ok := raw.(ports.OwnedSynchronousTransport) | |
| 189 | + | func testSendServerSynchronous(raw wire.Transport, message protocol.ServerMessage) error { | |
| 190 | + | transport, ok := raw.(wire.OwnedSynchronousTransport) | |
| 191 | 191 | if !ok { | |
| 192 | 192 | return errors.New("test server connection: synchronous unsupported") | |
| 193 | 193 | } | |
| @@ -198,7 +198,7 @@ func testSendServerSynchronous(raw ports.Transport, message protocol.ServerMessa | |||
| 198 | 198 | return transport.SendSynchronous(frame) | |
| 199 | 199 | } | |
| 200 | 200 | ||
| 201 | - | func testSendOutput(raw ports.Transport, output protocol.Output) error { | |
| 201 | + | func testSendOutput(raw wire.Transport, output protocol.Output) error { | |
| 202 | 202 | frame, err := testServerFrame(output) | |
| 203 | 203 | if err != nil { | |
| 204 | 204 | return err | |
| @@ -206,8 +206,8 @@ func testSendOutput(raw ports.Transport, output protocol.Output) error { | |||
| 206 | 206 | return raw.Send(frame) | |
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | - | func testSendOutputAsync(raw ports.Transport, output protocol.Output) error { | |
| 210 | - | transport, ok := raw.(ports.AsyncTransport) | |
| 209 | + | func testSendOutputAsync(raw wire.Transport, output protocol.Output) error { | |
| 210 | + | transport, ok := raw.(wire.AsyncTransport) | |
| 211 | 211 | if !ok { | |
| 212 | 212 | return errors.New("test server connection: async unsupported") | |
| 213 | 213 | } | |
| @@ -218,8 +218,8 @@ func testSendOutputAsync(raw ports.Transport, output protocol.Output) error { | |||
| 218 | 218 | return transport.SendAsync(frame) | |
| 219 | 219 | } | |
| 220 | 220 | ||
| 221 | - | func testSendOutputSynchronous(raw ports.Transport, output protocol.Output) error { | |
| 222 | - | transport, ok := raw.(ports.OwnedSynchronousTransport) | |
| 221 | + | func testSendOutputSynchronous(raw wire.Transport, output protocol.Output) error { | |
| 222 | + | transport, ok := raw.(wire.OwnedSynchronousTransport) | |
| 223 | 223 | if !ok { | |
| 224 | 224 | return errors.New("test server connection: synchronous unsupported") | |
| 225 | 225 | } | |
| @@ -230,10 +230,10 @@ func testSendOutputSynchronous(raw ports.Transport, output protocol.Output) erro | |||
| 230 | 230 | return transport.SendSynchronous(frame) | |
| 231 | 231 | } | |
| 232 | 232 | ||
| 233 | - | func testServerCapabilities(raw ports.Transport) protocol.ConnectionCapabilities { | |
| 234 | - | _, datagram := raw.(ports.DatagramTransport) | |
| 235 | - | _, async := raw.(ports.AsyncTransport) | |
| 236 | - | _, synchronous := raw.(ports.OwnedSynchronousTransport) | |
| 233 | + | func testServerCapabilities(raw wire.Transport) protocol.ConnectionCapabilities { | |
| 234 | + | _, datagram := raw.(wire.DatagramTransport) | |
| 235 | + | _, async := raw.(wire.AsyncTransport) | |
| 236 | + | _, synchronous := raw.(wire.OwnedSynchronousTransport) | |
| 237 | 237 | _, link := raw.(ports.LinkStateReporter) | |
| 238 | 238 | window := uint8(protocol.MaxOutputWindow) | |
| 239 | 239 | if datagram { | |
| @@ -242,14 +242,14 @@ func testServerCapabilities(raw ports.Transport) protocol.ConnectionCapabilities | |||
| 242 | 242 | return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, AsyncSend: async, OwnedSynchronousSend: synchronous, LinkState: link} | |
| 243 | 243 | } | |
| 244 | 244 | ||
| 245 | - | func testLinkState(raw ports.Transport) ports.LinkState { | |
| 245 | + | func testLinkState(raw wire.Transport) ports.LinkState { | |
| 246 | 246 | if reporter, ok := raw.(ports.LinkStateReporter); ok { | |
| 247 | 247 | return reporter.LinkState() | |
| 248 | 248 | } | |
| 249 | 249 | return ports.LinkStateConnected | |
| 250 | 250 | } | |
| 251 | 251 | ||
| 252 | - | func testLinkEvents(raw ports.Transport) <-chan ports.LinkEvent { | |
| 252 | + | func testLinkEvents(raw wire.Transport) <-chan ports.LinkEvent { | |
| 253 | 253 | if reporter, ok := raw.(ports.LinkStateReporter); ok { | |
| 254 | 254 | return reporter.LinkEvents() | |
| 255 | 255 | } | |
M scripts/remote-picker-harness/main.go +23 -22
| @@ -21,6 +21,7 @@ import ( | |||
| 21 | 21 | "github.com/bnema/vev/internal/domain" | |
| 22 | 22 | "github.com/bnema/vev/internal/ports" | |
| 23 | 23 | "github.com/bnema/vev/internal/protocol" | |
| 24 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 24 | 25 | "github.com/bnema/vev/internal/protocol/wire" | |
| 25 | 26 | ) | |
| 26 | 27 | ||
| @@ -82,7 +83,7 @@ func run() (runErr error) { | |||
| 82 | 83 | } | |
| 83 | 84 | ||
| 84 | 85 | func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteCatalogClient, preview ports.RemotePreviewClient) error { | |
| 85 | - | stdio, err := openTransport(ctx, ports.RemoteTransportStdio, target) | |
| 86 | + | stdio, err := openTransport(ctx, remoteadapter.TransportStdio, target) | |
| 86 | 87 | if err != nil { | |
| 87 | 88 | return fmt.Errorf("open stdio transport: %w", err) | |
| 88 | 89 | } | |
| @@ -114,7 +115,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC | |||
| 114 | 115 | if err != nil { | |
| 115 | 116 | return fmt.Errorf("multi-tab catalog: %w", err) | |
| 116 | 117 | } | |
| 117 | - | beforeTabs := ports.CatalogTabs(liveWithTabs) | |
| 118 | + | beforeTabs := catalogue.CatalogTabs(liveWithTabs) | |
| 118 | 119 | if err := sendCommand(ctx, stdio, "close-tab"); err != nil { | |
| 119 | 120 | return fmt.Errorf("remove tab: %w", err) | |
| 120 | 121 | } | |
| @@ -122,7 +123,7 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC | |||
| 122 | 123 | if err != nil { | |
| 123 | 124 | return fmt.Errorf("removed-tab catalog: %w", err) | |
| 124 | 125 | } | |
| 125 | - | removedID, ok := removedCatalogTabID(beforeTabs, ports.CatalogTabs(after)) | |
| 126 | + | removedID, ok := removedCatalogTabID(beforeTabs, catalogue.CatalogTabs(after)) | |
| 126 | 127 | if !ok { | |
| 127 | 128 | return errors.New("removed-tab catalog did not identify exactly one removed tab") | |
| 128 | 129 | } | |
| @@ -161,7 +162,7 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports | |||
| 161 | 162 | inputMarker = "VEV_REMOTE_PICKER_INPUT_OK" | |
| 162 | 163 | ) | |
| 163 | 164 | ||
| 164 | - | remote, err := openTransport(ctx, ports.RemoteTransportStdio, target) | |
| 165 | + | remote, err := openTransport(ctx, remoteadapter.TransportStdio, target) | |
| 165 | 166 | if err != nil { | |
| 166 | 167 | return fmt.Errorf("open remote picker source: %w", err) | |
| 167 | 168 | } | |
| @@ -201,7 +202,7 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports | |||
| 201 | 202 | if remoteCatalog.ActiveTabID == "" { | |
| 202 | 203 | return errors.New("remote picker catalog omitted active tab") | |
| 203 | 204 | } | |
| 204 | - | tabs := ports.CatalogTabs(remoteCatalog) | |
| 205 | + | tabs := catalogue.CatalogTabs(remoteCatalog) | |
| 205 | 206 | if len(tabs) != 2 { | |
| 206 | 207 | return errors.New("remote picker catalog did not retain both tabs") | |
| 207 | 208 | } | |
| @@ -347,7 +348,7 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem | |||
| 347 | 348 | if err != nil { | |
| 348 | 349 | return domain.RemoteSessionTarget{}, fmt.Errorf("resumed target: %w", err) | |
| 349 | 350 | } | |
| 350 | - | resumed, err := openTransport(ctx, ports.RemoteTransportStdio, target) | |
| 351 | + | resumed, err := openTransport(ctx, remoteadapter.TransportStdio, target) | |
| 351 | 352 | if err != nil { | |
| 352 | 353 | return domain.RemoteSessionTarget{}, fmt.Errorf("open restart stdio transport: %w", err) | |
| 353 | 354 | } | |
| @@ -381,7 +382,7 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem | |||
| 381 | 382 | } | |
| 382 | 383 | ||
| 383 | 384 | func runUDPPhase(ctx context.Context, target string) error { | |
| 384 | - | udp, err := openTransport(ctx, ports.RemoteTransportUDP, target) | |
| 385 | + | udp, err := openTransport(ctx, remoteadapter.TransportUDP, target) | |
| 385 | 386 | if err != nil { | |
| 386 | 387 | return fmt.Errorf("open UDP transport: %w", err) | |
| 387 | 388 | } | |
| @@ -418,7 +419,7 @@ func runStaleTargetPhase(ctx context.Context, target string, catalog ports.Remot | |||
| 418 | 419 | if err := waitForMissingSession(ctx, catalog, target, sessionName); err != nil { | |
| 419 | 420 | return fmt.Errorf("wait for destroyed session: %w", err) | |
| 420 | 421 | } | |
| 421 | - | stale, err := openTransport(ctx, ports.RemoteTransportStdio, target) | |
| 422 | + | stale, err := openTransport(ctx, remoteadapter.TransportStdio, target) | |
| 422 | 423 | if err != nil { | |
| 423 | 424 | return fmt.Errorf("open stale-target transport: %w", err) | |
| 424 | 425 | } | |
| @@ -429,7 +430,7 @@ func runStaleTargetPhase(ctx context.Context, target string, catalog ports.Remot | |||
| 429 | 430 | return nil | |
| 430 | 431 | } | |
| 431 | 432 | ||
| 432 | - | func removedCatalogTabID(before, after []ports.RemoteCatalogTab) (string, bool) { | |
| 433 | + | func removedCatalogTabID(before, after []catalogue.RemoteCatalogTab) (string, bool) { | |
| 433 | 434 | remaining := make(map[string]struct{}, len(after)) | |
| 434 | 435 | for _, tab := range after { | |
| 435 | 436 | remaining[tab.ID] = struct{}{} | |
| @@ -464,7 +465,7 @@ type harnessFrame struct { | |||
| 464 | 465 | } | |
| 465 | 466 | ||
| 466 | 467 | type harnessTransport struct { | |
| 467 | - | ports.Transport | |
| 468 | + | wire.Transport | |
| 468 | 469 | probe *visualProbe | |
| 469 | 470 | frames chan harnessFrame | |
| 470 | 471 | queued []harnessFrame | |
| @@ -473,7 +474,7 @@ type harnessTransport struct { | |||
| 473 | 474 | closeErr error | |
| 474 | 475 | } | |
| 475 | 476 | ||
| 476 | - | func newHarnessTransport(transport ports.Transport) *harnessTransport { | |
| 477 | + | func newHarnessTransport(transport wire.Transport) *harnessTransport { | |
| 477 | 478 | h := &harnessTransport{ | |
| 478 | 479 | Transport: transport, | |
| 479 | 480 | probe: newVisualProbe(domain.Size{Cols: defaultProbeCols, Rows: defaultProbeRows}), | |
| @@ -506,13 +507,13 @@ func (h *harnessTransport) Close() error { | |||
| 506 | 507 | return h.closeErr | |
| 507 | 508 | } | |
| 508 | 509 | ||
| 509 | - | func openTransport(ctx context.Context, mode ports.RemoteTransportMode, target string) (*harnessTransport, error) { | |
| 510 | - | var transport ports.Transport | |
| 510 | + | func openTransport(ctx context.Context, mode remoteadapter.TransportMode, target string) (*harnessTransport, error) { | |
| 511 | + | var transport wire.Transport | |
| 511 | 512 | var err error | |
| 512 | 513 | switch mode { | |
| 513 | - | case ports.RemoteTransportStdio: | |
| 514 | + | case remoteadapter.TransportStdio: | |
| 514 | 515 | transport, err = sshstdio.DialContext(ctx, target, "", slog.New(slog.DiscardHandler)) | |
| 515 | - | case ports.RemoteTransportUDP: | |
| 516 | + | case remoteadapter.TransportUDP: | |
| 516 | 517 | dialer := dgram.NewRemoteDialerWithLogger(target, "", slog.New(slog.DiscardHandler)) | |
| 517 | 518 | dialer.BootstrapTimeout = 8 * time.Second | |
| 518 | 519 | dialer.ProbeTimeout = 3 * time.Second | |
| @@ -893,8 +894,8 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain | |||
| 893 | 894 | return nil | |
| 894 | 895 | } | |
| 895 | 896 | ||
| 896 | - | func targetForSession(endpoint string, session ports.RemoteCatalogSession, stopped bool) (domain.RemoteSessionTarget, error) { | |
| 897 | - | tabs := ports.CatalogTabs(session) | |
| 897 | + | func targetForSession(endpoint string, session catalogue.RemoteCatalogSession, stopped bool) (domain.RemoteSessionTarget, error) { | |
| 898 | + | tabs := catalogue.CatalogTabs(session) | |
| 898 | 899 | if len(tabs) == 0 { | |
| 899 | 900 | return domain.RemoteSessionTarget{}, errors.New("catalog session has no tabs") | |
| 900 | 901 | } | |
| @@ -932,18 +933,18 @@ func targetEndpoint(endpoint string) string { | |||
| 932 | 933 | return endpoint | |
| 933 | 934 | } | |
| 934 | 935 | ||
| 935 | - | func waitForSession(ctx context.Context, catalog ports.RemoteCatalogClient, endpoint, name string, state ports.RemoteCatalogSessionState) (ports.RemoteCatalogSession, error) { | |
| 936 | + | func waitForSession(ctx context.Context, catalog ports.RemoteCatalogClient, endpoint, name string, state catalogue.RemoteCatalogSessionState) (catalogue.RemoteCatalogSession, error) { | |
| 936 | 937 | return waitForSessionWithTabCount(ctx, catalog, endpoint, name, state, 0) | |
| 937 | 938 | } | |
| 938 | 939 | ||
| 939 | - | func waitForSessionWithTabCount(ctx context.Context, catalog ports.RemoteCatalogClient, endpoint, name string, state ports.RemoteCatalogSessionState, tabs int) (ports.RemoteCatalogSession, error) { | |
| 940 | + | func waitForSessionWithTabCount(ctx context.Context, catalog ports.RemoteCatalogClient, endpoint, name string, state catalogue.RemoteCatalogSessionState, tabs int) (catalogue.RemoteCatalogSession, error) { | |
| 940 | 941 | deadline, cancel := context.WithTimeout(ctx, 10*time.Second) | |
| 941 | 942 | defer cancel() | |
| 942 | 943 | for { | |
| 943 | 944 | result, err := catalog.List(deadline, endpoint) | |
| 944 | 945 | if err == nil { | |
| 945 | 946 | for _, session := range result.Sessions { | |
| 946 | - | if session.Name == name && session.State == state && (tabs == 0 || len(ports.CatalogTabs(session)) == tabs) { | |
| 947 | + | if session.Name == name && session.State == state && (tabs == 0 || len(catalogue.CatalogTabs(session)) == tabs) { | |
| 947 | 948 | return session, nil | |
| 948 | 949 | } | |
| 949 | 950 | } | |
| @@ -951,9 +952,9 @@ func waitForSessionWithTabCount(ctx context.Context, catalog ports.RemoteCatalog | |||
| 951 | 952 | select { | |
| 952 | 953 | case <-deadline.Done(): | |
| 953 | 954 | if err != nil { | |
| 954 | - | return ports.RemoteCatalogSession{}, err | |
| 955 | + | return catalogue.RemoteCatalogSession{}, err | |
| 955 | 956 | } | |
| 956 | - | return ports.RemoteCatalogSession{}, fmt.Errorf("session %q did not reach state %q", name, state) | |
| 957 | + | return catalogue.RemoteCatalogSession{}, fmt.Errorf("session %q did not reach state %q", name, state) | |
| 957 | 958 | case <-time.After(100 * time.Millisecond): | |
| 958 | 959 | } | |
| 959 | 960 | } | |
M transport_replay_integration_test.go +3 -4
| @@ -10,7 +10,6 @@ import ( | |||
| 10 | 10 | renderer "github.com/bnema/vev-vt/ansi" | |
| 11 | 11 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 12 | 12 | "github.com/bnema/vev/internal/adapters/sshstdio" | |
| 13 | - | "github.com/bnema/vev/internal/ports" | |
| 14 | 13 | "github.com/bnema/vev/internal/protocol" | |
| 15 | 14 | "github.com/bnema/vev/internal/protocol/wire" | |
| 16 | 15 | "github.com/bnema/vev/internal/testutil/replaytest" | |
| @@ -68,11 +67,11 @@ func TestThemeGenerationTransportSequences(t *testing.T) { | |||
| 68 | 67 | ||
| 69 | 68 | tests := []struct { | |
| 70 | 69 | name string | |
| 71 | - | pair func(*testing.T) (ports.Transport, ports.Transport) | |
| 70 | + | pair func(*testing.T) (wire.Transport, wire.Transport) | |
| 72 | 71 | }{ | |
| 73 | 72 | { | |
| 74 | 73 | name: "local ipc socket", | |
| 75 | - | pair: func(t *testing.T) (ports.Transport, ports.Transport) { | |
| 74 | + | pair: func(t *testing.T) (wire.Transport, wire.Transport) { | |
| 76 | 75 | left, right := net.Pipe() | |
| 77 | 76 | t.Cleanup(func() { _ = left.Close(); _ = right.Close() }) | |
| 78 | 77 | return ipc.NewTransport(left), ipc.NewTransport(right) | |
| @@ -80,7 +79,7 @@ func TestThemeGenerationTransportSequences(t *testing.T) { | |||
| 80 | 79 | }, | |
| 81 | 80 | { | |
| 82 | 81 | name: "remote ssh stdio", | |
| 83 | - | pair: func(t *testing.T) (ports.Transport, ports.Transport) { | |
| 82 | + | pair: func(t *testing.T) (wire.Transport, wire.Transport) { | |
| 84 | 83 | clientRead, serverWrite := io.Pipe() | |
| 85 | 84 | serverRead, clientWrite := io.Pipe() | |
| 86 | 85 | closeClient := func() error { | |