Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e5106f8
feat(renderer): implement animated district construction sequence
ekwe7 Jun 24, 2026
69f8a42
feat(audio): add per-district day/night ambient city soundtrack engine
ekwe7 Jun 24, 2026
c7570b6
feat(appearance): add sprite customization (skins, accessories, color…
ekwe7 Jun 24, 2026
634b157
ci: don't fail the preview workflow if posting the PR comment is forb…
ekwe7 Jun 24, 2026
6c223e5
feat(renderer): add canvas particle system for XP, payments, level-up…
ekwe7 Jun 25, 2026
16b43e7
feat(quests): add daily cron to auto-close stale quests after 30 days
ekwe7 Jun 26, 2026
d4bd55e
feat(cli): add interactive bootstrap wizard for new Open Stellar depl…
ekwe7 Jun 27, 2026
da4e782
feat(districts): add district unlock store and map API endpoint
ekwe7 Jun 27, 2026
db2c168
feat(theme): add persistent light, dark, and system theme toggle
ekwe7 Jun 29, 2026
434de81
random commit
ekwe7 Jun 30, 2026
bc64c91
Implement structural updates and optimizations across multiple modules
ekwe7 Jul 25, 2026
4c8814f
Refactor code structure for improved readability and maintainability
ekwe7 Jul 25, 2026
04e7989
fix(ci): resolve SonarCloud warnings for nested ternaries, CSPRNG cry…
ekwe7 Jul 25, 2026
f811d72
merge: resolve merge conflicts with upstream/main
ekwe7 Jul 25, 2026
483d213
fix(test): resolve time-dependent and environment-based test failures…
ekwe7 Jul 25, 2026
3770605
feat(tests): add tests for dynamic agent profile page and metadata ge…
ekwe7 Jul 25, 2026
6309755
Refactor code structure for improved readability and maintainability
ekwe7 Jul 25, 2026
0f64f78
Refactor code structure for improved readability and maintainability
ekwe7 Jul 25, 2026
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
40 changes: 40 additions & 0 deletions __tests__/agent-profile-page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest"
import AgentPage, { generateMetadata } from "@/app/agents/[id]/page"
import { registerAgent, resetAgentRegistryForTests } from "@/lib/agent-registry"

vi.mock("next/navigation", () => ({
notFound: vi.fn(() => {
throw new Error("404")
})
}))

describe("Agent Profile Page", () => {
it("renders 404 when agent does not exist", async () => {
resetAgentRegistryForTests()
await expect(AgentPage({ params: Promise.resolve({ id: "non-existent" }) }))
.rejects.toThrow("404")
})

it("renders dynamic agent page metadata and page element correctly", async () => {
resetAgentRegistryForTests()
registerAgent({
agentId: "agent-007",
model: "gpt-5-mini",
district: "defense",
capabilities: ["Threat Detection"],
status: "active",
endpoint: "http://localhost:8080",
x402: { accepts: false },
registeredAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})

const element = await AgentPage({ params: Promise.resolve({ id: "agent-007" }) })
expect(element).toBeDefined()

// Check metadata generation
const metadata = await generateMetadata({ params: Promise.resolve({ id: "agent-007" }) })
expect(metadata.title).toContain("agent-007")
expect(metadata.description).toContain("Defense Grid")
})
})
108 changes: 108 additions & 0 deletions __tests__/api/agents/districts-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { afterEach, describe, expect, it } from "vitest"
import { GET as getDistrictsMap } from "@/app/api/agents/[id]/districts/map/route"
import {
getDistrictUnlockMap,
recordAgentXp,
resetDistrictUnlockStore,
} from "@/lib/districts/district-unlock-store"
import { DISTRICT_REGISTRY } from "@/lib/districts/district-registry"

function context(id: string) {
return { params: Promise.resolve({ id }) }
}

afterEach(() => {
resetDistrictUnlockStore()
})

describe("district map route", () => {
it("returns all configured districts with 1 unlocked and 2 locked with correct status fields", async () => {
const unlockMs = Date.parse("2026-06-27T10:00:00.000Z")
recordAgentXp("bot-1", 531, unlockMs)

const res = await getDistrictsMap(
new Request("http://localhost/api/agents/bot-1/districts/map"),
context("bot-1"),
)
const data = await res.json()

expect(res.status).toBe(200)
expect(data.agentId).toBe("bot-1")
expect(data.districts).toHaveLength(DISTRICT_REGISTRY.length)

const dataCenter = data.districts.find((d: { id: string }) => d.id === "data-center")
expect(dataCenter).toMatchObject({
id: "data-center",
label: "Data Center",
status: "unlocked",
unlockedAt: unlockMs,
xpRequired: 500,
xpAtUnlock: 531,
})
expect(dataCenter).not.toHaveProperty("xpCurrent")
expect(dataCenter).not.toHaveProperty("progressPct")

const commHub = data.districts.find((d: { id: string }) => d.id === "comm-hub")
expect(commHub).toMatchObject({
id: "comm-hub",
label: "Comm Hub",
status: "locked",
xpRequired: 1500,
xpCurrent: 531,
progressPct: 35,
})
expect(commHub).not.toHaveProperty("unlockedAt")
expect(commHub).not.toHaveProperty("xpAtUnlock")

const processing = data.districts.find((d: { id: string }) => d.id === "processing")
expect(processing).toMatchObject({
status: "locked",
xpRequired: 3000,
xpCurrent: 531,
progressPct: 17,
})
})

it("returns all districts as locked with progressPct 0 for an agent with no XP recorded", async () => {
const res = await getDistrictsMap(
new Request("http://localhost/api/agents/bot-new/districts/map"),
context("bot-new"),
)
const data = await res.json()

expect(res.status).toBe(200)
expect(data.agentId).toBe("bot-new")
expect(data.districts).toHaveLength(DISTRICT_REGISTRY.length)
for (const district of data.districts) {
expect(district.status).toBe("locked")
expect(district.xpCurrent).toBe(0)
expect(district.progressPct).toBe(0)
}
})

it("unlocks multiple districts when XP surpasses several thresholds at once", () => {
recordAgentXp("bot-2", 4000)
const result = getDistrictUnlockMap("bot-2")

const unlocked = result.districts.filter((d) => d.status === "unlocked")
const locked = result.districts.filter((d) => d.status === "locked")

expect(unlocked).toHaveLength(3)
expect(unlocked.map((d) => d.id)).toEqual(["data-center", "comm-hub", "processing"])
expect(locked).toHaveLength(2)
expect(locked.map((d) => d.id)).toEqual(["defense", "research"])

for (const d of unlocked) {
expect(d).toHaveProperty("xpAtUnlock", 4000)
}
})

it("xpRequired on each district matches the registry threshold", async () => {
recordAgentXp("bot-3", 0)
const result = getDistrictUnlockMap("bot-3")

for (let i = 0; i < DISTRICT_REGISTRY.length; i++) {
expect(result.districts[i].xpRequired).toBe(DISTRICT_REGISTRY[i].xpRequired)
}
})
})
2 changes: 1 addition & 1 deletion __tests__/api/agents/positions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe("agent position store", () => {
expect(records[0]).toMatchObject({ pixelX: 16 })
expect(records.at(-1)).toMatchObject({ pixelX: 1015 })
expect(listAgentPositionHistory("bot-1", 1000)).toHaveLength(1000)
})
}, 20000)

