Skip to content
144 changes: 55 additions & 89 deletions __tests__/api/agents/badges.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, it, expect, beforeEach } from "vitest"
import { beforeEach, describe, expect, it } from "vitest"

import { GET } from "@/app/api/agents/[id]/badges/route"
import { registerAgent, resetAgentRegistryForTests } from "@/lib/agent-registry"
import { awardXP, resetAgentXpDb } from "@/lib/gamification/xp"
import { registerAgent, resetAgentRegistryForTests, getRegisteredAgent } from "@/lib/agent-registry"
import { upsertReputationMetrics, resetReputationStoreForTests } from "@/lib/reputation/reputation-store"
import { markQuestClaimed, resetQuestCompletions } from "@/lib/gamification/quest-completions"
import { resetBadgeStoreForTests } from "@/lib/agents/badges"

const minAgent = (agentId: string) => ({
agentId,
Expand All @@ -16,117 +20,79 @@ const minAgent = (agentId: string) => ({
beforeEach(() => {
resetAgentRegistryForTests()
resetReputationStoreForTests()
resetQuestCompletions()
resetAgentXpDb()
resetBadgeStoreForTests()
})

describe("GET /api/agents/:id/badges", () => {
it("returns 404 for unknown agent", async () => {
const res = await GET(
new Request("http://localhost/api/agents/ghost/badges"),
{ params: Promise.resolve({ id: "ghost" }) },
)
const res = await GET(new Request("http://localhost/api/agents/ghost/badges"), {
params: Promise.resolve({ id: "ghost" }),
})

expect(res.status).toBe(404)
const json = await res.json()
expect(json.ok).toBe(false)
expect(json.error).toBe("agent not found")
expect(await res.json()).toEqual({ ok: false, error: "agent not found" })
})

it("returns empty badge list for agent with no badges", async () => {
it("returns the new badge API shape", async () => {
registerAgent(minAgent("bot-empty"))
const res = await GET(
new Request("http://localhost/api/agents/bot-empty/badges"),
{ params: Promise.resolve({ id: "bot-empty" }) },
)
expect(res.status).toBe(200)
const json = await res.json()
expect(json).toEqual({ agentId: "bot-empty", badges: [], total: 0 })
})

it("returns badges sorted by earnedAt descending", async () => {
registerAgent(minAgent("bot-sorted"))
upsertReputationMetrics("bot-sorted", {
badges: [
{ id: "first-quest", rarity: "common", awardedAt: "2026-05-01T00:00:00.000Z" },
{ id: "rare-taskmaster", rarity: "rare", awardedAt: "2026-06-01T00:00:00.000Z" },
],
const res = await GET(new Request("http://localhost/api/agents/bot-empty/badges"), {
params: Promise.resolve({ id: "bot-empty" }),
})
const res = await GET(
new Request("http://localhost/api/agents/bot-sorted/badges"),
{ params: Promise.resolve({ id: "bot-sorted" }) },
)

expect(res.status).toBe(200)
const json = await res.json()
expect(json.total).toBe(2)
expect(json.badges[0].badgeId).toBe("rare-taskmaster")
expect(json.badges[0].earnedAt).toBe("2026-06-01T00:00:00.000Z")
expect(json.badges[0].rarity).toBe("rare")
expect(json.badges[0].xpValue).toBeGreaterThan(0)
expect(json.badges[0].name).toBe("Rare Taskmaster")
expect(json.badges[1].badgeId).toBe("first-quest")
expect(await res.json()).toEqual({ badges: [], count: 0 })
})

it("returns catalog metadata for known badge ids", async () => {
registerAgent(minAgent("bot-meta"))
upsertReputationMetrics("bot-meta", {
badges: [{ id: "zk-certified", rarity: "epic", awardedAt: "2026-06-01T00:00:00.000Z" }],
it("awards and returns badges automatically when conditions are met", async () => {
registerAgent(minAgent("bot-leveler"))
upsertReputationMetrics("bot-leveler", { tasksCompleted: 1 })
awardXP("bot-leveler", 4_000, "task.completed")

const res = await GET(new Request("http://localhost/api/agents/bot-leveler/badges"), {
params: Promise.resolve({ id: "bot-leveler" }),
})
const res = await GET(
new Request("http://localhost/api/agents/bot-meta/badges"),
{ params: Promise.resolve({ id: "bot-meta" }) },
)
const json = await res.json()
expect(json.badges[0]).toMatchObject({
badgeId: "zk-certified",
name: "ZK Certified",
description: "Minted first ZK passport for permanent identity.",
rarity: "epic",
xpValue: 100,
})

expect(json.count).toBeGreaterThanOrEqual(2)
expect(json.badges).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "first_task", name: "First Task" }),
expect.objectContaining({ type: "level_10", name: "Level 10" }),
]),
)
})

it("returns fallback metadata for unknown badge ids", async () => {
registerAgent(minAgent("bot-unknown-badge"))
upsertReputationMetrics("bot-unknown-badge", {
badges: [{ id: "mystery-award-12345", rarity: "common", awardedAt: "2026-06-01T00:00:00.000Z" }],
it("awards veteran on read once the registration date is old enough", async () => {
registerAgent(minAgent("bot-veteran"))
const agent = getRegisteredAgent("bot-veteran")
agent!.registeredAt = "2026-01-01T00:00:00.000Z"

const res = await GET(new Request("http://localhost/api/agents/bot-veteran/badges"), {
params: Promise.resolve({ id: "bot-veteran" }),
})
const res = await GET(
new Request("http://localhost/api/agents/bot-unknown-badge/badges"),
{ params: Promise.resolve({ id: "bot-unknown-badge" }) },
)
const json = await res.json()
expect(json.total).toBe(1)
expect(json.badges[0].badgeId).toBe("mystery-award-12345")
expect(json.badges[0].name).toBe("mystery-award-12345")
expect(json.badges[0].xpValue).toBe(0)

expect(json.badges).toEqual(
expect.arrayContaining([expect.objectContaining({ type: "veteran" })]),
)
})

it("filters badges by rarity", async () => {
registerAgent(minAgent("bot-rarity"))
upsertReputationMetrics("bot-rarity", {
badges: [
{ id: "first-quest", rarity: "common", awardedAt: "2026-05-01T00:00:00.000Z" },
{ id: "rare-taskmaster", rarity: "rare", awardedAt: "2026-06-01T00:00:00.000Z" },
],
it("awards quest master after five quest completions", async () => {
registerAgent(minAgent("bot-quester"))
for (let i = 0; i < 5; i += 1) {
markQuestClaimed(`quest-${i}`, "bot-quester")
}

const res = await GET(new Request("http://localhost/api/agents/bot-quester/badges"), {
params: Promise.resolve({ id: "bot-quester" }),
})
const res = await GET(
new Request("http://localhost/api/agents/bot-rarity/badges?rarity=common"),
{ params: Promise.resolve({ id: "bot-rarity" }) },
)
expect(res.status).toBe(200)
const json = await res.json()
expect(json.total).toBe(1)
expect(json.badges[0].rarity).toBe("common")
})

it("returns 400 for invalid rarity", async () => {
registerAgent(minAgent("bot-bad-rarity"))
const res = await GET(
new Request("http://localhost/api/agents/bot-bad-rarity/badges?rarity=mythic"),
{ params: Promise.resolve({ id: "bot-bad-rarity" }) },
expect(json.badges).toEqual(
expect.arrayContaining([expect.objectContaining({ type: "quest_master" })]),
)
expect(res.status).toBe(400)
const json = await res.json()
expect(json.ok).toBe(false)
expect(json.error).toContain("rarity")
expect(json.error).toContain("mythic")
})
})
141 changes: 40 additions & 101 deletions __tests__/api/badges/catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,55 @@
import { describe, it, expect, beforeEach } from "vitest"
import { beforeEach, describe, expect, it } from "vitest"

import { GET } from "@/app/api/badges/route"
import { awardXP, resetAgentXpDb } from "@/lib/gamification/xp"
import { checkAndAwardBadges, resetBadgeStoreForTests } from "@/lib/agents/badges"
import { registerAgent, resetAgentRegistryForTests } from "@/lib/agent-registry"
import { upsertReputationMetrics, resetReputationStoreForTests } from "@/lib/reputation/reputation-store"
import { BADGE_CATALOG, getBadgeCatalogEntry, BADGE_RARITY_VALUES } from "@/lib/gamification/badge-catalog"

describe("badge-catalog module", () => {
it("exports all five rarity tiers in order", () => {
expect(BADGE_RARITY_VALUES).toEqual(["common", "uncommon", "rare", "epic", "legendary"])
})

it("getBadgeCatalogEntry returns entry for known id", () => {
const entry = getBadgeCatalogEntry("iron-badge-progress")
expect(entry).toBeDefined()
expect(entry!.badgeId).toBe("iron-badge-progress")
expect(entry!.xpValue).toBeGreaterThan(0)
expect(entry!.rarity).toBe("common")
})

it("getBadgeCatalogEntry returns undefined for unknown id", () => {
expect(getBadgeCatalogEntry("does-not-exist")).toBeUndefined()
})

it("catalog has at least 5 entries", () => {
expect(BADGE_CATALOG.length).toBeGreaterThanOrEqual(5)
})
import { resetReputationStoreForTests, upsertReputationMetrics } from "@/lib/reputation/reputation-store"

const makeAgent = (agentId: string) => ({
agentId,
model: "test",
district: "defense" as const,
capabilities: [],
status: "active" as const,
endpoint: "http://example.test",
x402: { accepts: false },
})

it("all catalog entries have required fields", () => {
for (const entry of BADGE_CATALOG) {
expect(entry.badgeId).toBeTruthy()
expect(entry.name).toBeTruthy()
expect(entry.description).toBeTruthy()
expect(BADGE_RARITY_VALUES as readonly string[]).toContain(entry.rarity)
expect(entry.xpValue).toBeGreaterThan(0)
}
})
beforeEach(() => {
resetAgentRegistryForTests()
resetBadgeStoreForTests()
resetAgentXpDb()
resetReputationStoreForTests()
})

describe("GET /api/badges", () => {
beforeEach(() => {
resetAgentRegistryForTests()
resetReputationStoreForTests()
})

it("returns full catalog", async () => {
const res = await GET(new Request("http://localhost/api/badges"))
expect(res.status).toBe(200)
const json = await res.json()
expect(Array.isArray(json)).toBe(true)
expect(json.length).toBe(BADGE_CATALOG.length)
const entry = json[0]
expect(entry).toHaveProperty("badgeId")
expect(entry).toHaveProperty("name")
expect(entry).toHaveProperty("description")
expect(entry).toHaveProperty("rarity")
expect(entry).toHaveProperty("xpValue")
expect(entry).toHaveProperty("earnedByCount")
})

it("earnedByCount is 0 when no agents have earned the badge", async () => {
const res = await GET(new Request("http://localhost/api/badges"))
it("returns the five implemented badge types", async () => {
const res = await GET()
const json = await res.json()
for (const entry of json) {
expect(entry.earnedByCount).toBe(0)
}
})

it("earnedByCount counts distinct agents with that badge", async () => {
registerAgent({ agentId: "agent-a", model: "m", district: "defense" as const, capabilities: [], status: "active" as const, endpoint: "http://x", x402: { accepts: false } })
registerAgent({ agentId: "agent-b", model: "m", district: "defense" as const, capabilities: [], status: "active" as const, endpoint: "http://x", x402: { accepts: false } })
upsertReputationMetrics("agent-a", {
badges: [{ id: "first-quest", rarity: "common", awardedAt: "2026-05-01T00:00:00.000Z" }],
})
upsertReputationMetrics("agent-b", {
badges: [{ id: "first-quest", rarity: "common", awardedAt: "2026-05-02T00:00:00.000Z" }],
})

const res = await GET(new Request("http://localhost/api/badges"))
const json = await res.json()
const fq = json.find((e: { badgeId: string }) => e.badgeId === "first-quest")
expect(fq).toBeDefined()
expect(fq.earnedByCount).toBe(2)
expect(json).toHaveLength(5)
expect(json.map((badge: { type: string }) => badge.type).sort()).toEqual([
"first_task",
"level_10",
"quest_master",
"top_earner",
"veteran",
])
})

it("earnedByCount counts each agent once even if they have duplicate badge ids", async () => {
registerAgent({ agentId: "agent-dup", model: "m", district: "defense" as const, capabilities: [], status: "active" as const, endpoint: "http://x", x402: { accepts: false } })
upsertReputationMetrics("agent-dup", {
badges: [
{ id: "first-quest", rarity: "common", awardedAt: "2026-05-01T00:00:00.000Z" },
{ id: "first-quest", rarity: "common", awardedAt: "2026-05-03T00:00:00.000Z" },
],
})

const res = await GET(new Request("http://localhost/api/badges"))
const json = await res.json()
const fq = json.find((e: { badgeId: string }) => e.badgeId === "first-quest")
expect(fq.earnedByCount).toBe(1)
})
it("includes earnedByCount from the file-backed badge store", async () => {
registerAgent(makeAgent("agent-counted"))
upsertReputationMetrics("agent-counted", { tasksCompleted: 1 })
awardXP("agent-counted", 4_000, "task.completed")
checkAndAwardBadges("agent-counted")

it("filters catalog by rarity", async () => {
const res = await GET(new Request("http://localhost/api/badges?rarity=common"))
expect(res.status).toBe(200)
const res = await GET()
const json = await res.json()
expect(json.length).toBeGreaterThan(0)
expect(json.every((e: { rarity: string }) => e.rarity === "common")).toBe(true)
})

it("returns 400 for invalid rarity", async () => {
const res = await GET(new Request("http://localhost/api/badges?rarity=mythic"))
expect(res.status).toBe(400)
const json = await res.json()
expect(json.ok).toBe(false)
expect(json.error).toContain("rarity")
expect(json.error).toContain("mythic")
const firstTask = json.find((badge: { type: string }) => badge.type === "first_task")
const levelTen = json.find((badge: { type: string }) => badge.type === "level_10")
expect(firstTask.earnedByCount).toBe(1)
expect(levelTen.earnedByCount).toBe(1)
})
})
Loading
Loading