Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md) must give the use-cases-to-be their contract surface: driven-port interfaces, test fakes, the secrets interface moved into core, and the postgres package inside the adapters tree — before the first vertical slice (Phase 3) can wire a composition root. Change: - internal/core/ports: full driven-port catalog per plan §3.3 — repositories as transaction-scoped aggregates whose inputs carry derived checks, audit, and events (§3.6), plus CommandExecutor, TargetResolver, Checker, Secrets, EventPublisher, Provisioner. Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry, ExecResult) keep signatures off infrastructure; TypeTree aliases internal/ontology (pure over domain) until checkdefaults is absorbed. ReadModels intentionally not declared yet — it materializes with the Phase 3 slice and grows as report handlers rewire. - secrets.Backend is now an alias of ports.Secrets; implementations (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend subset is deleted; tool constructors take ports.Secrets. - internal/db → internal/adapters/postgres (mechanical import rewrite; package identifier stays db until the Phase 3 repository split). sqlc.yaml, Makefile, golangci exclusions, and docs follow the move; make generate-check verified. - internal/adapters/ssh: Executor implements ports.CommandExecutor over the actuator dial pool + RunStreaming (10-min default timeout carried over from the httpapi path). - internal/adapters/remote: Resolver implements ports.TargetResolver delegating to internal/remote (still pool-based; drops onto ports.EntityRepository when repositories land in Phase 3 — documented transitional import). - internal/core/ports/portstest: importable fakes — in-memory EntityRepo (with check-then-act SetState, side-effect recording), RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction guards; tests. Risk: ports are declared ahead of implementations — signatures firm up per phase as slices land (documented in the package doc); the remote→postgres transitional import is explicit and dissolves in Phase 3. Verification: go vet, make test (race, 19 packages), generate-check, golangci on core+adapters — 0 issues; full-repo baseline down 365→344.
56 lines
2.1 KiB
Go
56 lines
2.1 KiB
Go
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// attrTruthy replaces a previous strings.Contains check over raw JSONB text.
|
|
// The key regression it guards: a literal attribute like
|
|
// {"backups_verified": false} must NOT satisfy the "backups-verified"
|
|
// precondition, even though the key text is present in the column.
|
|
func TestAttrTruthy(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
attrs map[string]any
|
|
key string
|
|
want bool
|
|
}{
|
|
{"absent", map[string]any{}, "backups_verified", false},
|
|
{"nil map", nil, "backups_verified", false},
|
|
{"explicit nil value", map[string]any{"backups_verified": nil}, "backups_verified", false},
|
|
{"bool true", map[string]any{"backups_verified": true}, "backups_verified", true},
|
|
{"bool false is the regression case", map[string]any{"backups_verified": false}, "backups_verified", false},
|
|
{"nonempty string age pubkey", map[string]any{"age_pubkey": "age1abc"}, "age_pubkey", true},
|
|
{"empty string is falsy", map[string]any{"mesh_ip": ""}, "mesh_ip", false},
|
|
{"number counts as present", map[string]any{"port": float64(22)}, "port", true},
|
|
{"other keys present", map[string]any{"backups_verified": true, "unrelated": "x"}, "backups_verified", true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := attrTruthy(tc.attrs, tc.key); got != tc.want {
|
|
t.Fatalf("attrTruthy(%v, %q) = %v, want %v", tc.attrs, tc.key, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// fetchAttrs decodes the JSONB column text; verify the decode shape that
|
|
// attrTruthy then evaluates (the DB round-trip itself is covered by make test-db).
|
|
func TestAttrTruthyAfterDecode(t *testing.T) {
|
|
raw := `{"backups_verified": true, "mesh_ip": "10.0.0.5", "secrets_revoked": false}`
|
|
var got map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &got); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if !attrTruthy(got, "backups_verified") {
|
|
t.Error("backups_verified should be truthy after decode")
|
|
}
|
|
if !attrTruthy(got, "mesh_ip") {
|
|
t.Error("mesh_ip should be truthy after decode")
|
|
}
|
|
if attrTruthy(got, "secrets_revoked") {
|
|
t.Error("secrets_revoked:false is the regression — must be falsy")
|
|
}
|
|
}
|