mirror of
https://github.com/hanzoai/llm-toolchain.git
synced 2026-10-30 18:29:08 +00:00
454 lines
14 KiB
Go
454 lines
14 KiB
Go
package server_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
basetests "github.com/hanzoai/base/tests"
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
zcap "github.com/zap-proto/go/cap"
|
|
|
|
gen "github.com/hanzoai/llm-toolchain/gen"
|
|
"github.com/hanzoai/llm-toolchain/server"
|
|
)
|
|
|
|
const testOrg = "test-org"
|
|
|
|
// testMasterKey is a fixed 32-byte master KEK for the suite — the SecretBox is
|
|
// deterministic given (master, org), so Seal/Open round-trips within a test.
|
|
var testMasterKey = []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
// newService spins up a Base test app with the four collections provisioned, a
|
|
// SecretBox, a ZAP router node listening, and returns the listen address, node
|
|
// id, and a cleanup func.
|
|
func newService(t *testing.T, port int) (addr, peerID string, cleanup func()) {
|
|
t.Helper()
|
|
|
|
app, err := basetests.NewTestApp()
|
|
if err != nil {
|
|
t.Fatalf("new test app: %v", err)
|
|
}
|
|
if err := server.EnsureCollections(app); err != nil {
|
|
t.Fatalf("ensure collections: %v", err)
|
|
}
|
|
secrets, err := server.NewSecretBox(testMasterKey, testOrg)
|
|
if err != nil {
|
|
t.Fatalf("secret box: %v", err)
|
|
}
|
|
|
|
logger := luxlog.New("component", "llmtc-test")
|
|
node := zaplib.NewNode(zaplib.NodeConfig{
|
|
NodeID: "llmtc-test-srv",
|
|
Port: port,
|
|
NoDiscovery: true,
|
|
})
|
|
srv := server.NewServer(app, logger, testOrg, secrets, zcap.Verifier{})
|
|
srv.Register(node)
|
|
if err := node.Start(); err != nil {
|
|
t.Fatalf("node start: %v", err)
|
|
}
|
|
|
|
return "127.0.0.1:" + itoa(port), "llmtc-test-srv", func() {
|
|
node.Stop()
|
|
app.Cleanup()
|
|
}
|
|
}
|
|
|
|
// newClient dials the service with a synthetic CapKindIAMSession cap holding the
|
|
// given permissions.
|
|
func newClient(t *testing.T, addr, peerID string, perms uint64, port int) (*server.Client, func()) {
|
|
t.Helper()
|
|
capBuf, err := server.SyntheticCap(perms)
|
|
if err != nil {
|
|
t.Fatalf("synthetic cap: %v", err)
|
|
}
|
|
cli := zaplib.NewNode(zaplib.NodeConfig{
|
|
NodeID: "llmtc-test-cli-" + itoa(port),
|
|
Port: port,
|
|
NoDiscovery: true,
|
|
})
|
|
if err := cli.Start(); err != nil {
|
|
t.Fatalf("client node start: %v", err)
|
|
}
|
|
c, err := server.Dial(cli, addr, peerID, capBuf)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
time.Sleep(150 * time.Millisecond) // handshake settle
|
|
return c, func() { cli.Stop() }
|
|
}
|
|
|
|
func ctx5(t *testing.T) (context.Context, func()) {
|
|
return context.WithTimeout(context.Background(), 5*time.Second)
|
|
}
|
|
|
|
// ─── LLM API keys ─────────────────────────────────────────────────────────────
|
|
|
|
// TestKeyCreateEncryptsAndAllOmitsSecret is the security-critical test: the
|
|
// secret is NEVER returned by keyCreate or keyAll (only the masked display), and
|
|
// the create→all round trip surfaces the key by its safe projection.
|
|
func TestKeyCreateEncryptsAndAllOmitsSecret(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19710)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermKeyRead|server.PermKeyWrite, 19711)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
const secret = "sk-super-secret-value-1234"
|
|
ref, err := cli.KeyCreate(ctx, gen.KeyCreateParamsInput{
|
|
ProjectId: "p1", Provider: "openai", Adapter: "openai", SecretKey: secret, BaseURL: "https://api.openai.com",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("KeyCreate: %v", err)
|
|
}
|
|
if ref.Id() == "" {
|
|
t.Fatal("expected non-empty id")
|
|
}
|
|
if ref.DisplaySecretKey() != "...1234" {
|
|
t.Fatalf("display = %q, want ...1234", ref.DisplaySecretKey())
|
|
}
|
|
|
|
list, err := cli.KeyAll(ctx, "p1")
|
|
if err != nil {
|
|
t.Fatalf("KeyAll: %v", err)
|
|
}
|
|
if list.TotalCount() != 1 || list.Data().Len() != 1 {
|
|
t.Fatalf("count = %d / len = %d, want 1/1", list.TotalCount(), list.Data().Len())
|
|
}
|
|
view, err := gen.WrapLlmApiKeyView(list.Data().BytesAt(0))
|
|
if err != nil {
|
|
t.Fatalf("wrap view: %v", err)
|
|
}
|
|
if view.DisplaySecretKey() != "...1234" {
|
|
t.Fatalf("view display = %q", view.DisplaySecretKey())
|
|
}
|
|
// The SAFE projection must not leak the plaintext anywhere in the row body.
|
|
if containsSecret(list.Data().BytesAt(0), secret) {
|
|
t.Fatal("SECURITY: plaintext secret leaked into keyAll body")
|
|
}
|
|
}
|
|
|
|
func TestKeyUpdateRejectsProviderChange(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19712)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermKeyWrite|server.PermKeyRead, 19713)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ref, err := cli.KeyCreate(ctx, gen.KeyCreateParamsInput{
|
|
ProjectId: "p1", Provider: "openai", Adapter: "openai", SecretKey: "sk-aaaa",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("KeyCreate: %v", err)
|
|
}
|
|
// Changing provider must be rejected.
|
|
if _, err := cli.KeyUpdate(ctx, gen.KeyUpdateParamsInput{
|
|
ProjectId: "p1", Id: ref.Id(), Provider: "anthropic", Adapter: "openai",
|
|
}); err == nil {
|
|
t.Fatal("expected provider-change rejection")
|
|
}
|
|
// Same provider/adapter, no new secret → allowed, display unchanged.
|
|
got, err := cli.KeyUpdate(ctx, gen.KeyUpdateParamsInput{
|
|
ProjectId: "p1", Id: ref.Id(), Provider: "openai", Adapter: "openai", BaseURL: "https://x",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("KeyUpdate (no secret): %v", err)
|
|
}
|
|
if got.DisplaySecretKey() != "...aaaa" {
|
|
t.Fatalf("display after no-secret update = %q, want ...aaaa", got.DisplaySecretKey())
|
|
}
|
|
}
|
|
|
|
func TestKeyDelete(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19714)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermKeyWrite|server.PermKeyRead, 19715)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ref, err := cli.KeyCreate(ctx, gen.KeyCreateParamsInput{ProjectId: "p1", Provider: "openai", Adapter: "openai", SecretKey: "sk-z"})
|
|
if err != nil {
|
|
t.Fatalf("KeyCreate: %v", err)
|
|
}
|
|
res, err := cli.KeyDelete(ctx, "p1", ref.Id())
|
|
if err != nil {
|
|
t.Fatalf("KeyDelete: %v", err)
|
|
}
|
|
if !res.Success() {
|
|
t.Fatal("expected success=true")
|
|
}
|
|
list, _ := cli.KeyAll(ctx, "p1")
|
|
if list.TotalCount() != 0 {
|
|
t.Fatalf("expected 0 keys after delete, got %d", list.TotalCount())
|
|
}
|
|
}
|
|
|
|
func TestKeyTest(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19716)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermKeyRead, 19717)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ok, err := cli.KeyTest(ctx, gen.KeyTestParamsInput{Provider: "openai", Adapter: "openai", SecretKey: "sk-x"})
|
|
if err != nil {
|
|
t.Fatalf("KeyTest: %v", err)
|
|
}
|
|
if !ok.Success() {
|
|
t.Fatalf("expected success, got error %q", ok.Error())
|
|
}
|
|
bad, err := cli.KeyTest(ctx, gen.KeyTestParamsInput{Provider: "openai"})
|
|
if err != nil {
|
|
t.Fatalf("KeyTest (bad): %v", err)
|
|
}
|
|
if bad.Success() {
|
|
t.Fatal("expected failure for missing secretKey")
|
|
}
|
|
}
|
|
|
|
// ─── LLM schemas + tools (identical handlers; test both ordinals) ─────────────
|
|
|
|
func TestSchemaCreateGetUpdateDelete(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19718)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermSchemaRead|server.PermSchemaWrite, 19719)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ref, err := cli.SchemaCreate(ctx, gen.NamedDocParamsInput{ProjectId: "p1", Name: "s1", Description: "d", Body: `{"type":"object"}`})
|
|
if err != nil {
|
|
t.Fatalf("SchemaCreate: %v", err)
|
|
}
|
|
// Duplicate name → conflict.
|
|
if _, err := cli.SchemaCreate(ctx, gen.NamedDocParamsInput{ProjectId: "p1", Name: "s1", Body: `{}`}); err == nil {
|
|
t.Fatal("expected duplicate-name conflict")
|
|
}
|
|
upd, err := cli.SchemaUpdate(ctx, gen.NamedDocUpdateInput{ProjectId: "p1", Id: ref.Id(), Name: "s1", Description: "d2", Body: `{"type":"array"}`})
|
|
if err != nil {
|
|
t.Fatalf("SchemaUpdate: %v", err)
|
|
}
|
|
if upd.Description() != "d2" {
|
|
t.Fatalf("description = %q, want d2", upd.Description())
|
|
}
|
|
list, err := cli.SchemaGetAll(ctx, "p1")
|
|
if err != nil {
|
|
t.Fatalf("SchemaGetAll: %v", err)
|
|
}
|
|
if list.Data().Len() != 1 {
|
|
t.Fatalf("len = %d, want 1", list.Data().Len())
|
|
}
|
|
res, err := cli.SchemaDelete(ctx, "p1", ref.Id())
|
|
if err != nil || !res.Success() {
|
|
t.Fatalf("SchemaDelete: %v success=%v", err, res.Success())
|
|
}
|
|
}
|
|
|
|
func TestToolCreateGetDelete(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19720)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermToolRead|server.PermToolWrite, 19721)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ref, err := cli.ToolCreate(ctx, gen.NamedDocParamsInput{ProjectId: "p1", Name: "t1", Description: "tool", Body: `{"type":"object"}`})
|
|
if err != nil {
|
|
t.Fatalf("ToolCreate: %v", err)
|
|
}
|
|
list, err := cli.ToolGetAll(ctx, "p1")
|
|
if err != nil {
|
|
t.Fatalf("ToolGetAll: %v", err)
|
|
}
|
|
if list.Data().Len() != 1 {
|
|
t.Fatalf("len = %d, want 1", list.Data().Len())
|
|
}
|
|
doc, _ := gen.WrapLlmDocRef(list.Data().BytesAt(0))
|
|
if doc.Name() != "t1" {
|
|
t.Fatalf("name = %q, want t1", doc.Name())
|
|
}
|
|
if _, err := cli.ToolDelete(ctx, "p1", ref.Id()); err != nil {
|
|
t.Fatalf("ToolDelete: %v", err)
|
|
}
|
|
}
|
|
|
|
// ─── Cloud models ─────────────────────────────────────────────────────────────
|
|
|
|
func TestModelConfigSetGetDefaults(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19722)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermModelRead|server.PermModelWrite, 19723)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
// Unset project → tRPC defaults.
|
|
def, err := cli.ModelGetConfig(ctx, "fresh")
|
|
if err != nil {
|
|
t.Fatalf("ModelGetConfig (default): %v", err)
|
|
}
|
|
if def.DefaultModel() != "zen4" || def.MaxTokens() != 4096 {
|
|
t.Fatalf("defaults = %s/%d, want zen4/4096", def.DefaultModel(), def.MaxTokens())
|
|
}
|
|
// Set, then read back the persisted values.
|
|
if _, err := cli.ModelSetConfig(ctx, gen.ModelConfigParamsInput{ProjectId: "p1", DefaultModel: "zen5", Temperature: 0.3, MaxTokens: 8192}); err != nil {
|
|
t.Fatalf("ModelSetConfig: %v", err)
|
|
}
|
|
got, err := cli.ModelGetConfig(ctx, "p1")
|
|
if err != nil {
|
|
t.Fatalf("ModelGetConfig: %v", err)
|
|
}
|
|
if got.DefaultModel() != "zen5" || got.MaxTokens() != 8192 {
|
|
t.Fatalf("got = %s/%d, want zen5/8192", got.DefaultModel(), got.MaxTokens())
|
|
}
|
|
if got.Temperature() < 0.29 || got.Temperature() > 0.31 {
|
|
t.Fatalf("temperature = %v, want ~0.3", got.Temperature())
|
|
}
|
|
}
|
|
|
|
func TestModelListEmpty(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19724)
|
|
defer stop()
|
|
cli, stopCli := newClient(t, addr, peer, server.PermModelRead, 19725)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
list, err := cli.ModelList(ctx, "p1")
|
|
if err != nil {
|
|
t.Fatalf("ModelList: %v", err)
|
|
}
|
|
if list.Object() != "list" {
|
|
t.Fatalf("object = %q, want list", list.Object())
|
|
}
|
|
if list.Data().Len() != 0 {
|
|
t.Fatalf("expected empty registry, got %d", list.Data().Len())
|
|
}
|
|
}
|
|
|
|
// ─── Permission chokepoint ────────────────────────────────────────────────────
|
|
|
|
// TestPermissionDenied proves the bitmask chokepoint per category: a cap holding
|
|
// ONLY key perms cannot touch schemas, and a write op needs the write bit.
|
|
func TestPermissionDenied(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19726)
|
|
defer stop()
|
|
// Holds key read+write, but NO schema/tool/model bits.
|
|
cli, stopCli := newClient(t, addr, peer, server.PermKeyRead|server.PermKeyWrite, 19727)
|
|
defer stopCli()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
if _, err := cli.SchemaGetAll(ctx, "p1"); err == nil {
|
|
t.Fatal("expected schema read to be denied without PermSchemaRead")
|
|
}
|
|
if _, err := cli.ModelGetConfig(ctx, "p1"); err == nil {
|
|
t.Fatal("expected model read to be denied without PermModelRead")
|
|
}
|
|
|
|
// A read-only key cap cannot write keys.
|
|
roCli, stopRO := newClient(t, addr, peer, server.PermKeyRead, 19728)
|
|
defer stopRO()
|
|
if _, err := roCli.KeyCreate(ctx, gen.KeyCreateParamsInput{ProjectId: "p1", Provider: "openai", Adapter: "openai", SecretKey: "x"}); err == nil {
|
|
t.Fatal("expected keyCreate to be denied without PermKeyWrite")
|
|
}
|
|
}
|
|
|
|
// ─── Pipelining proof (create-then-use) ───────────────────────────────────────
|
|
|
|
// TestPipeliningCreateThenList is the load-bearing pipelining proof: schemaGetAll
|
|
// (pipelined off schemaCreate's promise) must ship before schemaCreate's answer
|
|
// resolves. We assert on the instrumented send log that BOTH sends precede the
|
|
// first recv, and that the dependent getAll observed the just-created row
|
|
// (inheriting org through the resolved promise).
|
|
func TestPipeliningCreateThenList(t *testing.T) {
|
|
addr, peer, stop := newService(t, 19730)
|
|
defer stop()
|
|
|
|
var log []server.SendEvent
|
|
cli, stopCli := newClient(t, addr, peer, server.PermSchemaRead|server.PermSchemaWrite, 19731)
|
|
defer stopCli()
|
|
cli.WithSendLog(&log)
|
|
dep, stopDep := newClient(t, addr, peer, server.PermSchemaRead|server.PermSchemaWrite, 19732)
|
|
defer stopDep()
|
|
dep.WithSendLog(&log)
|
|
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
ref, list, err := cli.PipelineCreateThenList(ctx, dep, gen.NamedDocParamsInput{
|
|
ProjectId: "p1", Name: "piped", Description: "x", Body: `{"type":"object"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PipelineCreateThenList: %v", err)
|
|
}
|
|
if ref.Id() == "" {
|
|
t.Fatal("expected created id")
|
|
}
|
|
if list.Data().Len() == 0 {
|
|
t.Fatal("dependent getAll saw no rows — promise org not inherited")
|
|
}
|
|
|
|
firstRecv, sendsBeforeFirstRecv := -1, 0
|
|
for i, e := range log {
|
|
if e.Kind == "recv" {
|
|
firstRecv = i
|
|
break
|
|
}
|
|
if e.Kind == "send" {
|
|
sendsBeforeFirstRecv++
|
|
}
|
|
}
|
|
t.Logf("send log: %s", formatLog(log))
|
|
if firstRecv == -1 {
|
|
t.Fatal("no recv events recorded")
|
|
}
|
|
if sendsBeforeFirstRecv < 2 {
|
|
t.Fatalf("pipelining violated: only %d sends before first answer; want 2", sendsBeforeFirstRecv)
|
|
}
|
|
t.Logf("PIPELINING PROVEN: %d calls shipped before the first answer resolved", sendsBeforeFirstRecv)
|
|
}
|
|
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
func containsSecret(body []byte, secret string) bool {
|
|
for i := 0; i+len(secret) <= len(body); i++ {
|
|
if string(body[i:i+len(secret)]) == secret {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b [12]byte
|
|
i := len(b)
|
|
for n > 0 {
|
|
i--
|
|
b[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(b[i:])
|
|
}
|
|
|
|
func formatLog(log []server.SendEvent) string {
|
|
out := ""
|
|
for _, e := range log {
|
|
method := "create"
|
|
if e.Method == server.MethodSchemaGetAll {
|
|
method = "getAll"
|
|
}
|
|
out += e.Kind + "(" + method + ",p=" + itoa(int(e.PromiseID)) + ",t=" + itoa(int(e.Target)) + ") "
|
|
}
|
|
return out
|
|
}
|