@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,182 @@
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 { graphRoutes } from "../routes/graph.js";
6
+
7
+ const mockGraph = {
8
+ entities: new Map([
9
+ [
10
+ "Acme Corp",
11
+ {
12
+ type: "client",
13
+ name: "Acme Corp",
14
+ aliases: ["Acme"],
15
+ confidence: 0.95,
16
+ source_file: "knowledge/clients/acme.md",
17
+ },
18
+ ],
19
+ [
20
+ "Next.js",
21
+ {
22
+ type: "technology",
23
+ name: "Next.js",
24
+ aliases: ["nextjs"],
25
+ confidence: 0.9,
26
+ source_file: "knowledge/clients/acme.md",
27
+ },
28
+ ],
29
+ ]),
30
+ relations: [
31
+ {
32
+ from_entity: "Acme Corp",
33
+ to_entity: "Next.js",
34
+ relation_type: "uses",
35
+ confidence: 0.8,
36
+ evidence: "Migration to Next.js",
37
+ source_file: "knowledge/clients/acme.md",
38
+ },
39
+ ],
40
+ clusters: [],
41
+ built_at: "2026-04-01T00:00:00Z",
42
+ file_count: 1,
43
+ };
44
+
45
+ function createMockEngine(): EngineContext {
46
+ return {
47
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
48
+ config: {} as any,
49
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
50
+ github: {} as any,
51
+ knowledge: {
52
+ getGraph: vi.fn().mockReturnValue(mockGraph),
53
+ query: vi.fn().mockImplementation((name: string) => {
54
+ const entity = mockGraph.entities.get(name);
55
+ if (!entity) return null;
56
+ return {
57
+ entity,
58
+ relations: mockGraph.relations.filter(
59
+ (r) => r.from_entity === name || r.to_entity === name,
60
+ ),
61
+ related_entities: [],
62
+ files: [entity.source_file],
63
+ };
64
+ }),
65
+ findStale: vi.fn().mockReturnValue([]),
66
+ findGaps: vi.fn().mockReturnValue([
67
+ {
68
+ entity_name: "Acme Corp",
69
+ entity_type: "client",
70
+ knowledge_file_count: 1,
71
+ content_file_count: 0,
72
+ reason: "No content for Acme Corp",
73
+ },
74
+ ]),
75
+ buildGraph: vi.fn().mockResolvedValue(mockGraph),
76
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
77
+ } as any,
78
+ knowledgeStore: {
79
+ list: vi.fn().mockResolvedValue([]),
80
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
81
+ } as any,
82
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
83
+ cache: {} as any,
84
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
85
+ budget: {} as any,
86
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
87
+ provider: {} as any,
88
+ listContent: vi.fn().mockResolvedValue([]),
89
+ getContent: vi.fn().mockResolvedValue(null),
90
+ getCollectionDef: vi.fn().mockReturnValue(null),
91
+ listCollections: vi.fn().mockReturnValue(["posts"]),
92
+ };
93
+ }
94
+
95
+ function createTestApp(engine: EngineContext) {
96
+ const app = new Hono().basePath("/api");
97
+ app.use("*", errorHandler());
98
+ app.route("/", graphRoutes(engine));
99
+ return app;
100
+ }
101
+
102
+ describe("Graph routes", () => {
103
+ let engine: EngineContext;
104
+ let app: Hono;
105
+
106
+ beforeEach(() => {
107
+ engine = createMockEngine();
108
+ app = createTestApp(engine);
109
+ });
110
+
111
+ describe("GET /api/graph", () => {
112
+ it("returns graph summary when graph exists", async () => {
113
+ const res = await app.request("/api/graph");
114
+ expect(res.status).toBe(200);
115
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
116
+ const body = (await res.json()) as any;
117
+ expect(body.status).toBe("built");
118
+ expect(body.entity_count).toBe(2);
119
+ expect(body.relation_count).toBe(1);
120
+ });
121
+
122
+ it("returns empty status when no graph", async () => {
123
+ // biome-ignore lint/suspicious/noExplicitAny: accessing vi mock method on typed interface
124
+ (engine.knowledge.getGraph as any).mockReturnValue(null);
125
+ const res = await app.request("/api/graph");
126
+ expect(res.status).toBe(200);
127
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
128
+ const body = (await res.json()) as any;
129
+ expect(body.status).toBe("empty");
130
+ });
131
+ });
132
+
133
+ describe("GET /api/graph/entity/:name", () => {
134
+ it("returns entity with relations", async () => {
135
+ const res = await app.request("/api/graph/entity/Acme%20Corp");
136
+ expect(res.status).toBe(200);
137
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
138
+ const body = (await res.json()) as any;
139
+ expect(body.entity.name).toBe("Acme Corp");
140
+ });
141
+
142
+ it("returns 404 for unknown entity", async () => {
143
+ const res = await app.request("/api/graph/entity/Unknown");
144
+ expect(res.status).toBe(404);
145
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
146
+ const body = (await res.json()) as any;
147
+ expect(body.code).toBe("ENTITY_NOT_FOUND");
148
+ });
149
+ });
150
+
151
+ describe("GET /api/graph/gaps", () => {
152
+ it("returns knowledge gaps", async () => {
153
+ const res = await app.request("/api/graph/gaps");
154
+ expect(res.status).toBe(200);
155
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
156
+ const body = (await res.json()) as any;
157
+ expect(body.total).toBe(1);
158
+ expect(body.items[0].entity_name).toBe("Acme Corp");
159
+ });
160
+ });
161
+
162
+ describe("GET /api/graph/stale", () => {
163
+ it("returns stale content list", async () => {
164
+ const res = await app.request("/api/graph/stale");
165
+ expect(res.status).toBe(200);
166
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
167
+ const body = (await res.json()) as any;
168
+ expect(body.total).toBe(0);
169
+ });
170
+ });
171
+
172
+ describe("POST /api/graph/rebuild", () => {
173
+ it("rebuilds graph and returns stats", async () => {
174
+ const res = await app.request("/api/graph/rebuild", { method: "POST" });
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.rebuilt).toBe(true);
179
+ expect(body.entity_count).toBe(2);
180
+ });
181
+ });
182
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createApp } from "../app.js";
3
+ import type { HealthStatus } from "../routes/health.js";
4
+
5
+ describe("Health endpoint", () => {
6
+ const app = createApp();
7
+
8
+ it("GET /api/health returns 200 with status ok", async () => {
9
+ const res = await app.request("/api/health");
10
+ expect(res.status).toBe(200);
11
+
12
+ const body = (await res.json()) as HealthStatus;
13
+ expect(body.status).toBe("ok");
14
+ expect(body.version).toBe("0.1.0");
15
+ expect(body.timestamp).toBeDefined();
16
+ expect(body.services).toBeDefined();
17
+ });
18
+
19
+ it("returns correct service status fields", async () => {
20
+ const res = await app.request("/api/health");
21
+ const body = (await res.json()) as HealthStatus;
22
+
23
+ expect(body.services.github).toBe("unknown");
24
+ expect(body.services.knowledge).toBe("unknown");
25
+ expect(body.services.cache).toBe("unknown");
26
+ });
27
+ });
28
+
29
+ describe("CORS middleware", () => {
30
+ const app = createApp();
31
+
32
+ it("responds to OPTIONS with CORS headers", async () => {
33
+ const res = await app.request("/api/health", {
34
+ method: "OPTIONS",
35
+ headers: {
36
+ Origin: "http://localhost:3000",
37
+ "Access-Control-Request-Method": "GET",
38
+ },
39
+ });
40
+ // CORS now denies by default and only reflects allowed origins, not wildcard
41
+ expect(res.headers.get("access-control-allow-origin")).toBe("http://localhost:3000");
42
+ });
43
+ });
44
+
45
+ describe("Error handler", () => {
46
+ const app = createApp();
47
+
48
+ it("returns 404 for unknown routes", async () => {
49
+ const res = await app.request("/api/nonexistent");
50
+ expect(res.status).toBe(404);
51
+ });
52
+ });
53
+
54
+ describe("Auth middleware", () => {
55
+ const app = createApp();
56
+
57
+ it("allows requests without auth header", async () => {
58
+ const res = await app.request("/api/health");
59
+ expect(res.status).toBe(200);
60
+ });
61
+
62
+ it("allows requests with Bearer token", async () => {
63
+ const res = await app.request("/api/health", {
64
+ headers: { Authorization: "Bearer test-token" },
65
+ });
66
+ expect(res.status).toBe(200);
67
+ });
68
+ });
@@ -0,0 +1,148 @@
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 { importRoutes } from "../routes/import.js";
6
+
7
+ function createMockEngine(): EngineContext {
8
+ return {
9
+ knowledge: {
10
+ importUrl: vi.fn().mockResolvedValue({
11
+ path: "knowledge/example.com/page.md",
12
+ type: "article",
13
+ quality: "structured",
14
+ quality_score: 0.8,
15
+ entities: [{ type: "company", name: "Acme" }],
16
+ source: "url",
17
+ source_url: "https://example.com/page",
18
+ body: "content",
19
+ ingested_at: "2026-04-04T10:00:00Z",
20
+ }),
21
+ parseSitemap: vi.fn().mockResolvedValue({
22
+ sitemap_url: "https://example.com/sitemap.xml",
23
+ sections: [
24
+ { name: "Blog", pattern: "/blog/*", urls: ["https://example.com/blog/1"], count: 1 },
25
+ {
26
+ name: "Services",
27
+ pattern: "/services/*",
28
+ urls: ["https://example.com/services/web"],
29
+ count: 1,
30
+ },
31
+ ],
32
+ total_urls: 2,
33
+ }),
34
+ filterSitemapUrls: vi.fn().mockReturnValue(["https://example.com/services/web"]),
35
+ importBatch: vi.fn(),
36
+ },
37
+ jobs: {
38
+ enqueue: vi.fn().mockResolvedValue("job-1"),
39
+ status: vi.fn().mockResolvedValue({
40
+ job_id: "job-1",
41
+ type: "import-batch",
42
+ status: "queued",
43
+ progress: { completed: 0, total: 0, failed: 0 },
44
+ created_at: "2026-04-04T10:00:00Z",
45
+ }),
46
+ register: vi.fn(),
47
+ cancel: vi.fn(),
48
+ list: vi.fn(),
49
+ },
50
+ } as unknown as EngineContext;
51
+ }
52
+
53
+ function createTestApp(engine: EngineContext) {
54
+ const app = new Hono().basePath("/api");
55
+ app.use("*", errorHandler());
56
+ app.route("/", importRoutes(engine));
57
+ return app;
58
+ }
59
+
60
+ describe("Import routes", () => {
61
+ let engine: EngineContext;
62
+ let app: Hono;
63
+
64
+ beforeEach(() => {
65
+ engine = createMockEngine();
66
+ app = createTestApp(engine);
67
+ });
68
+
69
+ describe("POST /knowledge/import", () => {
70
+ it("imports a single URL", async () => {
71
+ const res = await app.request("/api/knowledge/import", {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: JSON.stringify({ url: "https://example.com/page" }),
75
+ });
76
+ expect(res.status).toBe(201);
77
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
78
+ const data = (await res.json()) as any;
79
+ expect(data.imported).toBe(true);
80
+ expect(data.path).toBe("knowledge/example.com/page.md");
81
+ });
82
+
83
+ it("rejects missing url", async () => {
84
+ const res = await app.request("/api/knowledge/import", {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({}),
88
+ });
89
+ expect(res.status).toBe(400);
90
+ });
91
+ });
92
+
93
+ describe("POST /knowledge/import/batch", () => {
94
+ it("enqueues a batch import job", async () => {
95
+ const res = await app.request("/api/knowledge/import/batch", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({ urls: ["https://example.com/a", "https://example.com/b"] }),
99
+ });
100
+ expect(res.status).toBe(202);
101
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
102
+ const data = (await res.json()) as any;
103
+ expect(data.job_id).toBe("job-1");
104
+ expect(data.type).toBe("import-batch");
105
+ });
106
+
107
+ it("rejects empty urls array", async () => {
108
+ const res = await app.request("/api/knowledge/import/batch", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ urls: [] }),
112
+ });
113
+ expect(res.status).toBe(400);
114
+ });
115
+ });
116
+
117
+ describe("POST /knowledge/import/sitemap", () => {
118
+ it("parses a sitemap and returns sections", async () => {
119
+ const res = await app.request("/api/knowledge/import/sitemap", {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({ url: "https://example.com/sitemap.xml" }),
123
+ });
124
+ expect(res.status).toBe(200);
125
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
126
+ const data = (await res.json()) as any;
127
+ expect(data.total_urls).toBe(2);
128
+ expect(data.sections).toHaveLength(2);
129
+ });
130
+ });
131
+
132
+ describe("POST /knowledge/import/sitemap/run", () => {
133
+ it("enqueues a sitemap import job", async () => {
134
+ const res = await app.request("/api/knowledge/import/sitemap/run", {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json" },
137
+ body: JSON.stringify({
138
+ sitemap_url: "https://example.com/sitemap.xml",
139
+ include: ["/services/*"],
140
+ }),
141
+ });
142
+ expect(res.status).toBe(202);
143
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
144
+ const data = (await res.json()) as any;
145
+ expect(data.job_id).toBe("job-1");
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,133 @@
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 { intentRoutes } from "../routes/intent.js";
6
+
7
+ function createMockEngine(): EngineContext {
8
+ return {
9
+ config: {
10
+ intent: { path: "intent/" },
11
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
12
+ } as any,
13
+ github: {
14
+ getTree: vi.fn().mockResolvedValue([
15
+ { path: "intent/tone-of-voice.md", sha: "abc", type: "blob" },
16
+ { path: "intent/brand-rules.md", sha: "def", type: "blob" },
17
+ ]),
18
+ getFile: vi.fn().mockImplementation(async (path: string) => {
19
+ if (path === "intent/tone-of-voice.md") {
20
+ return {
21
+ path: "intent/tone-of-voice.md",
22
+ sha: "abc123",
23
+ content: "# Tone of Voice\n\n- Vi-form\n- Konkreta resultat",
24
+ encoding: "utf-8",
25
+ };
26
+ }
27
+ return null;
28
+ }),
29
+ createOrUpdateFile: vi.fn().mockResolvedValue({
30
+ sha: "new-sha",
31
+ commit_sha: "commit-sha",
32
+ }),
33
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
34
+ } as any,
35
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
36
+ knowledge: {} as any,
37
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
38
+ knowledgeStore: {} as any,
39
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
40
+ cache: {} as any,
41
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
42
+ budget: {} as any,
43
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
44
+ provider: {} as any,
45
+ listContent: vi.fn().mockResolvedValue([]),
46
+ getContent: vi.fn().mockResolvedValue(null),
47
+ getCollectionDef: vi.fn().mockReturnValue(null),
48
+ listCollections: vi.fn().mockReturnValue(["posts"]),
49
+ };
50
+ }
51
+
52
+ function createTestApp(engine: EngineContext) {
53
+ const app = new Hono().basePath("/api");
54
+ app.use("*", errorHandler());
55
+ app.route("/", intentRoutes(engine));
56
+ return app;
57
+ }
58
+
59
+ describe("Intent routes", () => {
60
+ let engine: EngineContext;
61
+ let app: Hono;
62
+
63
+ beforeEach(() => {
64
+ engine = createMockEngine();
65
+ app = createTestApp(engine);
66
+ });
67
+
68
+ describe("GET /api/intent", () => {
69
+ it("returns list of intent files", async () => {
70
+ const res = await app.request("/api/intent");
71
+ expect(res.status).toBe(200);
72
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
73
+ const body = (await res.json()) as any;
74
+ expect(body.total).toBe(2);
75
+ expect(body.items[0].name).toBe("tone-of-voice");
76
+ });
77
+ });
78
+
79
+ describe("GET /api/intent/:name", () => {
80
+ it("returns intent file content", async () => {
81
+ const res = await app.request("/api/intent/tone-of-voice");
82
+ expect(res.status).toBe(200);
83
+ // biome-ignore lint/suspicious/noExplicitAny: typed JSON response parsing in tests
84
+ const body = (await res.json()) as any;
85
+ expect(body.name).toBe("tone-of-voice");
86
+ expect(body.content).toContain("Vi-form");
87
+ });
88
+
89
+ it("returns 404 for unknown intent", async () => {
90
+ const res = await app.request("/api/intent/nonexistent");
91
+ expect(res.status).toBe(404);
92
+ });
93
+ });
94
+
95
+ describe("PUT /api/intent/:name", () => {
96
+ it("updates intent file", async () => {
97
+ const res = await app.request("/api/intent/tone-of-voice", {
98
+ method: "PUT",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ content: "# Updated tone\n\n- New rules" }),
101
+ });
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.updated).toBe(true);
106
+ });
107
+
108
+ it("returns 400 for missing content", async () => {
109
+ const res = await app.request("/api/intent/tone-of-voice", {
110
+ method: "PUT",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({}),
113
+ });
114
+ expect(res.status).toBe(400);
115
+ });
116
+ });
117
+
118
+ describe("GET /api/intent/:name/impact", () => {
119
+ it("returns impact analysis placeholder", async () => {
120
+ const res = await app.request("/api/intent/tone-of-voice/impact");
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.intent).toBe("tone-of-voice");
125
+ expect(body.content_count).toBeDefined();
126
+ });
127
+
128
+ it("returns 404 for unknown intent", async () => {
129
+ const res = await app.request("/api/intent/nonexistent/impact");
130
+ expect(res.status).toBe(404);
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,107 @@
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 { jobRoutes } from "../routes/jobs.js";
6
+
7
+ function createMockEngine(withJobs = true): EngineContext {
8
+ const jobs = withJobs
9
+ ? {
10
+ enqueue: vi.fn().mockResolvedValue("job-1"),
11
+ status: vi.fn().mockResolvedValue({
12
+ job_id: "job-1",
13
+ type: "rebuild-graph",
14
+ status: "completed",
15
+ progress: { completed: 1, total: 1, failed: 0 },
16
+ created_at: "2026-04-04T10:00:00Z",
17
+ completed_at: "2026-04-04T10:01:00Z",
18
+ }),
19
+ cancel: vi.fn().mockResolvedValue(undefined),
20
+ list: vi.fn().mockResolvedValue([
21
+ {
22
+ job_id: "job-1",
23
+ type: "rebuild-graph",
24
+ status: "completed",
25
+ progress: { completed: 1, total: 1, failed: 0 },
26
+ created_at: "2026-04-04T10:00:00Z",
27
+ },
28
+ {
29
+ job_id: "job-2",
30
+ type: "sync",
31
+ status: "running",
32
+ progress: { completed: 5, total: 10, failed: 0 },
33
+ created_at: "2026-04-04T10:02:00Z",
34
+ },
35
+ ]),
36
+ register: vi.fn(),
37
+ }
38
+ : undefined;
39
+
40
+ return { jobs } as unknown as EngineContext;
41
+ }
42
+
43
+ function createTestApp(engine: EngineContext) {
44
+ const app = new Hono().basePath("/api");
45
+ app.use("*", errorHandler());
46
+ app.route("/", jobRoutes(engine));
47
+ return app;
48
+ }
49
+
50
+ describe("Job routes", () => {
51
+ let engine: EngineContext;
52
+ let app: Hono;
53
+
54
+ beforeEach(() => {
55
+ engine = createMockEngine();
56
+ app = createTestApp(engine);
57
+ });
58
+
59
+ it("GET /api/jobs lists all jobs", async () => {
60
+ const res = await app.request("/api/jobs");
61
+ expect(res.status).toBe(200);
62
+ const body = (await res.json()) as Record<string, unknown>;
63
+ expect(body.total).toBe(2);
64
+ });
65
+
66
+ it("GET /api/jobs/:id returns job status", async () => {
67
+ const res = await app.request("/api/jobs/job-1");
68
+ expect(res.status).toBe(200);
69
+ const body = (await res.json()) as Record<string, unknown>;
70
+ expect(body.job_id).toBe("job-1");
71
+ expect(body.status).toBe("completed");
72
+ });
73
+
74
+ it("POST /api/jobs starts a new job", async () => {
75
+ const res = await app.request("/api/jobs", {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ type: "rebuild-graph" }),
79
+ });
80
+ expect(res.status).toBe(201);
81
+ const body = (await res.json()) as Record<string, unknown>;
82
+ expect(body.job_id).toBe("job-1");
83
+ });
84
+
85
+ it("POST /api/jobs returns 400 without type", async () => {
86
+ const res = await app.request("/api/jobs", {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({}),
90
+ });
91
+ expect(res.status).toBe(400);
92
+ });
93
+
94
+ it("DELETE /api/jobs/:id cancels a job", async () => {
95
+ const res = await app.request("/api/jobs/job-1", { method: "DELETE" });
96
+ expect(res.status).toBe(200);
97
+ const body = (await res.json()) as Record<string, unknown>;
98
+ expect(body.cancelled).toBe(true);
99
+ });
100
+
101
+ it("returns 501 when jobs not configured", async () => {
102
+ const noJobsEngine = createMockEngine(false);
103
+ const noJobsApp = createTestApp(noJobsEngine);
104
+ const res = await noJobsApp.request("/api/jobs");
105
+ expect(res.status).toBe(501);
106
+ });
107
+ });