@sourcepress/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.omc/state/agent-replay-31d84b63-606a-4368-b9e6-93fe4f5ae0f7.jsonl +2 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/mission-state.json +53 -0
- package/.omc/state/subagent-tracking.json +17 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +26 -0
- package/dist/__tests__/app-integration.test.d.ts +2 -0
- package/dist/__tests__/app-integration.test.d.ts.map +1 -0
- package/dist/__tests__/app-integration.test.js +71 -0
- package/dist/__tests__/app-integration.test.js.map +1 -0
- package/dist/__tests__/approval.test.d.ts +2 -0
- package/dist/__tests__/approval.test.d.ts.map +1 -0
- package/dist/__tests__/approval.test.js +170 -0
- package/dist/__tests__/approval.test.js.map +1 -0
- package/dist/__tests__/content.test.d.ts +2 -0
- package/dist/__tests__/content.test.d.ts.map +1 -0
- package/dist/__tests__/content.test.js +187 -0
- package/dist/__tests__/content.test.js.map +1 -0
- package/dist/__tests__/engine.test.d.ts +2 -0
- package/dist/__tests__/engine.test.d.ts.map +1 -0
- package/dist/__tests__/engine.test.js +77 -0
- package/dist/__tests__/engine.test.js.map +1 -0
- package/dist/__tests__/eval.test.d.ts +2 -0
- package/dist/__tests__/eval.test.d.ts.map +1 -0
- package/dist/__tests__/eval.test.js +320 -0
- package/dist/__tests__/eval.test.js.map +1 -0
- package/dist/__tests__/graph.test.d.ts +2 -0
- package/dist/__tests__/graph.test.d.ts.map +1 -0
- package/dist/__tests__/graph.test.js +169 -0
- package/dist/__tests__/graph.test.js.map +1 -0
- package/dist/__tests__/health.test.d.ts +2 -0
- package/dist/__tests__/health.test.d.ts.map +1 -0
- package/dist/__tests__/health.test.js +56 -0
- package/dist/__tests__/health.test.js.map +1 -0
- package/dist/__tests__/import.test.d.ts +2 -0
- package/dist/__tests__/import.test.d.ts.map +1 -0
- package/dist/__tests__/import.test.js +138 -0
- package/dist/__tests__/import.test.js.map +1 -0
- package/dist/__tests__/intent.test.d.ts +2 -0
- package/dist/__tests__/intent.test.d.ts.map +1 -0
- package/dist/__tests__/intent.test.js +122 -0
- package/dist/__tests__/intent.test.js.map +1 -0
- package/dist/__tests__/jobs.test.d.ts +2 -0
- package/dist/__tests__/jobs.test.d.ts.map +1 -0
- package/dist/__tests__/jobs.test.js +96 -0
- package/dist/__tests__/jobs.test.js.map +1 -0
- package/dist/__tests__/knowledge.test.d.ts +2 -0
- package/dist/__tests__/knowledge.test.d.ts.map +1 -0
- package/dist/__tests__/knowledge.test.js +110 -0
- package/dist/__tests__/knowledge.test.js.map +1 -0
- package/dist/__tests__/media-routes.test.d.ts +2 -0
- package/dist/__tests__/media-routes.test.d.ts.map +1 -0
- package/dist/__tests__/media-routes.test.js +88 -0
- package/dist/__tests__/media-routes.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +92 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/app.d.ts +7 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +85 -0
- package/dist/app.js.map +1 -0
- package/dist/engine.d.ts +38 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +106 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +17 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +54 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +13 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +24 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +30 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +11 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +26 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/route-error-handler.d.ts +12 -0
- package/dist/middleware/route-error-handler.d.ts.map +1 -0
- package/dist/middleware/route-error-handler.js +9 -0
- package/dist/middleware/route-error-handler.js.map +1 -0
- package/dist/routes/approval.d.ts +4 -0
- package/dist/routes/approval.d.ts.map +1 -0
- package/dist/routes/approval.js +70 -0
- package/dist/routes/approval.js.map +1 -0
- package/dist/routes/content.d.ts +4 -0
- package/dist/routes/content.d.ts.map +1 -0
- package/dist/routes/content.js +145 -0
- package/dist/routes/content.js.map +1 -0
- package/dist/routes/eval.d.ts +4 -0
- package/dist/routes/eval.d.ts.map +1 -0
- package/dist/routes/eval.js +178 -0
- package/dist/routes/eval.js.map +1 -0
- package/dist/routes/graph.d.ts +4 -0
- package/dist/routes/graph.d.ts.map +1 -0
- package/dist/routes/graph.js +90 -0
- package/dist/routes/graph.js.map +1 -0
- package/dist/routes/health.d.ts +13 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +19 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/import.d.ts +4 -0
- package/dist/routes/import.d.ts.map +1 -0
- package/dist/routes/import.js +85 -0
- package/dist/routes/import.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/intent.d.ts +4 -0
- package/dist/routes/intent.d.ts.map +1 -0
- package/dist/routes/intent.js +80 -0
- package/dist/routes/intent.js.map +1 -0
- package/dist/routes/jobs.d.ts +4 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +67 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/knowledge.d.ts +4 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +48 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/media.d.ts +4 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +87 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/schema.d.ts +4 -0
- package/dist/routes/schema.d.ts.map +1 -0
- package/dist/routes/schema.js +54 -0
- package/dist/routes/schema.js.map +1 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +115 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/app-integration.test.ts +80 -0
- package/src/__tests__/approval.test.ts +195 -0
- package/src/__tests__/content.test.ts +202 -0
- package/src/__tests__/engine.test.ts +86 -0
- package/src/__tests__/eval.test.ts +343 -0
- package/src/__tests__/graph.test.ts +182 -0
- package/src/__tests__/health.test.ts +68 -0
- package/src/__tests__/import.test.ts +148 -0
- package/src/__tests__/intent.test.ts +133 -0
- package/src/__tests__/jobs.test.ts +107 -0
- package/src/__tests__/knowledge.test.ts +121 -0
- package/src/__tests__/media-routes.test.ts +109 -0
- package/src/__tests__/schema.test.ts +100 -0
- package/src/app.ts +92 -0
- package/src/engine.ts +168 -0
- package/src/index.ts +31 -0
- package/src/middleware/auth.ts +66 -0
- package/src/middleware/cors.ts +15 -0
- package/src/middleware/error-handler.ts +42 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/rate-limit.ts +27 -0
- package/src/middleware/route-error-handler.ts +13 -0
- package/src/routes/approval.ts +90 -0
- package/src/routes/content.ts +256 -0
- package/src/routes/eval.ts +262 -0
- package/src/routes/graph.ts +111 -0
- package/src/routes/health.ts +33 -0
- package/src/routes/import.ts +122 -0
- package/src/routes/index.ts +11 -0
- package/src/routes/intent.ts +105 -0
- package/src/routes/jobs.ts +84 -0
- package/src/routes/knowledge.ts +73 -0
- package/src/routes/media.ts +117 -0
- package/src/routes/schema.ts +75 -0
- package/src/standalone.ts +130 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CollectionDefinition, ContentFile } from "@sourcepress/core";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { EngineContext } from "../engine.js";
|
|
5
|
+
import { errorHandler } from "../middleware/error-handler.js";
|
|
6
|
+
import { contentRoutes } from "../routes/content.js";
|
|
7
|
+
|
|
8
|
+
const mockPosts: ContentFile[] = [
|
|
9
|
+
{
|
|
10
|
+
collection: "posts",
|
|
11
|
+
slug: "hello-world",
|
|
12
|
+
path: "content/posts/hello-world.mdx",
|
|
13
|
+
frontmatter: { title: "Hello World", draft: false },
|
|
14
|
+
body: "# Hello World\n\nThis is a test post.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
collection: "posts",
|
|
18
|
+
slug: "second-post",
|
|
19
|
+
path: "content/posts/second-post.mdx",
|
|
20
|
+
frontmatter: { title: "Second Post", draft: true },
|
|
21
|
+
body: "# Second Post\n\nAnother post.",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const mockCollections: Record<string, CollectionDefinition> = {
|
|
26
|
+
posts: {
|
|
27
|
+
name: "Blog Posts",
|
|
28
|
+
path: "content/posts",
|
|
29
|
+
format: "mdx",
|
|
30
|
+
fields: { title: { type: "string", required: true } },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createMockEngine(): EngineContext {
|
|
35
|
+
return {
|
|
36
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
37
|
+
config: {} as any,
|
|
38
|
+
github: {
|
|
39
|
+
createBranch: vi.fn().mockResolvedValue("abc123"),
|
|
40
|
+
createOrUpdateFile: vi.fn().mockResolvedValue({ sha: "def456", commit_sha: "ghi789" }),
|
|
41
|
+
createPR: vi
|
|
42
|
+
.fn()
|
|
43
|
+
.mockResolvedValue({ number: 42, html_url: "https://github.com/test/pr/42" }),
|
|
44
|
+
getFile: vi.fn().mockResolvedValue({ sha: "existing-sha" }),
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
46
|
+
} as any,
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
48
|
+
knowledge: {} as any,
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
50
|
+
knowledgeStore: {} as any,
|
|
51
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
52
|
+
cache: {} as any,
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
54
|
+
budget: {} as any,
|
|
55
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
56
|
+
provider: {} as any,
|
|
57
|
+
listContent: vi.fn().mockImplementation(async (collection: string) => {
|
|
58
|
+
if (collection === "posts") return mockPosts;
|
|
59
|
+
return [];
|
|
60
|
+
}),
|
|
61
|
+
getContent: vi.fn().mockImplementation(async (collection: string, slug: string) => {
|
|
62
|
+
if (collection === "posts") return mockPosts.find((p) => p.slug === slug) ?? null;
|
|
63
|
+
return null;
|
|
64
|
+
}),
|
|
65
|
+
getCollectionDef: vi.fn().mockImplementation((collection: string) => {
|
|
66
|
+
return mockCollections[collection] ?? null;
|
|
67
|
+
}),
|
|
68
|
+
listCollections: vi.fn().mockReturnValue(["posts"]),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createTestApp(engine: EngineContext) {
|
|
73
|
+
const app = new Hono().basePath("/api");
|
|
74
|
+
app.use("*", errorHandler());
|
|
75
|
+
app.route("/", contentRoutes(engine));
|
|
76
|
+
return app;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("Content routes", () => {
|
|
80
|
+
let engine: EngineContext;
|
|
81
|
+
let app: Hono;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
engine = createMockEngine();
|
|
85
|
+
app = createTestApp(engine);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("GET /api/content", () => {
|
|
89
|
+
it("returns all content across collections", async () => {
|
|
90
|
+
const res = await app.request("/api/content");
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
93
|
+
const body = (await res.json()) as any;
|
|
94
|
+
expect(body.total).toBe(2);
|
|
95
|
+
expect(body.items[0].slug).toBe("hello-world");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("GET /api/content/:collection", () => {
|
|
100
|
+
it("returns content for a valid collection", async () => {
|
|
101
|
+
const res = await app.request("/api/content/posts");
|
|
102
|
+
expect(res.status).toBe(200);
|
|
103
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
104
|
+
const body = (await res.json()) as any;
|
|
105
|
+
expect(body.total).toBe(2);
|
|
106
|
+
expect(body.collection.name).toBe("Blog Posts");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns 404 for unknown collection", async () => {
|
|
110
|
+
const res = await app.request("/api/content/unknown");
|
|
111
|
+
expect(res.status).toBe(404);
|
|
112
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
113
|
+
const body = (await res.json()) as any;
|
|
114
|
+
expect(body.code).toBe("COLLECTION_NOT_FOUND");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("GET /api/content/:collection/:slug", () => {
|
|
119
|
+
it("returns a single content file", async () => {
|
|
120
|
+
const res = await app.request("/api/content/posts/hello-world");
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
123
|
+
const body = (await res.json()) as any;
|
|
124
|
+
expect(body.slug).toBe("hello-world");
|
|
125
|
+
expect(body.body).toContain("Hello World");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns 404 for unknown slug", async () => {
|
|
129
|
+
const res = await app.request("/api/content/posts/nonexistent");
|
|
130
|
+
expect(res.status).toBe(404);
|
|
131
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
132
|
+
const body = (await res.json()) as any;
|
|
133
|
+
expect(body.code).toBe("CONTENT_NOT_FOUND");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("POST /api/content/:collection", () => {
|
|
138
|
+
it("creates content and returns PR", async () => {
|
|
139
|
+
const res = await app.request("/api/content/posts", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
slug: "new-post",
|
|
144
|
+
frontmatter: { title: "New Post" },
|
|
145
|
+
body: "# New Post\n\nContent here.",
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
expect(res.status).toBe(201);
|
|
149
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
150
|
+
const body = (await res.json()) as any;
|
|
151
|
+
expect(body.created).toBe(true);
|
|
152
|
+
expect(body.pr.number).toBe(42);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("returns 400 for missing slug", async () => {
|
|
156
|
+
const res = await app.request("/api/content/posts", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ body: "content" }),
|
|
160
|
+
});
|
|
161
|
+
expect(res.status).toBe(400);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("PUT /api/content/:collection/:slug", () => {
|
|
166
|
+
it("updates content and returns PR", async () => {
|
|
167
|
+
const res = await app.request("/api/content/posts/hello-world", {
|
|
168
|
+
method: "PUT",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
frontmatter: { title: "Updated Title" },
|
|
172
|
+
body: "# Updated\n\nNew content.",
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
expect(res.status).toBe(200);
|
|
176
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
177
|
+
const body = (await res.json()) as any;
|
|
178
|
+
expect(body.updated).toBe(true);
|
|
179
|
+
expect(body.pr.number).toBe(42);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("DELETE /api/content/:collection/:slug", () => {
|
|
184
|
+
it("deletes content and returns PR", async () => {
|
|
185
|
+
const res = await app.request("/api/content/posts/hello-world", {
|
|
186
|
+
method: "DELETE",
|
|
187
|
+
});
|
|
188
|
+
expect(res.status).toBe(200);
|
|
189
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
190
|
+
const body = (await res.json()) as any;
|
|
191
|
+
expect(body.deleted).toBe(true);
|
|
192
|
+
expect(body.pr.number).toBe(42);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("returns 404 for unknown slug", async () => {
|
|
196
|
+
const res = await app.request("/api/content/posts/nonexistent", {
|
|
197
|
+
method: "DELETE",
|
|
198
|
+
});
|
|
199
|
+
expect(res.status).toBe(404);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SourcePressConfig } from "@sourcepress/core";
|
|
2
|
+
import { InMemoryKnowledgeStore } from "@sourcepress/knowledge";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createEngine } from "../engine.js";
|
|
5
|
+
|
|
6
|
+
const testConfig: SourcePressConfig = {
|
|
7
|
+
repository: { owner: "test", repo: "site", branch: "main" },
|
|
8
|
+
ai: { provider: "anthropic", model: "claude-sonnet-4-5-20250514" },
|
|
9
|
+
collections: {
|
|
10
|
+
posts: {
|
|
11
|
+
name: "Blog Posts",
|
|
12
|
+
path: "content/posts",
|
|
13
|
+
format: "mdx",
|
|
14
|
+
fields: {
|
|
15
|
+
title: { type: "string", required: true },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
team: {
|
|
19
|
+
name: "Team",
|
|
20
|
+
path: "content/team",
|
|
21
|
+
format: "yaml",
|
|
22
|
+
fields: {
|
|
23
|
+
name: { type: "string", required: true },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
knowledge: { path: "knowledge/", graph: { backend: "local" } },
|
|
28
|
+
intent: { path: "intent/" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("createEngine", () => {
|
|
32
|
+
it("creates engine context with all required properties", async () => {
|
|
33
|
+
const engine = await createEngine({
|
|
34
|
+
config: testConfig,
|
|
35
|
+
githubToken: "test-token",
|
|
36
|
+
knowledgeStore: new InMemoryKnowledgeStore(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(engine.config).toBe(testConfig);
|
|
40
|
+
expect(engine.github).toBeDefined();
|
|
41
|
+
expect(engine.knowledge).toBeDefined();
|
|
42
|
+
expect(engine.knowledgeStore).toBeDefined();
|
|
43
|
+
expect(engine.cache).toBeDefined();
|
|
44
|
+
expect(engine.budget).toBeDefined();
|
|
45
|
+
expect(engine.provider).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("listCollections returns configured collection names", async () => {
|
|
49
|
+
const engine = await createEngine({
|
|
50
|
+
config: testConfig,
|
|
51
|
+
githubToken: "test-token",
|
|
52
|
+
knowledgeStore: new InMemoryKnowledgeStore(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const collections = engine.listCollections();
|
|
56
|
+
expect(collections).toContain("posts");
|
|
57
|
+
expect(collections).toContain("team");
|
|
58
|
+
expect(collections.length).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("getCollectionDef returns definition for valid collection", async () => {
|
|
62
|
+
const engine = await createEngine({
|
|
63
|
+
config: testConfig,
|
|
64
|
+
githubToken: "test-token",
|
|
65
|
+
knowledgeStore: new InMemoryKnowledgeStore(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const def = engine.getCollectionDef("posts");
|
|
69
|
+
expect(def).toBeDefined();
|
|
70
|
+
// biome-ignore lint/style/noNonNullAssertion: asserted defined on line above
|
|
71
|
+
expect(def!.name).toBe("Blog Posts");
|
|
72
|
+
// biome-ignore lint/style/noNonNullAssertion: asserted defined on line above
|
|
73
|
+
expect(def!.format).toBe("mdx");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("getCollectionDef returns null for unknown collection", async () => {
|
|
77
|
+
const engine = await createEngine({
|
|
78
|
+
config: testConfig,
|
|
79
|
+
githubToken: "test-token",
|
|
80
|
+
knowledgeStore: new InMemoryKnowledgeStore(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const def = engine.getCollectionDef("nonexistent");
|
|
84
|
+
expect(def).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { EngineContext } from "../engine.js";
|
|
4
|
+
import { errorHandler } from "../middleware/error-handler.js";
|
|
5
|
+
import { evalRoutes } from "../routes/eval.js";
|
|
6
|
+
|
|
7
|
+
// Mock @sourcepress/ai
|
|
8
|
+
vi.mock("@sourcepress/ai", async () => {
|
|
9
|
+
const actual = await vi.importActual("@sourcepress/ai");
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
judge: vi.fn().mockResolvedValue({
|
|
13
|
+
score: 82,
|
|
14
|
+
reasoning: "Good match.",
|
|
15
|
+
usage: {
|
|
16
|
+
input_tokens: 1000,
|
|
17
|
+
output_tokens: 100,
|
|
18
|
+
estimated_cost_usd: 0.004,
|
|
19
|
+
function_name: "judge",
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
EvalRunner: vi.fn().mockImplementation(() => ({
|
|
24
|
+
run: vi.fn().mockResolvedValue({
|
|
25
|
+
iterations: [
|
|
26
|
+
{
|
|
27
|
+
id: "eval-case-study-1",
|
|
28
|
+
content_type: "case-study",
|
|
29
|
+
prompt_version: "v1",
|
|
30
|
+
score: 82,
|
|
31
|
+
status: "keep",
|
|
32
|
+
reasoning: "Good match.",
|
|
33
|
+
iteration: 1,
|
|
34
|
+
generated_content: "# Case Study\n\nContent.",
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
total_usage: {
|
|
37
|
+
input_tokens: 3000,
|
|
38
|
+
output_tokens: 500,
|
|
39
|
+
estimated_cost_usd: 0.016,
|
|
40
|
+
function_name: "eval-run",
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
final_score: 82,
|
|
46
|
+
final_status: "keep",
|
|
47
|
+
final_content: "# Case Study\n\nContent.",
|
|
48
|
+
final_prompt: "Write a case study.",
|
|
49
|
+
prompt_improved: false,
|
|
50
|
+
total_usage: {
|
|
51
|
+
input_tokens: 3000,
|
|
52
|
+
output_tokens: 500,
|
|
53
|
+
estimated_cost_usd: 0.016,
|
|
54
|
+
function_name: "eval-run",
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
judgeOnly: vi.fn().mockResolvedValue({
|
|
59
|
+
score: 72,
|
|
60
|
+
reasoning: "Decent.",
|
|
61
|
+
usage: {
|
|
62
|
+
input_tokens: 1000,
|
|
63
|
+
output_tokens: 100,
|
|
64
|
+
estimated_cost_usd: 0.004,
|
|
65
|
+
function_name: "judge",
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function createMockEngine(): EngineContext {
|
|
74
|
+
return {
|
|
75
|
+
config: {
|
|
76
|
+
evals: { threshold: 70 },
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
78
|
+
} as any,
|
|
79
|
+
github: {
|
|
80
|
+
getTree: vi.fn().mockResolvedValue([
|
|
81
|
+
{ path: "evals/prompts/case-study.md", sha: "abc", type: "blob" },
|
|
82
|
+
{ path: "evals/prompts/blog-post.md", sha: "def", type: "blob" },
|
|
83
|
+
{ path: "evals/standards/case-study.mdx", sha: "ghi", type: "blob" },
|
|
84
|
+
{ path: "evals/standards/blog-post.mdx", sha: "jkl", type: "blob" },
|
|
85
|
+
]),
|
|
86
|
+
getFile: vi.fn().mockImplementation(async (path: string) => {
|
|
87
|
+
if (path === "evals/results.tsv") {
|
|
88
|
+
return {
|
|
89
|
+
path,
|
|
90
|
+
sha: "sha1",
|
|
91
|
+
content:
|
|
92
|
+
"date\tcontent_type\tprompt_version\tscore\tstatus\treasoning\n2026-04-04\tcase-study\tv1\t52\tdiscard\tGeneric\n2026-04-04\tcase-study\tv2\t82\tkeep\tGood",
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (path === "evals/prompts/case-study.md") {
|
|
97
|
+
return {
|
|
98
|
+
path,
|
|
99
|
+
sha: "sha2",
|
|
100
|
+
content: "Write a compelling case study with specific metrics.",
|
|
101
|
+
encoding: "utf-8",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (path === "evals/judge.md") {
|
|
105
|
+
return {
|
|
106
|
+
path,
|
|
107
|
+
sha: "sha3",
|
|
108
|
+
content: "Score content 0-100. Focus on specificity, tone, and accuracy.",
|
|
109
|
+
encoding: "utf-8",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (path === "evals/standards/case-study.mdx") {
|
|
113
|
+
return {
|
|
114
|
+
path,
|
|
115
|
+
sha: "sha4",
|
|
116
|
+
content: "# Perfect Case Study\n\nDetailed metrics and client quotes.",
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}),
|
|
122
|
+
createOrUpdateFile: vi.fn().mockResolvedValue({
|
|
123
|
+
sha: "new-sha",
|
|
124
|
+
commit_sha: "commit-sha",
|
|
125
|
+
}),
|
|
126
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
127
|
+
} as any,
|
|
128
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
129
|
+
knowledge: {} as any,
|
|
130
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
131
|
+
knowledgeStore: {} as any,
|
|
132
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
133
|
+
cache: {} as any,
|
|
134
|
+
// biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
|
|
135
|
+
budget: {} as any,
|
|
136
|
+
provider: { provider: "anthropic", model: "claude-sonnet-4-5-20250514" },
|
|
137
|
+
listContent: vi.fn().mockResolvedValue([]),
|
|
138
|
+
getContent: vi.fn().mockResolvedValue(null),
|
|
139
|
+
getCollectionDef: vi.fn().mockReturnValue(null),
|
|
140
|
+
listCollections: vi.fn().mockReturnValue([]),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createTestApp(engine: EngineContext) {
|
|
145
|
+
const app = new Hono().basePath("/api");
|
|
146
|
+
app.use("*", errorHandler());
|
|
147
|
+
app.route("/", evalRoutes(engine));
|
|
148
|
+
return app;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe("Eval routes", () => {
|
|
152
|
+
let engine: EngineContext;
|
|
153
|
+
let app: Hono;
|
|
154
|
+
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
engine = createMockEngine();
|
|
157
|
+
app = createTestApp(engine);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("POST /api/eval/judge", () => {
|
|
161
|
+
it("judges a draft against gold standard", async () => {
|
|
162
|
+
const res = await app.request("/api/eval/judge", {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "Content-Type": "application/json" },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
draft: "# Case Study\n\nWe helped a client.",
|
|
167
|
+
gold_standard: "# Perfect Case Study\n\nMetrics included.",
|
|
168
|
+
judge_prompt: "Score 0-100.",
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
expect(res.status).toBe(200);
|
|
172
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
173
|
+
const body = (await res.json()) as any;
|
|
174
|
+
expect(body.score).toBeDefined();
|
|
175
|
+
expect(body.reasoning).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns 400 for missing fields", async () => {
|
|
179
|
+
const res = await app.request("/api/eval/judge", {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({ draft: "test" }),
|
|
183
|
+
});
|
|
184
|
+
expect(res.status).toBe(400);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("POST /api/eval/run", () => {
|
|
189
|
+
it("runs the full eval loop", async () => {
|
|
190
|
+
const res = await app.request("/api/eval/run", {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: { "Content-Type": "application/json" },
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
content_type: "case-study",
|
|
195
|
+
knowledge_context: "Acme migrated 12 services.",
|
|
196
|
+
gold_standard: "# Perfect Case Study",
|
|
197
|
+
judge_prompt: "Score 0-100.",
|
|
198
|
+
generation_prompt: "Write a case study.",
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
expect(res.status).toBe(200);
|
|
202
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
203
|
+
const body = (await res.json()) as any;
|
|
204
|
+
expect(body.final_score).toBeDefined();
|
|
205
|
+
expect(body.final_status).toBeDefined();
|
|
206
|
+
expect(body.iterations).toBeInstanceOf(Array);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns 400 for missing required fields", async () => {
|
|
210
|
+
const res = await app.request("/api/eval/run", {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/json" },
|
|
213
|
+
body: JSON.stringify({ content_type: "case-study" }),
|
|
214
|
+
});
|
|
215
|
+
expect(res.status).toBe(400);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("GET /api/eval/results", () => {
|
|
220
|
+
it("returns parsed eval results from TSV", async () => {
|
|
221
|
+
const res = await app.request("/api/eval/results");
|
|
222
|
+
expect(res.status).toBe(200);
|
|
223
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
224
|
+
const body = (await res.json()) as any;
|
|
225
|
+
expect(body.total).toBe(2);
|
|
226
|
+
expect(body.items[0].content_type).toBe("case-study");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("filters by content_type query param", async () => {
|
|
230
|
+
const res = await app.request("/api/eval/results?content_type=case-study");
|
|
231
|
+
expect(res.status).toBe(200);
|
|
232
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
233
|
+
const body = (await res.json()) as any;
|
|
234
|
+
expect(body.total).toBe(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("returns empty when results file not found", async () => {
|
|
238
|
+
vi.mocked(engine.github.getFile).mockResolvedValueOnce(null);
|
|
239
|
+
const res = await app.request("/api/eval/results");
|
|
240
|
+
expect(res.status).toBe(200);
|
|
241
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
242
|
+
const body = (await res.json()) as any;
|
|
243
|
+
expect(body.total).toBe(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("GET /api/eval/prompts", () => {
|
|
248
|
+
it("returns list of generation prompts", async () => {
|
|
249
|
+
const res = await app.request("/api/eval/prompts");
|
|
250
|
+
expect(res.status).toBe(200);
|
|
251
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
252
|
+
const body = (await res.json()) as any;
|
|
253
|
+
expect(body.total).toBe(2);
|
|
254
|
+
expect(body.items[0].name).toBe("case-study");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("GET /api/eval/prompts/:name", () => {
|
|
259
|
+
it("returns a specific generation prompt", async () => {
|
|
260
|
+
const res = await app.request("/api/eval/prompts/case-study");
|
|
261
|
+
expect(res.status).toBe(200);
|
|
262
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
263
|
+
const body = (await res.json()) as any;
|
|
264
|
+
expect(body.name).toBe("case-study");
|
|
265
|
+
expect(body.content).toContain("metrics");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("returns 404 for unknown prompt", async () => {
|
|
269
|
+
vi.mocked(engine.github.getFile).mockResolvedValueOnce(null);
|
|
270
|
+
const res = await app.request("/api/eval/prompts/nonexistent");
|
|
271
|
+
expect(res.status).toBe(404);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("PUT /api/eval/prompts/:name", () => {
|
|
276
|
+
it("updates a generation prompt", async () => {
|
|
277
|
+
const res = await app.request("/api/eval/prompts/case-study", {
|
|
278
|
+
method: "PUT",
|
|
279
|
+
headers: { "Content-Type": "application/json" },
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
content: "Improved prompt: write a case study with metrics and quotes.",
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
expect(res.status).toBe(200);
|
|
285
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
286
|
+
const body = (await res.json()) as any;
|
|
287
|
+
expect(body.updated).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("returns 400 for missing content", async () => {
|
|
291
|
+
const res = await app.request("/api/eval/prompts/case-study", {
|
|
292
|
+
method: "PUT",
|
|
293
|
+
headers: { "Content-Type": "application/json" },
|
|
294
|
+
body: JSON.stringify({}),
|
|
295
|
+
});
|
|
296
|
+
expect(res.status).toBe(400);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("GET /api/eval/judge-prompt", () => {
|
|
301
|
+
it("returns the judge.md prompt", async () => {
|
|
302
|
+
const res = await app.request("/api/eval/judge-prompt");
|
|
303
|
+
expect(res.status).toBe(200);
|
|
304
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
305
|
+
const body = (await res.json()) as any;
|
|
306
|
+
expect(body.content).toContain("Score");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("returns 404 when judge.md not found", async () => {
|
|
310
|
+
vi.mocked(engine.github.getFile).mockResolvedValueOnce(null);
|
|
311
|
+
const res = await app.request("/api/eval/judge-prompt");
|
|
312
|
+
expect(res.status).toBe(404);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("GET /api/eval/standards", () => {
|
|
317
|
+
it("returns list of gold standard files", async () => {
|
|
318
|
+
const res = await app.request("/api/eval/standards");
|
|
319
|
+
expect(res.status).toBe(200);
|
|
320
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
321
|
+
const body = (await res.json()) as any;
|
|
322
|
+
expect(body.total).toBe(2);
|
|
323
|
+
expect(body.items[0].name).toBe("case-study");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("GET /api/eval/standards/:name", () => {
|
|
328
|
+
it("returns a specific gold standard", async () => {
|
|
329
|
+
const res = await app.request("/api/eval/standards/case-study");
|
|
330
|
+
expect(res.status).toBe(200);
|
|
331
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
|
|
332
|
+
const body = (await res.json()) as any;
|
|
333
|
+
expect(body.name).toBe("case-study");
|
|
334
|
+
expect(body.content).toContain("Perfect");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("returns 404 for unknown standard", async () => {
|
|
338
|
+
vi.mocked(engine.github.getFile).mockImplementation(async () => null);
|
|
339
|
+
const res = await app.request("/api/eval/standards/nonexistent");
|
|
340
|
+
expect(res.status).toBe(404);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|