2026-03-05 01:28:17 +00:00
|
|
|
import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aelis/core"
|
2026-02-24 22:30:13 +00:00
|
|
|
|
|
|
|
|
import { describe, expect, test } from "bun:test"
|
|
|
|
|
import { Hono } from "hono"
|
|
|
|
|
|
|
|
|
|
import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts"
|
|
|
|
|
import { UserSessionManager } from "../session/index.ts"
|
|
|
|
|
import { registerFeedHttpHandlers } from "./http.ts"
|
|
|
|
|
|
|
|
|
|
interface FeedResponse {
|
|
|
|
|
items: Array<{
|
|
|
|
|
id: string
|
|
|
|
|
type: string
|
|
|
|
|
priority: number
|
|
|
|
|
timestamp: string
|
|
|
|
|
data: Record<string, unknown>
|
|
|
|
|
}>
|
|
|
|
|
errors: Array<{ sourceId: string; error: string }>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
async listActions(): Promise<Record<string, ActionDefinition>> {
|
|
|
|
|
return {}
|
|
|
|
|
},
|
|
|
|
|
async executeAction(): Promise<unknown> {
|
|
|
|
|
return undefined
|
|
|
|
|
},
|
2026-03-01 22:52:41 +00:00
|
|
|
async fetchContext(): Promise<readonly ContextEntry[] | null> {
|
2026-02-24 22:30:13 +00:00
|
|
|
return null
|
|
|
|
|
},
|
|
|
|
|
async fetchItems() {
|
|
|
|
|
return items
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildTestApp(sessionManager: UserSessionManager, userId?: string) {
|
|
|
|
|
const app = new Hono()
|
|
|
|
|
registerFeedHttpHandlers(app, {
|
|
|
|
|
sessionManager,
|
|
|
|
|
authSessionMiddleware: mockAuthSessionMiddleware(userId),
|
|
|
|
|
})
|
|
|
|
|
return app
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("GET /api/feed", () => {
|
|
|
|
|
test("returns 401 without auth", async () => {
|
2026-03-05 02:01:30 +00:00
|
|
|
const manager = new UserSessionManager({ providers: [] })
|
2026-02-24 22:30:13 +00:00
|
|
|
const app = buildTestApp(manager)
|
|
|
|
|
|
|
|
|
|
const res = await app.request("/api/feed")
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(401)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns cached feed when available", async () => {
|
|
|
|
|
const items: FeedItem[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "item-1",
|
|
|
|
|
type: "test",
|
|
|
|
|
priority: 0.8,
|
|
|
|
|
timestamp: new Date("2025-01-01T00:00:00.000Z"),
|
|
|
|
|
data: { value: 42 },
|
|
|
|
|
},
|
|
|
|
|
]
|
2026-03-05 02:01:30 +00:00
|
|
|
const manager = new UserSessionManager({
|
|
|
|
|
providers: [() => createStubSource("test", items)],
|
|
|
|
|
})
|
2026-02-24 22:30:13 +00:00
|
|
|
const app = buildTestApp(manager, "user-1")
|
|
|
|
|
|
|
|
|
|
// Prime the cache
|
|
|
|
|
const session = manager.getOrCreate("user-1")
|
|
|
|
|
await session.engine.refresh()
|
|
|
|
|
expect(session.engine.lastFeed()).not.toBeNull()
|
|
|
|
|
|
|
|
|
|
const res = await app.request("/api/feed")
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
|
const body = (await res.json()) as FeedResponse
|
|
|
|
|
expect(body.items).toHaveLength(1)
|
|
|
|
|
expect(body.items[0]!.id).toBe("item-1")
|
|
|
|
|
expect(body.items[0]!.type).toBe("test")
|
|
|
|
|
expect(body.items[0]!.priority).toBe(0.8)
|
|
|
|
|
expect(body.items[0]!.timestamp).toBe("2025-01-01T00:00:00.000Z")
|
|
|
|
|
expect(body.errors).toHaveLength(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("forces refresh when no cached feed", async () => {
|
|
|
|
|
const items: FeedItem[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "fresh-1",
|
|
|
|
|
type: "test",
|
|
|
|
|
priority: 0.5,
|
|
|
|
|
timestamp: new Date("2025-06-01T12:00:00.000Z"),
|
|
|
|
|
data: { fresh: true },
|
|
|
|
|
},
|
|
|
|
|
]
|
2026-03-05 02:01:30 +00:00
|
|
|
const manager = new UserSessionManager({
|
|
|
|
|
providers: [() => createStubSource("test", items)],
|
|
|
|
|
})
|
2026-02-24 22:30:13 +00:00
|
|
|
const app = buildTestApp(manager, "user-1")
|
|
|
|
|
|
|
|
|
|
// No prior refresh — lastFeed() returns null, handler should call refresh()
|
|
|
|
|
const res = await app.request("/api/feed")
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
|
const body = (await res.json()) as FeedResponse
|
|
|
|
|
expect(body.items).toHaveLength(1)
|
|
|
|
|
expect(body.items[0]!.id).toBe("fresh-1")
|
|
|
|
|
expect(body.items[0]!.data.fresh).toBe(true)
|
|
|
|
|
expect(body.errors).toHaveLength(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("serializes source errors as message strings", async () => {
|
|
|
|
|
const failingSource: FeedSource = {
|
|
|
|
|
id: "failing",
|
|
|
|
|
async listActions() {
|
|
|
|
|
return {}
|
|
|
|
|
},
|
|
|
|
|
async executeAction() {
|
|
|
|
|
return undefined
|
|
|
|
|
},
|
|
|
|
|
async fetchContext() {
|
|
|
|
|
return null
|
|
|
|
|
},
|
|
|
|
|
async fetchItems() {
|
|
|
|
|
throw new Error("connection timeout")
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-03-05 02:01:30 +00:00
|
|
|
const manager = new UserSessionManager({ providers: [() => failingSource] })
|
2026-02-24 22:30:13 +00:00
|
|
|
const app = buildTestApp(manager, "user-1")
|
|
|
|
|
|
|
|
|
|
const res = await app.request("/api/feed")
|
|
|
|
|
|
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
|
const body = (await res.json()) as FeedResponse
|
|
|
|
|
expect(body.items).toHaveLength(0)
|
|
|
|
|
expect(body.errors).toHaveLength(1)
|
|
|
|
|
expect(body.errors[0]!.sourceId).toBe("failing")
|
|
|
|
|
expect(body.errors[0]!.error).toBe("connection timeout")
|
|
|
|
|
})
|
|
|
|
|
})
|