mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
345 lines
12 KiB
Go
345 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/multica-ai/multica/server/internal/util"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
// seedSquadForBriefing creates a squad with the seeded test agent as
|
|
// leader. Returns the loaded db.Squad and a cleanup-registered ID.
|
|
func seedSquadForBriefing(t *testing.T, leaderID string, name, instructions string) db.Squad {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
var squadID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id, instructions)
|
|
VALUES ($1, $2, '', $3, $4, $5)
|
|
RETURNING id
|
|
`, testWorkspaceID, name, leaderID, testUserID, instructions).Scan(&squadID); err != nil {
|
|
t.Fatalf("create squad: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(ctx, `DELETE FROM squad WHERE id = $1`, squadID)
|
|
})
|
|
|
|
uuid := util.MustParseUUID(squadID)
|
|
squad, err := testHandler.Queries.GetSquadInWorkspace(ctx, db.GetSquadInWorkspaceParams{
|
|
ID: uuid,
|
|
WorkspaceID: util.MustParseUUID(testWorkspaceID),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load squad: %v", err)
|
|
}
|
|
return squad
|
|
}
|
|
|
|
func addAgentMember(t *testing.T, squadID pgtype.UUID, agentID, role string) {
|
|
t.Helper()
|
|
if _, err := testHandler.Queries.AddSquadMember(context.Background(), db.AddSquadMemberParams{
|
|
SquadID: squadID,
|
|
MemberType: "agent",
|
|
MemberID: util.MustParseUUID(agentID),
|
|
Role: role,
|
|
}); err != nil {
|
|
t.Fatalf("add agent member: %v", err)
|
|
}
|
|
}
|
|
|
|
func addHumanMember(t *testing.T, squadID pgtype.UUID, userID, role string) {
|
|
t.Helper()
|
|
if _, err := testHandler.Queries.AddSquadMember(context.Background(), db.AddSquadMemberParams{
|
|
SquadID: squadID,
|
|
MemberType: "member",
|
|
MemberID: util.MustParseUUID(userID),
|
|
Role: role,
|
|
}); err != nil {
|
|
t.Fatalf("add human member: %v", err)
|
|
}
|
|
}
|
|
|
|
// seededLeaderAgent loads the first seeded agent in the test workspace.
|
|
func seededLeaderAgent(t *testing.T) (id, name string) {
|
|
t.Helper()
|
|
if err := testPool.QueryRow(context.Background(), `
|
|
SELECT id, name FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1
|
|
`, testWorkspaceID).Scan(&id, &name); err != nil {
|
|
t.Fatalf("load seeded agent: %v", err)
|
|
}
|
|
return id, name
|
|
}
|
|
|
|
// seededHumanMember returns the (member_row_id, user_id, user_name) of the
|
|
// test fixture's human member in the workspace.
|
|
func seededHumanMember(t *testing.T) (memberID, userID, userName string) {
|
|
t.Helper()
|
|
if err := testPool.QueryRow(context.Background(), `
|
|
SELECT m.id, u.id, u.name
|
|
FROM member m JOIN "user" u ON u.id = m.user_id
|
|
WHERE m.workspace_id = $1 ORDER BY m.created_at ASC LIMIT 1
|
|
`, testWorkspaceID).Scan(&memberID, &userID, &userName); err != nil {
|
|
t.Fatalf("load seeded member: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func TestBuildSquadLeaderBriefing_FullSquad(t *testing.T) {
|
|
ctx := context.Background()
|
|
leaderID, leaderName := seededLeaderAgent(t)
|
|
|
|
squad := seedSquadForBriefing(t, leaderID, "Full Squad", "Always write tests.")
|
|
|
|
helper1 := createHandlerTestAgent(t, "Helper One", []byte("[]"))
|
|
helper2 := createHandlerTestAgent(t, "Helper Two", []byte("[]"))
|
|
addAgentMember(t, squad.ID, helper1, "implementer")
|
|
addAgentMember(t, squad.ID, helper2, "")
|
|
|
|
memberRowID, userID, userName := seededHumanMember(t)
|
|
_ = memberRowID
|
|
addHumanMember(t, squad.ID, userID, "reviewer")
|
|
|
|
out := buildSquadLeaderBriefing(ctx, testHandler.Queries, squad)
|
|
|
|
for _, want := range []string{
|
|
"## Squad Operating Protocol",
|
|
"## Squad Roster",
|
|
"Leader (you):",
|
|
leaderName,
|
|
"## Squad Instructions (Full Squad)",
|
|
"Always write tests.",
|
|
"`[@Helper One](mention://agent/" + helper1 + ")`",
|
|
"`[@Helper Two](mention://agent/" + helper2 + ")`",
|
|
`role: "implementer"`,
|
|
`role: "reviewer"`,
|
|
"`[@" + userName + "](mention://member/" + userID + ")`",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("expected briefing to contain %q\n--- briefing ---\n%s", want, out)
|
|
}
|
|
}
|
|
|
|
// Helper Two has no role — must NOT render an empty role: "" segment.
|
|
if strings.Contains(out, `Helper Two — agent, role: ""`) {
|
|
t.Errorf("expected empty role to be omitted, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestBuildSquadLeaderBriefing_OnlyLeader(t *testing.T) {
|
|
ctx := context.Background()
|
|
leaderID, _ := seededLeaderAgent(t)
|
|
squad := seedSquadForBriefing(t, leaderID, "Solo Squad", "")
|
|
|
|
out := buildSquadLeaderBriefing(ctx, testHandler.Queries, squad)
|
|
if !strings.Contains(out, "Members: (none — you are the only member of this squad)") {
|
|
t.Errorf("expected lone-leader fallback line, got:\n%s", out)
|
|
}
|
|
// No user instructions → no Squad Instructions section.
|
|
if strings.Contains(out, "## Squad Instructions") {
|
|
t.Errorf("expected no Squad Instructions section when empty, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestBuildSquadLeaderBriefing_SkipsArchivedAgent(t *testing.T) {
|
|
ctx := context.Background()
|
|
leaderID, _ := seededLeaderAgent(t)
|
|
squad := seedSquadForBriefing(t, leaderID, "Archive Squad", "")
|
|
|
|
archived := createHandlerTestAgent(t, "Retired Bot", []byte("[]"))
|
|
addAgentMember(t, squad.ID, archived, "")
|
|
if _, err := testPool.Exec(ctx,
|
|
`UPDATE agent SET archived_at = now(), archived_by = $1 WHERE id = $2`,
|
|
testUserID, archived,
|
|
); err != nil {
|
|
t.Fatalf("archive agent: %v", err)
|
|
}
|
|
|
|
out := buildSquadLeaderBriefing(ctx, testHandler.Queries, squad)
|
|
if strings.Contains(out, "Retired Bot") {
|
|
t.Errorf("archived agent should not appear in roster:\n%s", out)
|
|
}
|
|
if strings.Contains(out, archived) {
|
|
t.Errorf("archived agent UUID should not appear in roster:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildSquadLeaderBriefing_MentionsRoundTrip is the contract test
|
|
// guaranteeing every emitted mention markdown string parses back through
|
|
// util.ParseMentions to its (type, id). If this ever breaks, the leader's
|
|
// dispatch comments will silently fail to trigger anyone.
|
|
func TestBuildSquadLeaderBriefing_MentionsRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
leaderID, _ := seededLeaderAgent(t)
|
|
squad := seedSquadForBriefing(t, leaderID, "Mention Round Trip", "")
|
|
|
|
helper := createHandlerTestAgent(t, "Round Trip Bot", []byte("[]"))
|
|
addAgentMember(t, squad.ID, helper, "")
|
|
|
|
memberRowID, userID, _ := seededHumanMember(t)
|
|
_ = memberRowID
|
|
addHumanMember(t, squad.ID, userID, "")
|
|
|
|
out := buildSquadLeaderBriefing(ctx, testHandler.Queries, squad)
|
|
mentions := util.ParseMentions(out)
|
|
|
|
wantIDs := map[string]string{
|
|
leaderID: "agent",
|
|
helper: "agent",
|
|
userID: "member",
|
|
}
|
|
got := make(map[string]string, len(mentions))
|
|
for _, m := range mentions {
|
|
got[m.ID] = m.Type
|
|
}
|
|
for id, kind := range wantIDs {
|
|
if got[id] != kind {
|
|
t.Errorf("expected %s mention for id %s, got %q (all parsed: %#v)", kind, id, got[id], mentions)
|
|
}
|
|
}
|
|
}
|
|
|
|
// claimAndDecodeAgent runs ClaimTaskByRuntime for the given runtime and
|
|
// returns the agent block of the response. Fails the test on non-200.
|
|
func claimAndDecodeAgent(t *testing.T, runtimeID string) *TaskAgentData {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
req := newDaemonTokenRequest("POST", "/api/daemon/runtimes/"+runtimeID+"/claim", nil, testWorkspaceID, "test-claim-squad-briefing")
|
|
req = withURLParam(req, "runtimeId", runtimeID)
|
|
testHandler.ClaimTaskByRuntime(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("ClaimTaskByRuntime: %d %s", w.Code, w.Body.String())
|
|
}
|
|
var resp struct {
|
|
Task *struct {
|
|
Agent *TaskAgentData `json:"agent"`
|
|
} `json:"task"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Task == nil || resp.Task.Agent == nil {
|
|
t.Fatalf("expected task.agent in response, got: %s", w.Body.String())
|
|
}
|
|
return resp.Task.Agent
|
|
}
|
|
|
|
// queueSquadIssueTaskFor creates an issue assigned to the squad and a queued
|
|
// task for the given (agentID, runtimeID). Returns the issue + task IDs.
|
|
func queueSquadIssueTaskFor(t *testing.T, squadID, agentID, runtimeID string, issueNumber int) (issueID, taskID string) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO issue (
|
|
workspace_id, title, status, priority, creator_id, creator_type,
|
|
assignee_type, assignee_id, number, position
|
|
) VALUES ($1, 'Squad briefing claim test', 'todo', 'medium', $2, 'member',
|
|
'squad', $3, $4, 0)
|
|
RETURNING id
|
|
`, testWorkspaceID, testUserID, squadID, issueNumber).Scan(&issueID); err != nil {
|
|
t.Fatalf("create squad-assigned issue: %v", err)
|
|
}
|
|
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM issue WHERE id = $1`, issueID) })
|
|
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority)
|
|
VALUES ($1, $2, $3, 'queued', 0)
|
|
RETURNING id
|
|
`, agentID, runtimeID, issueID).Scan(&taskID); err != nil {
|
|
t.Fatalf("queue task: %v", err)
|
|
}
|
|
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM agent_task_queue WHERE id = $1`, taskID) })
|
|
return
|
|
}
|
|
|
|
// TestClaimTask_LeaderGetsBriefing — when the squad leader claims a task on
|
|
// a squad-assigned issue, the response's agent.instructions must include
|
|
// the Operating Protocol + Roster + user instructions.
|
|
func TestClaimTask_LeaderGetsBriefing(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
var leaderID, runtimeID string
|
|
if err := testPool.QueryRow(ctx,
|
|
`SELECT id, runtime_id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
|
testWorkspaceID,
|
|
).Scan(&leaderID, &runtimeID); err != nil {
|
|
t.Fatalf("get leader agent: %v", err)
|
|
}
|
|
|
|
squad := seedSquadForBriefing(t, leaderID, "Briefing Claim Squad", "Be terse.")
|
|
|
|
helper := createHandlerTestAgent(t, "Briefing Helper", []byte("[]"))
|
|
addAgentMember(t, squad.ID, helper, "implementer")
|
|
|
|
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), leaderID, runtimeID, 95001)
|
|
|
|
agent := claimAndDecodeAgent(t, runtimeID)
|
|
for _, want := range []string{
|
|
"## Squad Operating Protocol",
|
|
"## Squad Roster",
|
|
"Leader (you):",
|
|
"## Squad Instructions (Briefing Claim Squad)",
|
|
"Be terse.",
|
|
"`[@Briefing Helper](mention://agent/" + helper + ")`",
|
|
} {
|
|
if !strings.Contains(agent.Instructions, want) {
|
|
t.Errorf("expected agent.instructions to contain %q\n--- instructions ---\n%s", want, agent.Instructions)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestClaimTask_NonLeaderGetsNoBriefing — when a non-leader squad member
|
|
// claims a task on a squad-assigned issue, NO briefing is injected.
|
|
func TestClaimTask_NonLeaderGetsNoBriefing(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
var leaderID string
|
|
if err := testPool.QueryRow(ctx,
|
|
`SELECT id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
|
testWorkspaceID,
|
|
).Scan(&leaderID); err != nil {
|
|
t.Fatalf("get leader agent: %v", err)
|
|
}
|
|
|
|
squad := seedSquadForBriefing(t, leaderID, "Non-Leader Squad", "Squad guidance.")
|
|
|
|
// Create a second agent (NOT the leader) with its own runtime so the
|
|
// claim path picks its task without ambiguity.
|
|
helperID := createHandlerTestAgent(t, "Non Leader Helper", []byte("[]"))
|
|
addAgentMember(t, squad.ID, helperID, "")
|
|
var helperRuntime string
|
|
if err := testPool.QueryRow(ctx,
|
|
`SELECT runtime_id FROM agent WHERE id = $1`, helperID,
|
|
).Scan(&helperRuntime); err != nil {
|
|
t.Fatalf("get helper runtime: %v", err)
|
|
}
|
|
|
|
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), helperID, helperRuntime, 95002)
|
|
|
|
agent := claimAndDecodeAgent(t, helperRuntime)
|
|
for _, mustNot := range []string{
|
|
"Squad Operating Protocol",
|
|
"Squad Roster",
|
|
"Squad Instructions (Non-Leader Squad)",
|
|
} {
|
|
if strings.Contains(agent.Instructions, mustNot) {
|
|
t.Errorf("non-leader claim should NOT contain %q\n--- instructions ---\n%s", mustNot, agent.Instructions)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid "imported and not used: pgtype" if helpers above are the only users.
|
|
var _ pgtype.UUID
|