package calls import ( "context" "errors" "regexp" "strconv" "strings" "testing" "time" ) // itoa is the test package's own decimal formatter, used only to spell // out the actor string an assertion expects. func itoa(id int64) string { return strconv.FormatInt(id, 10) } var agentTokenBodyRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) func TestAgentTokenIsShownOnceAndStoredHashed(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, err := s.ensurePerson(ctx, "paul@example.com") if err != nil { t.Fatal(err) } ag, token, err := s.createAgent(ctx, "claude", op) if err != nil { t.Fatal(err) } // The token's own value must never appear in a failure message — // only its shape does (fix round 1, item 6). if !strings.HasPrefix(token, "mt_") { t.Fatal("token lacks the mt_ prefix") } if len(token) != 46 { t.Fatalf("token length %d, want 46", len(token)) } if !agentTokenBodyRe.MatchString(token[3:]) { t.Fatal("token body after mt_ is not [A-Za-z0-9_-]+") } if ag.TokenHash != hashAgentToken(token) || len(ag.TokenHash) != 64 { t.Fatalf("stored hash %q does not match the minted token's hash", ag.TokenHash) } if ag.TokenHash == token { t.Fatal("the hash equals the plaintext token") } // Two mints never coincide. token2, _, err := newAgentToken() if err != nil { t.Fatal(err) } if token2 == token { t.Fatal("two mints produced the same token") } got, opGot, ok, err := s.agentByToken(ctx, token) if err != nil || !ok || got.ID != ag.ID || opGot.ID != op.ID { t.Fatalf("lookup: ok=%v err=%v got.ID=%d opGot.ID=%d", ok, err, got.ID, opGot.ID) } if _, _, ok, _ := s.agentByToken(ctx, "mt_nope"); ok { t.Fatal("an unknown token must not resolve") } if principal(ag, op) != "claude~paul@example.com" { t.Fatalf("principal %q", principal(ag, op)) } if actorForAgent(ag) != "agent:"+itoa(ag.ID) { t.Fatalf("actor %q", actorForAgent(ag)) } } // Fix round 1, item 1: resolving an agent whose operator row no longer // exists must fail closed (false, no error, indistinguishable from an // unknown token) rather than fall back to a bare-ID Person. func TestAgentByTokenFailsClosedWhenTheOperatorRowIsGone(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, err := s.ensurePerson(ctx, "ghost@example.com") if err != nil { t.Fatal(err) } ag, token, err := s.createAgent(ctx, "claude", op) if err != nil { t.Fatal(err) } if err := s.g.WithContext(ctx).Where("id = ?", op.ID).Delete(&Person{}).Error; err != nil { t.Fatal(err) } if _, _, ok, err := s.agentByToken(ctx, token); ok || err != nil { t.Fatalf("agentByToken with a deleted operator: ok=%v err=%v", ok, err) } if _, _, ok, err := s.agentByID(ctx, ag.ID, op); ok || err != nil { t.Fatalf("agentByID with a deleted operator: ok=%v err=%v", ok, err) } } func TestRevokedAgentDoesNotResolveAndOnlyTheOperatorMayRevoke(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, err := s.ensurePerson(ctx, "paul@example.com") if err != nil { t.Fatal(err) } other, err := s.ensurePerson(ctx, "bea@example.com") if err != nil { t.Fatal(err) } ag, token, _ := s.createAgent(ctx, "claude", op) if ok, _ := s.revokeAgent(ctx, ag.ID, other); ok { t.Fatal("another person revoked the agent") } if ok, _ := s.revokeAgent(ctx, ag.ID, op); !ok { t.Fatal("the operator could not revoke") } if ok, _ := s.revokeAgent(ctx, ag.ID, op); ok { t.Fatal("revoking twice must report false") } if _, _, ok, _ := s.agentByToken(ctx, token); ok { t.Fatal("a revoked token must not resolve") } list, _ := s.agentsFor(ctx, op) if len(list) != 1 || list[0].RevokedAt == nil { t.Fatalf("list %+v", list) } if list2, _ := s.agentsFor(ctx, other); len(list2) != 0 { t.Fatal("another person's list must be empty") } } // Fix round 1, item 2: agentByID is scoped to its operator, the same // 404 shape as every other owned row. func TestAgentByIDIsScopedToItsOperator(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, _ := s.ensurePerson(ctx, "paul@example.com") other, _ := s.ensurePerson(ctx, "bea@example.com") ag, _, err := s.createAgent(ctx, "claude", op) if err != nil { t.Fatal(err) } if _, _, ok, err := s.agentByID(ctx, ag.ID, other); ok || err != nil { t.Fatalf("another operator's agentByID: ok=%v err=%v", ok, err) } got, gotOp, ok, err := s.agentByID(ctx, ag.ID, op) if err != nil || !ok || got.ID != ag.ID || gotOp.ID != op.ID { t.Fatalf("the real operator's agentByID: got=%+v ok=%v err=%v", got, ok, err) } if _, _, ok, err := s.agentByID(ctx, 999999, op); ok || err != nil { t.Fatalf("unknown id: ok=%v err=%v", ok, err) } } // Fix round 1, item 6: mint two agents, revoke the older, and check // both that the list stays newest-first and that the revoked row is // still there (revoking removes nothing, only marks it). func TestAgentsForOrdersNewestFirstAndKeepsRevokedRows(t *testing.T) { s, now := testStore(t) ctx := context.Background() op, _ := s.ensurePerson(ctx, "paul@example.com") older, _, err := s.createAgent(ctx, "older", op) if err != nil { t.Fatal(err) } *now = now.Add(time.Minute) newer, _, err := s.createAgent(ctx, "newer", op) if err != nil { t.Fatal(err) } if ok, err := s.revokeAgent(ctx, older.ID, op); !ok || err != nil { t.Fatalf("revoke: ok=%v err=%v", ok, err) } list, err := s.agentsFor(ctx, op) if err != nil { t.Fatal(err) } if len(list) != 2 { t.Fatalf("agentsFor returned %d rows, want 2", len(list)) } if list[0].ID != newer.ID || list[1].ID != older.ID { t.Fatalf("order = [%d, %d], want newest [%d] then oldest [%d]", list[0].ID, list[1].ID, newer.ID, older.ID) } if list[1].RevokedAt == nil { t.Fatal("the revoked row is missing from the list") } if list[0].RevokedAt != nil { t.Fatal("the live row was reported revoked") } } func TestAgentNameRules(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op := Person{ID: 1, Email: "paul@example.com"} for _, bad := range []string{"", " ", "with space", "tilde~x", "colon:x", strings.Repeat("a", 65), "é"} { if _, _, err := s.createAgent(ctx, bad, op); err == nil { t.Fatalf("accepted name %q", bad) } } if _, _, err := s.createAgent(ctx, " claude-2_ok ", op); err != nil { t.Fatalf("trimmed valid name refused: %v", err) } } // Fix round 1, item 3: a zero Person is never a legitimate operator. func TestCreateAgentRefusesAZeroOperator(t *testing.T) { s, _ := testStore(t) ctx := context.Background() if _, _, err := s.createAgent(ctx, "claude", Person{}); err == nil { t.Fatal("created an agent for a zero-value operator") } } // Fix round 1, item 4: the cap lives in the store now, as // errAgentLimit; revoked agents never count against it. func TestCreateAgentRefusesTheTwentyFirstLiveAgent(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, _ := s.ensurePerson(ctx, "paul@example.com") for i := 0; i < maxLiveAgents; i++ { if _, _, err := s.createAgent(ctx, "agent"+itoa(int64(i)), op); err != nil { t.Fatalf("create agent %d: %v", i, err) } } if _, _, err := s.createAgent(ctx, "one-too-many", op); !errors.Is(err, errAgentLimit) { t.Fatalf("21st create returned %v, want errAgentLimit", err) } list, _ := s.agentsFor(ctx, op) if len(list) != maxLiveAgents { t.Fatalf("agent count after the refused create: %d, want %d", len(list), maxLiveAgents) } // Revoking one frees a slot. if ok, _ := s.revokeAgent(ctx, list[0].ID, op); !ok { t.Fatal("revoke to free a slot failed") } if _, _, err := s.createAgent(ctx, "one-more", op); err != nil { t.Fatalf("create after revoking one: %v", err) } } // Fix round 1, item 5: a duplicate live name is a Go-side check, not a // unique index — so a revoked name is free to reuse. func TestCreateAgentRefusesADuplicateLiveNameButAllowsReuseAfterRevoke(t *testing.T) { s, _ := testStore(t) ctx := context.Background() op, _ := s.ensurePerson(ctx, "paul@example.com") first, _, err := s.createAgent(ctx, "scheduler", op) if err != nil { t.Fatal(err) } if _, _, err := s.createAgent(ctx, "scheduler", op); !errors.Is(err, errAgentDuplicate) { t.Fatalf("duplicate create returned %v, want errAgentDuplicate", err) } // Another operator may use the same name freely. other, _ := s.ensurePerson(ctx, "bea@example.com") if _, _, err := s.createAgent(ctx, "scheduler", other); err != nil { t.Fatalf("another operator's same name was refused: %v", err) } if ok, _ := s.revokeAgent(ctx, first.ID, op); !ok { t.Fatal("revoke failed") } if _, _, err := s.createAgent(ctx, "scheduler", op); err != nil { t.Fatalf("reuse after revoke was refused: %v", err) } }