| 48 | // wrapper concentrates SDK calls and address resolution, so tests stub |
| 49 | // it directly rather than the namespaced SDK underneath. |
| 50 | function makeStubClient( |
| 51 | overrides: Partial<{ agentEmail: string; scope: "account" | "agent" }> = {}, |
| 52 | ): McpClient { |
| 53 | const stub = { |
| 54 | agentEmail: overrides.agentEmail ?? "bot@example.com", |
| 55 | // scope drives §6a tier-gating in buildServer. Default to account (full |
| 56 | // surface) so behavior tests see every tool; gating tests pass "agent". |
| 57 | scope: overrides.scope ?? "account", |
| 58 | send: vi.fn(async () => ({ messageId: "msg_sent", status: "sent" })), |
| 59 | reply: vi.fn(async () => ({ messageId: "msg_reply", status: "sent" })), |
| 60 | forward: vi.fn(async () => ({ messageId: "msg_fwd", status: "sent" })), |
| 61 | updateMessageLabels: vi.fn(async () => ({ messageId: "msg_in", labels: ["urgent"] })), |
| 62 | // Cursor-paginated lists return a Page { items, next_cursor }. |
| 63 | listConversations: vi.fn(async () => ({ items: [{ conversationId: "conv_1" }], next_cursor: undefined })), |
| 64 | getConversation: vi.fn(async () => ({ conversationId: "conv_1", messages: [] })), |
| 65 | listMessages: vi.fn(async () => ({ items: [], next_cursor: undefined })), |
| 66 | listAgents: vi.fn(async () => [{ email: "bot@example.com" }]), |
| 67 | // whoami → client.whoami() returns an AccountView (the authenticated |
| 68 | // account identity), NOT an agent record. No default-agent resolution. |
| 69 | whoami: vi.fn(async () => ({ |
| 70 | user: "owner@example.com", |
| 71 | scope: "account", |
| 72 | agentAddress: undefined, |
| 73 | plan: "pro", |
| 74 | limits: { messagesPerDay: 1000 }, |
| 75 | })), |
| 76 | // create_agent now takes { email, name? } and returns the full AgentView. |
| 77 | createAgent: vi.fn(async (body: { email: string; name?: string }) => ({ |
| 78 | id: body.email, |
| 79 | email: body.email, |
| 80 | ...(body.name !== undefined ? { name: body.name } : {}), |
| 81 | domain: body.email.split("@")[1], |
| 82 | })), |
| 83 | getAgent: vi.fn(async (email: string) => ({ |
| 84 | id: email, |
| 85 | email, |
| 86 | hitlEnabled: false, |
| 87 | })), |
| 88 | updateAgent: vi.fn(async (body: Record<string, unknown>) => ({ |
| 89 | id: "bot@example.com", |
| 90 | email: "bot@example.com", |
| 91 | ...body, |
| 92 | })), |
| 93 | getProtection: vi.fn(async (_addr?: string) => ({ |
| 94 | inbound: { gate: { policy: "open", allowlist: [], action: "flag" }, scan: { sensitivity: "off" } }, |
| 95 | outbound: { gate: { policy: "open", allowlist: [], action: "flag" }, scan: { sensitivity: "off" } }, |
| 96 | holds: { ttlSeconds: 604800, onExpiry: "reject" }, |
| 97 | })), |
| 98 | updateProtection: vi.fn(async (config: unknown, _addr?: string) => config), |
| 99 | deleteAgent: vi.fn(async (addr?: string) => addr ?? "bot@example.com"), |
| 100 | listDomains: vi.fn(async () => [ |
| 101 | { domain: "mail.acme.com", verified: true, verificationToken: "tok1" }, |
| 102 | ]), |
| 103 | registerDomain: vi.fn(async (domain: string) => ({ |
| 104 | domain, |
| 105 | verified: false, |
| 106 | verificationToken: "tok_new", |
| 107 | dnsRecords: { |