@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.
Files changed (184) hide show
  1. package/.omc/state/agent-replay-31d84b63-606a-4368-b9e6-93fe4f5ae0f7.jsonl +2 -0
  2. package/.omc/state/last-tool-error.json +7 -0
  3. package/.omc/state/mission-state.json +53 -0
  4. package/.omc/state/subagent-tracking.json +17 -0
  5. package/.turbo/turbo-build.log +4 -0
  6. package/.turbo/turbo-test.log +26 -0
  7. package/dist/__tests__/app-integration.test.d.ts +2 -0
  8. package/dist/__tests__/app-integration.test.d.ts.map +1 -0
  9. package/dist/__tests__/app-integration.test.js +71 -0
  10. package/dist/__tests__/app-integration.test.js.map +1 -0
  11. package/dist/__tests__/approval.test.d.ts +2 -0
  12. package/dist/__tests__/approval.test.d.ts.map +1 -0
  13. package/dist/__tests__/approval.test.js +170 -0
  14. package/dist/__tests__/approval.test.js.map +1 -0
  15. package/dist/__tests__/content.test.d.ts +2 -0
  16. package/dist/__tests__/content.test.d.ts.map +1 -0
  17. package/dist/__tests__/content.test.js +187 -0
  18. package/dist/__tests__/content.test.js.map +1 -0
  19. package/dist/__tests__/engine.test.d.ts +2 -0
  20. package/dist/__tests__/engine.test.d.ts.map +1 -0
  21. package/dist/__tests__/engine.test.js +77 -0
  22. package/dist/__tests__/engine.test.js.map +1 -0
  23. package/dist/__tests__/eval.test.d.ts +2 -0
  24. package/dist/__tests__/eval.test.d.ts.map +1 -0
  25. package/dist/__tests__/eval.test.js +320 -0
  26. package/dist/__tests__/eval.test.js.map +1 -0
  27. package/dist/__tests__/graph.test.d.ts +2 -0
  28. package/dist/__tests__/graph.test.d.ts.map +1 -0
  29. package/dist/__tests__/graph.test.js +169 -0
  30. package/dist/__tests__/graph.test.js.map +1 -0
  31. package/dist/__tests__/health.test.d.ts +2 -0
  32. package/dist/__tests__/health.test.d.ts.map +1 -0
  33. package/dist/__tests__/health.test.js +56 -0
  34. package/dist/__tests__/health.test.js.map +1 -0
  35. package/dist/__tests__/import.test.d.ts +2 -0
  36. package/dist/__tests__/import.test.d.ts.map +1 -0
  37. package/dist/__tests__/import.test.js +138 -0
  38. package/dist/__tests__/import.test.js.map +1 -0
  39. package/dist/__tests__/intent.test.d.ts +2 -0
  40. package/dist/__tests__/intent.test.d.ts.map +1 -0
  41. package/dist/__tests__/intent.test.js +122 -0
  42. package/dist/__tests__/intent.test.js.map +1 -0
  43. package/dist/__tests__/jobs.test.d.ts +2 -0
  44. package/dist/__tests__/jobs.test.d.ts.map +1 -0
  45. package/dist/__tests__/jobs.test.js +96 -0
  46. package/dist/__tests__/jobs.test.js.map +1 -0
  47. package/dist/__tests__/knowledge.test.d.ts +2 -0
  48. package/dist/__tests__/knowledge.test.d.ts.map +1 -0
  49. package/dist/__tests__/knowledge.test.js +110 -0
  50. package/dist/__tests__/knowledge.test.js.map +1 -0
  51. package/dist/__tests__/media-routes.test.d.ts +2 -0
  52. package/dist/__tests__/media-routes.test.d.ts.map +1 -0
  53. package/dist/__tests__/media-routes.test.js +88 -0
  54. package/dist/__tests__/media-routes.test.js.map +1 -0
  55. package/dist/__tests__/schema.test.d.ts +2 -0
  56. package/dist/__tests__/schema.test.d.ts.map +1 -0
  57. package/dist/__tests__/schema.test.js +92 -0
  58. package/dist/__tests__/schema.test.js.map +1 -0
  59. package/dist/app.d.ts +7 -0
  60. package/dist/app.d.ts.map +1 -0
  61. package/dist/app.js +85 -0
  62. package/dist/app.js.map +1 -0
  63. package/dist/engine.d.ts +38 -0
  64. package/dist/engine.d.ts.map +1 -0
  65. package/dist/engine.js +106 -0
  66. package/dist/engine.js.map +1 -0
  67. package/dist/index.d.ts +8 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +9 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/middleware/auth.d.ts +17 -0
  72. package/dist/middleware/auth.d.ts.map +1 -0
  73. package/dist/middleware/auth.js +54 -0
  74. package/dist/middleware/auth.js.map +1 -0
  75. package/dist/middleware/cors.d.ts +2 -0
  76. package/dist/middleware/cors.d.ts.map +1 -0
  77. package/dist/middleware/cors.js +13 -0
  78. package/dist/middleware/cors.js.map +1 -0
  79. package/dist/middleware/error-handler.d.ts +24 -0
  80. package/dist/middleware/error-handler.d.ts.map +1 -0
  81. package/dist/middleware/error-handler.js +30 -0
  82. package/dist/middleware/error-handler.js.map +1 -0
  83. package/dist/middleware/index.d.ts +7 -0
  84. package/dist/middleware/index.d.ts.map +1 -0
  85. package/dist/middleware/index.js +5 -0
  86. package/dist/middleware/index.js.map +1 -0
  87. package/dist/middleware/rate-limit.d.ts +11 -0
  88. package/dist/middleware/rate-limit.d.ts.map +1 -0
  89. package/dist/middleware/rate-limit.js +26 -0
  90. package/dist/middleware/rate-limit.js.map +1 -0
  91. package/dist/middleware/route-error-handler.d.ts +12 -0
  92. package/dist/middleware/route-error-handler.d.ts.map +1 -0
  93. package/dist/middleware/route-error-handler.js +9 -0
  94. package/dist/middleware/route-error-handler.js.map +1 -0
  95. package/dist/routes/approval.d.ts +4 -0
  96. package/dist/routes/approval.d.ts.map +1 -0
  97. package/dist/routes/approval.js +70 -0
  98. package/dist/routes/approval.js.map +1 -0
  99. package/dist/routes/content.d.ts +4 -0
  100. package/dist/routes/content.d.ts.map +1 -0
  101. package/dist/routes/content.js +145 -0
  102. package/dist/routes/content.js.map +1 -0
  103. package/dist/routes/eval.d.ts +4 -0
  104. package/dist/routes/eval.d.ts.map +1 -0
  105. package/dist/routes/eval.js +178 -0
  106. package/dist/routes/eval.js.map +1 -0
  107. package/dist/routes/graph.d.ts +4 -0
  108. package/dist/routes/graph.d.ts.map +1 -0
  109. package/dist/routes/graph.js +90 -0
  110. package/dist/routes/graph.js.map +1 -0
  111. package/dist/routes/health.d.ts +13 -0
  112. package/dist/routes/health.d.ts.map +1 -0
  113. package/dist/routes/health.js +19 -0
  114. package/dist/routes/health.js.map +1 -0
  115. package/dist/routes/import.d.ts +4 -0
  116. package/dist/routes/import.d.ts.map +1 -0
  117. package/dist/routes/import.js +85 -0
  118. package/dist/routes/import.js.map +1 -0
  119. package/dist/routes/index.d.ts +12 -0
  120. package/dist/routes/index.d.ts.map +1 -0
  121. package/dist/routes/index.js +12 -0
  122. package/dist/routes/index.js.map +1 -0
  123. package/dist/routes/intent.d.ts +4 -0
  124. package/dist/routes/intent.d.ts.map +1 -0
  125. package/dist/routes/intent.js +80 -0
  126. package/dist/routes/intent.js.map +1 -0
  127. package/dist/routes/jobs.d.ts +4 -0
  128. package/dist/routes/jobs.d.ts.map +1 -0
  129. package/dist/routes/jobs.js +67 -0
  130. package/dist/routes/jobs.js.map +1 -0
  131. package/dist/routes/knowledge.d.ts +4 -0
  132. package/dist/routes/knowledge.d.ts.map +1 -0
  133. package/dist/routes/knowledge.js +48 -0
  134. package/dist/routes/knowledge.js.map +1 -0
  135. package/dist/routes/media.d.ts +4 -0
  136. package/dist/routes/media.d.ts.map +1 -0
  137. package/dist/routes/media.js +87 -0
  138. package/dist/routes/media.js.map +1 -0
  139. package/dist/routes/schema.d.ts +4 -0
  140. package/dist/routes/schema.d.ts.map +1 -0
  141. package/dist/routes/schema.js +54 -0
  142. package/dist/routes/schema.js.map +1 -0
  143. package/dist/standalone.d.ts +2 -0
  144. package/dist/standalone.d.ts.map +1 -0
  145. package/dist/standalone.js +115 -0
  146. package/dist/standalone.js.map +1 -0
  147. package/package.json +36 -0
  148. package/src/__tests__/app-integration.test.ts +80 -0
  149. package/src/__tests__/approval.test.ts +195 -0
  150. package/src/__tests__/content.test.ts +202 -0
  151. package/src/__tests__/engine.test.ts +86 -0
  152. package/src/__tests__/eval.test.ts +343 -0
  153. package/src/__tests__/graph.test.ts +182 -0
  154. package/src/__tests__/health.test.ts +68 -0
  155. package/src/__tests__/import.test.ts +148 -0
  156. package/src/__tests__/intent.test.ts +133 -0
  157. package/src/__tests__/jobs.test.ts +107 -0
  158. package/src/__tests__/knowledge.test.ts +121 -0
  159. package/src/__tests__/media-routes.test.ts +109 -0
  160. package/src/__tests__/schema.test.ts +100 -0
  161. package/src/app.ts +92 -0
  162. package/src/engine.ts +168 -0
  163. package/src/index.ts +31 -0
  164. package/src/middleware/auth.ts +66 -0
  165. package/src/middleware/cors.ts +15 -0
  166. package/src/middleware/error-handler.ts +42 -0
  167. package/src/middleware/index.ts +6 -0
  168. package/src/middleware/rate-limit.ts +27 -0
  169. package/src/middleware/route-error-handler.ts +13 -0
  170. package/src/routes/approval.ts +90 -0
  171. package/src/routes/content.ts +256 -0
  172. package/src/routes/eval.ts +262 -0
  173. package/src/routes/graph.ts +111 -0
  174. package/src/routes/health.ts +33 -0
  175. package/src/routes/import.ts +122 -0
  176. package/src/routes/index.ts +11 -0
  177. package/src/routes/intent.ts +105 -0
  178. package/src/routes/jobs.ts +84 -0
  179. package/src/routes/knowledge.ts +73 -0
  180. package/src/routes/media.ts +117 -0
  181. package/src/routes/schema.ts +75 -0
  182. package/src/standalone.ts +130 -0
  183. package/tsconfig.json +8 -0
  184. 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
+ });