Adds the Phase B paliad-side migration: a thin HTTP client of the centralized aichat backend shipped in m/mAi#207 Phase A (darwin's mai/darwin/issue-207-aichat branch). Implements the same services.Paliadin interface as LocalPaliadinService / RemotePaliadinService — handler plumbing is unchanged, the cutover is a single env-var flip. internal/services/aichat_paliadin.go (~530 LoC): - POST /chat/turn + POST /chat/reset + GET /chat/health via the aichat JSON envelope (mirrors m/mAi internal/aichat/api/types.go verbatim; no module import to keep paliad self-contained). - Per-turn HS256 JWT mint (uses paliadin_jwt.go from the prior commit) when SUPABASE_JWT_SECRET is configured. Aichat owns file write + cleanup; we just sign and ship. - Service-wide health-gate cache (10 s success window, no failure cache — failures re-probe so recovery surfaces immediately). - Per-user-window primer cache. Pulls up to MaxPrimerTurns prior exchanges from paliad.paliadin_turns and ships them in TurnRequest. Primer so a pane respawn (pane_spawned=true in response) doesn't strand the user with a cold claude. Cleared on ResetSession + pane_spawned response. - Username from email_localpart per m's §13 Q2 pick (sanitized inside aichat). Nil-DB fallback: "user-<uuid8>". - Maps aichat's typed wire errors (auth_failed, persona_unknown, mriver_unreachable, bootstrap_failed, timeout, shim_error) onto paliad's existing audit-row codes — preserves the German i18n table in paliadin.ts unchanged (no new strings needed per design §11). cmd/server/main.go: - PALIADIN_BACKEND env: "aichat" → AichatPaliadinService, anything else → existing remote/local/disabled tree. Default = legacy, so every existing deploy is byte-identical until flipped. - buildAichatPaliadinConfig validates AICHAT_URL + AICHAT_TOKEN at boot; AICHAT_PERSONA defaults to "paliadin". JWT secret threaded in so per-user RLS is on by default. Tests cover constructor defaults, health-gate caching + retry + expiry, ResetSession wiring, error-envelope decoding + classifier, HTTP-layer auth/JSON wiring via a roundTripper, JWT mint integration, TurnContext → meta packing, and the env-gate helper. go test ./... green. NOT self-merged — head owns the merge per task instructions.
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestBuildAichatPaliadinConfig pins the env-driven wiring used by the
|
|
// PALIADIN_BACKEND=aichat path in main(). It guards three things:
|
|
//
|
|
// 1. Required vars (AICHAT_URL, AICHAT_TOKEN) must be set — otherwise
|
|
// boot fails fast with a clear error message.
|
|
// 2. AICHAT_PERSONA defaults to "paliadin" so a misconfigured deploy
|
|
// doesn't silently route to a different persona.
|
|
// 3. The JWT secret threads through so per-turn JWT mint is on by
|
|
// default (folded-in t-paliad-156 work).
|
|
//
|
|
// We can't unit-test the switch{} block in main() directly without
|
|
// invoking the rest of boot, so this test exercises the helper that
|
|
// branch calls — the same surface a Phase B regression would hit.
|
|
func TestBuildAichatPaliadinConfig(t *testing.T) {
|
|
t.Run("rejects empty URL", func(t *testing.T) {
|
|
t.Setenv("AICHAT_URL", "")
|
|
t.Setenv("AICHAT_TOKEN", "tok")
|
|
_, err := buildAichatPaliadinConfig("secret")
|
|
if err == nil || !strings.Contains(err.Error(), "AICHAT_URL") {
|
|
t.Errorf("err = %v; want AICHAT_URL complaint", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects empty token", func(t *testing.T) {
|
|
t.Setenv("AICHAT_URL", "http://aichat.test")
|
|
t.Setenv("AICHAT_TOKEN", "")
|
|
_, err := buildAichatPaliadinConfig("secret")
|
|
if err == nil || !strings.Contains(err.Error(), "AICHAT_TOKEN") {
|
|
t.Errorf("err = %v; want AICHAT_TOKEN complaint", err)
|
|
}
|
|
})
|
|
|
|
t.Run("defaults persona to paliadin", func(t *testing.T) {
|
|
t.Setenv("AICHAT_URL", "http://aichat.test/")
|
|
t.Setenv("AICHAT_TOKEN", "tok")
|
|
t.Setenv("AICHAT_PERSONA", "")
|
|
cfg, err := buildAichatPaliadinConfig("secret")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if cfg.Persona != "paliadin" {
|
|
t.Errorf("persona = %q; want paliadin", cfg.Persona)
|
|
}
|
|
if cfg.BaseURL != "http://aichat.test" {
|
|
t.Errorf("base url trailing slash leaked: %q", cfg.BaseURL)
|
|
}
|
|
if string(cfg.JWTSecret) != "secret" {
|
|
t.Errorf("JWT secret not threaded; got %q", string(cfg.JWTSecret))
|
|
}
|
|
if cfg.BearerToken != "tok" {
|
|
t.Errorf("BearerToken = %q; want tok", cfg.BearerToken)
|
|
}
|
|
})
|
|
|
|
t.Run("honours AICHAT_PERSONA override", func(t *testing.T) {
|
|
t.Setenv("AICHAT_URL", "http://aichat.test")
|
|
t.Setenv("AICHAT_TOKEN", "tok")
|
|
t.Setenv("AICHAT_PERSONA", "custom-paliadin")
|
|
cfg, err := buildAichatPaliadinConfig("secret")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if cfg.Persona != "custom-paliadin" {
|
|
t.Errorf("persona = %q; want custom-paliadin", cfg.Persona)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRLSModeLabel(t *testing.T) {
|
|
if got := rlsModeLabel(nil); got != "service-role" {
|
|
t.Errorf("nil → %q; want service-role", got)
|
|
}
|
|
if got := rlsModeLabel([]byte{}); got != "service-role" {
|
|
t.Errorf("empty → %q; want service-role", got)
|
|
}
|
|
if got := rlsModeLabel([]byte("x")); got != "per-user" {
|
|
t.Errorf("non-empty → %q; want per-user", got)
|
|
}
|
|
}
|