Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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