it("accepts a 500-char agentId but stores it with a 200-char key", () => {
const hugeId = "A".repeat(500)
Expand Down
203 changes: 203 additions & 0 deletions __tests__/api/cron/close-stale-quests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { GET as runCloseStaleQuests } from "@/app/api/cron/close-stale-quests/route"
import { createQuest, getQuest, resetQuestStore, STALE_THRESHOLD_MS } from "@/lib/quests/quest-store"
import { publishSystemEvent } from "@/lib/events/system-events"

vi.mock("@/lib/events/system-events", () => ({
publishSystemEvent: vi.fn(),
}))

const publishMock = vi.mocked(publishSystemEvent)

function makeRequest(opts: { secret?: string } = {}) {
const headers: Record<string, string> = {}
if (opts.secret) headers["authorization"] = `Bearer ${opts.secret}`
return new Request("http://localhost/api/cron/close-stale-quests", { headers })
}

const THIRTY_ONE_DAYS_AGO = new Date(Date.now() - STALE_THRESHOLD_MS - 1).toISOString()
const TWENTY_NINE_DAYS_AGO = new Date(Date.now() - STALE_THRESHOLD_MS + 24 * 60 * 60 * 1000).toISOString()

beforeEach(() => {
delete process.env.CRON_SECRET
publishMock.mockClear()
})

afterEach(() => {
resetQuestStore()
delete process.env.CRON_SECRET
})

describe("GET /api/cron/close-stale-quests β€” authorization", () => {
it("returns 401 without a correct CRON_SECRET", async () => {
process.env.CRON_SECRET = "my-secret"

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(401)
expect(data.ok).toBe(false)
})

it("returns 401 with a wrong CRON_SECRET", async () => {
process.env.CRON_SECRET = "my-secret"

const res = await runCloseStaleQuests(makeRequest({ secret: "wrong-secret" }))
const data = await res.json()

expect(res.status).toBe(401)
expect(data.ok).toBe(false)
})

it("allows through when CRON_SECRET matches", async () => {
process.env.CRON_SECRET = "my-secret"

const res = await runCloseStaleQuests(makeRequest({ secret: "my-secret" }))
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
})

it("allows through when CRON_SECRET is not configured", async () => {
const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
})
})

describe("GET /api/cron/close-stale-quests β€” stale in-progress quests", () => {
it("transitions a stale in-progress quest to abandoned", async () => {
createQuest({ id: "q-stale", title: "Fix orbital drift", assignedTo: "agent-1", updatedAt: THIRTY_ONE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(200)
expect(data.abandoned).toHaveLength(1)
expect(data.abandoned[0].id).toBe("q-stale")
expect(data.abandoned[0].status).toBe("abandoned")
expect(data.expired).toHaveLength(0)

expect(getQuest("q-stale")?.status).toBe("abandoned")
})

it("emits quest.abandoned event for each stale in-progress quest", async () => {
createQuest({ id: "q-a1", title: "Calibrate sensors", assignedTo: "agent-2", updatedAt: THIRTY_ONE_DAYS_AGO })
createQuest({ id: "q-a2", title: "Map nebula", assignedTo: "agent-3", updatedAt: THIRTY_ONE_DAYS_AGO })

await runCloseStaleQuests(makeRequest())

const abandonedCalls = publishMock.mock.calls.filter(([e]) => e.type === "quest.abandoned")
expect(abandonedCalls).toHaveLength(2)
expect(abandonedCalls.map(([e]) => (e as { questId: string }).questId).sort()).toEqual(["q-a1", "q-a2"].sort())
})

it("does not close in-progress quests updated within the last 30 days", async () => {
createQuest({ id: "q-fresh", title: "Recent PR activity", assignedTo: "agent-4", updatedAt: TWENTY_NINE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(data.abandoned).toHaveLength(0)
expect(getQuest("q-fresh")?.status).toBe("in_progress")
})
})

describe("GET /api/cron/close-stale-quests β€” unassigned quests with no applicants", () => {
it("transitions a stale unassigned quest with no applicants to expired", async () => {
createQuest({ id: "q-expire", title: "Explore sector 7", updatedAt: THIRTY_ONE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(200)
expect(data.expired).toHaveLength(1)
expect(data.expired[0].id).toBe("q-expire")
expect(data.expired[0].status).toBe("expired")
expect(data.abandoned).toHaveLength(0)

expect(getQuest("q-expire")?.status).toBe("expired")
})

it("emits quest.expired event for each expired unassigned quest", async () => {
createQuest({ id: "q-e1", title: "Survey asteroid belt", updatedAt: THIRTY_ONE_DAYS_AGO })

await runCloseStaleQuests(makeRequest())

const expiredCalls = publishMock.mock.calls.filter(([e]) => e.type === "quest.expired")
expect(expiredCalls).toHaveLength(1)
expect((expiredCalls[0][0] as { questId: string }).questId).toBe("q-e1")
})

it("does not expire unassigned quests that still have applicants", async () => {
createQuest({ id: "q-applicants", title: "Has applicants", applicants: ["agent-5"], updatedAt: THIRTY_ONE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(data.expired).toHaveLength(0)
expect(getQuest("q-applicants")?.status).toBe("open")
})

it("does not expire unassigned quests updated within the last 30 days", async () => {
createQuest({ id: "q-fresh-open", title: "New quest", updatedAt: TWENTY_NINE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(data.expired).toHaveLength(0)
expect(getQuest("q-fresh-open")?.status).toBe("open")
})
})

describe("GET /api/cron/close-stale-quests β€” no quests closed when all are fresh", () => {
it("returns empty arrays when all quests are within the 30-day window", async () => {
createQuest({ id: "q-ok-1", title: "Active quest", assignedTo: "agent-6", updatedAt: TWENTY_NINE_DAYS_AGO })
createQuest({ id: "q-ok-2", title: "Open quest", updatedAt: TWENTY_NINE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(200)
expect(data.abandoned).toHaveLength(0)
expect(data.expired).toHaveLength(0)
expect(publishMock).not.toHaveBeenCalled()
})
})

describe("GET /api/cron/close-stale-quests β€” already-closed quests are skipped", () => {
it("does not re-close completed or already-abandoned quests", async () => {
createQuest({ id: "q-done", title: "Done quest", status: "completed", updatedAt: THIRTY_ONE_DAYS_AGO })
createQuest({ id: "q-already-abandoned", title: "Old abandoned", status: "abandoned", updatedAt: THIRTY_ONE_DAYS_AGO })

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(data.abandoned).toHaveLength(0)
expect(data.expired).toHaveLength(0)
expect(publishMock).not.toHaveBeenCalled()
})
})

describe("GET /api/cron/close-stale-quests β€” unit test: 31-day-old quest becomes abandoned", () => {
it("quest created 31 days ago with assignedTo set transitions to abandoned", async () => {
const thirtyOneDaysAgo = new Date(Date.now() - STALE_THRESHOLD_MS - 1).toISOString()
createQuest({ id: "q-unit", title: "Unit test quest", assignedTo: "agent-unit", updatedAt: thirtyOneDaysAgo })

expect(getQuest("q-unit")?.status).toBe("in_progress")

const res = await runCloseStaleQuests(makeRequest())
const data = await res.json()

expect(res.status).toBe(200)
expect(data.ok).toBe(true)
expect(getQuest("q-unit")?.status).toBe("abandoned")

const abandonedCall = publishMock.mock.calls.find(([e]) => e.type === "quest.abandoned")
expect(abandonedCall).toBeDefined()
expect((abandonedCall![0] as { questId: string }).questId).toBe("q-unit")
})
})
Loading
Loading