@mainahq/core 1.0.2 → 1.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/package.json +1 -1
- package/src/ai/__tests__/availability.test.ts +131 -0
- package/src/ai/__tests__/delegation.test.ts +55 -1
- package/src/ai/availability.ts +23 -0
- package/src/ai/delegation.ts +5 -3
- package/src/context/__tests__/budget.test.ts +29 -6
- package/src/context/__tests__/engine.test.ts +1 -0
- package/src/context/__tests__/selector.test.ts +23 -3
- package/src/context/__tests__/wiki.test.ts +349 -0
- package/src/context/budget.ts +12 -8
- package/src/context/engine.ts +37 -0
- package/src/context/selector.ts +30 -4
- package/src/context/wiki.ts +296 -0
- package/src/db/index.ts +12 -0
- package/src/feedback/__tests__/capture.test.ts +166 -0
- package/src/feedback/__tests__/signals.test.ts +144 -0
- package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
- package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
- package/src/feedback/capture.ts +102 -0
- package/src/feedback/signals.ts +68 -0
- package/src/index.ts +108 -1
- package/src/init/__tests__/init.test.ts +477 -18
- package/src/init/index.ts +419 -13
- package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
- package/src/prompts/defaults/index.ts +3 -1
- package/src/prompts/defaults/wiki-compile.md +20 -0
- package/src/prompts/defaults/wiki-query.md +18 -0
- package/src/stats/__tests__/tool-usage.test.ts +133 -0
- package/src/stats/tracker.ts +92 -0
- package/src/verify/__tests__/builtin.test.ts +270 -0
- package/src/verify/__tests__/pipeline.test.ts +11 -8
- package/src/verify/builtin.ts +350 -0
- package/src/verify/pipeline.ts +32 -2
- package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
- package/src/verify/tools/wiki-lint-runner.ts +38 -0
- package/src/verify/tools/wiki-lint.ts +898 -0
- package/src/wiki/__tests__/compiler.test.ts +389 -0
- package/src/wiki/__tests__/extractors/code.test.ts +99 -0
- package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
- package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
- package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
- package/src/wiki/__tests__/graph.test.ts +344 -0
- package/src/wiki/__tests__/hooks.test.ts +119 -0
- package/src/wiki/__tests__/indexer.test.ts +285 -0
- package/src/wiki/__tests__/linker.test.ts +230 -0
- package/src/wiki/__tests__/louvain.test.ts +229 -0
- package/src/wiki/__tests__/query.test.ts +316 -0
- package/src/wiki/__tests__/schema.test.ts +114 -0
- package/src/wiki/__tests__/signals.test.ts +474 -0
- package/src/wiki/__tests__/state.test.ts +168 -0
- package/src/wiki/__tests__/tracking.test.ts +118 -0
- package/src/wiki/__tests__/types.test.ts +387 -0
- package/src/wiki/compiler.ts +1075 -0
- package/src/wiki/extractors/code.ts +90 -0
- package/src/wiki/extractors/decision.ts +217 -0
- package/src/wiki/extractors/feature.ts +206 -0
- package/src/wiki/extractors/workflow.ts +112 -0
- package/src/wiki/graph.ts +445 -0
- package/src/wiki/hooks.ts +49 -0
- package/src/wiki/indexer.ts +105 -0
- package/src/wiki/linker.ts +117 -0
- package/src/wiki/louvain.ts +190 -0
- package/src/wiki/prompts/compile-architecture.md +59 -0
- package/src/wiki/prompts/compile-decision.md +66 -0
- package/src/wiki/prompts/compile-entity.md +56 -0
- package/src/wiki/prompts/compile-feature.md +60 -0
- package/src/wiki/prompts/compile-module.md +42 -0
- package/src/wiki/prompts/wiki-query.md +25 -0
- package/src/wiki/query.ts +338 -0
- package/src/wiki/schema.ts +111 -0
- package/src/wiki/signals.ts +368 -0
- package/src/wiki/state.ts +89 -0
- package/src/wiki/tracking.ts +30 -0
- package/src/wiki/types.ts +169 -0
- package/src/workflow/context.ts +26 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { CodeEntity } from "../extractors/code";
|
|
3
|
+
import type { KnowledgeGraph } from "../graph";
|
|
4
|
+
import { buildKnowledgeGraph, computePageRank, mapToArticles } from "../graph";
|
|
5
|
+
import type {
|
|
6
|
+
ExtractedDecision,
|
|
7
|
+
ExtractedFeature,
|
|
8
|
+
ExtractedWorkflowTrace,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
// ─── Test Fixtures ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeEntities(): CodeEntity[] {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: "runPipeline",
|
|
17
|
+
kind: "function",
|
|
18
|
+
file: "src/verify/pipeline.ts",
|
|
19
|
+
line: 10,
|
|
20
|
+
exported: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "syntaxGuard",
|
|
24
|
+
kind: "function",
|
|
25
|
+
file: "src/verify/syntax.ts",
|
|
26
|
+
line: 5,
|
|
27
|
+
exported: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "CacheManager",
|
|
31
|
+
kind: "class",
|
|
32
|
+
file: "src/cache/manager.ts",
|
|
33
|
+
line: 1,
|
|
34
|
+
exported: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "hashContent",
|
|
38
|
+
kind: "function",
|
|
39
|
+
file: "src/cache/hash.ts",
|
|
40
|
+
line: 3,
|
|
41
|
+
exported: true,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeFeatures(): ExtractedFeature[] {
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
id: "001-auth",
|
|
50
|
+
title: "Authentication",
|
|
51
|
+
scope: "Auth module",
|
|
52
|
+
specQualityScore: 0.8,
|
|
53
|
+
specAssertions: ["JWT tokens expire after 1 hour"],
|
|
54
|
+
tasks: [{ id: "T001", description: "Implement JWT", completed: true }],
|
|
55
|
+
entitiesModified: ["runPipeline"],
|
|
56
|
+
decisionsCreated: ["0001-jwt"],
|
|
57
|
+
branch: "feat/auth",
|
|
58
|
+
prNumber: 1,
|
|
59
|
+
merged: true,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeDecisions(): ExtractedDecision[] {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
id: "0001-jwt",
|
|
68
|
+
title: "Use JWT for Auth",
|
|
69
|
+
status: "accepted",
|
|
70
|
+
context: "Need stateless auth",
|
|
71
|
+
decision: "Use JWT tokens",
|
|
72
|
+
rationale: "Scalable and stateless",
|
|
73
|
+
alternativesRejected: ["Sessions", "OAuth only"],
|
|
74
|
+
entityMentions: ["src/verify/pipeline.ts"],
|
|
75
|
+
constitutionAlignment: ["security-first"],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeTraces(): ExtractedWorkflowTrace[] {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
featureId: "001-auth",
|
|
84
|
+
steps: [
|
|
85
|
+
{
|
|
86
|
+
command: "brainstorm",
|
|
87
|
+
timestamp: "2026-04-07T10:00:00.000Z",
|
|
88
|
+
summary: "Explored auth approaches",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
wikiRefsRead: [],
|
|
92
|
+
wikiRefsWritten: [],
|
|
93
|
+
rlSignals: [],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("Knowledge Graph", () => {
|
|
101
|
+
describe("buildKnowledgeGraph", () => {
|
|
102
|
+
it("should create nodes for code entities", () => {
|
|
103
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
104
|
+
expect(graph.nodes.has("entity:runPipeline")).toBe(true);
|
|
105
|
+
expect(graph.nodes.has("entity:syntaxGuard")).toBe(true);
|
|
106
|
+
expect(graph.nodes.has("entity:CacheManager")).toBe(true);
|
|
107
|
+
expect(graph.nodes.has("entity:hashContent")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should create module nodes from entity file paths", () => {
|
|
111
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
112
|
+
expect(graph.nodes.has("module:verify")).toBe(true);
|
|
113
|
+
expect(graph.nodes.has("module:cache")).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should create member_of edges between entities and modules", () => {
|
|
117
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
118
|
+
const memberOfEdges = graph.edges.filter((e) => e.type === "member_of");
|
|
119
|
+
expect(memberOfEdges.length).toBeGreaterThan(0);
|
|
120
|
+
|
|
121
|
+
const pipelineToVerify = memberOfEdges.find(
|
|
122
|
+
(e) =>
|
|
123
|
+
e.source === "entity:runPipeline" && e.target === "module:verify",
|
|
124
|
+
);
|
|
125
|
+
expect(pipelineToVerify).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should create feature nodes", () => {
|
|
129
|
+
const graph = buildKnowledgeGraph(makeEntities(), makeFeatures(), [], []);
|
|
130
|
+
expect(graph.nodes.has("feature:001-auth")).toBe(true);
|
|
131
|
+
expect(graph.nodes.get("feature:001-auth")?.label).toBe("Authentication");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should create decision nodes", () => {
|
|
135
|
+
const graph = buildKnowledgeGraph(
|
|
136
|
+
makeEntities(),
|
|
137
|
+
[],
|
|
138
|
+
makeDecisions(),
|
|
139
|
+
[],
|
|
140
|
+
);
|
|
141
|
+
expect(graph.nodes.has("decision:0001-jwt")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should create workflow nodes", () => {
|
|
145
|
+
const graph = buildKnowledgeGraph(
|
|
146
|
+
makeEntities(),
|
|
147
|
+
makeFeatures(),
|
|
148
|
+
[],
|
|
149
|
+
makeTraces(),
|
|
150
|
+
);
|
|
151
|
+
expect(graph.nodes.has("workflow:001-auth")).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should create edges for all 11 edge types", () => {
|
|
155
|
+
const graph = buildKnowledgeGraph(
|
|
156
|
+
makeEntities(),
|
|
157
|
+
makeFeatures(),
|
|
158
|
+
makeDecisions(),
|
|
159
|
+
makeTraces(),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const edgeTypes = new Set(graph.edges.map((e) => e.type));
|
|
163
|
+
|
|
164
|
+
// Code edges
|
|
165
|
+
expect(edgeTypes.has("member_of")).toBe(true);
|
|
166
|
+
expect(edgeTypes.has("references")).toBe(true);
|
|
167
|
+
|
|
168
|
+
// Lifecycle edges that should be present
|
|
169
|
+
expect(edgeTypes.has("modified_by")).toBe(true);
|
|
170
|
+
expect(edgeTypes.has("specified_by")).toBe(true);
|
|
171
|
+
expect(edgeTypes.has("decided_by")).toBe(true);
|
|
172
|
+
expect(edgeTypes.has("constrains")).toBe(true);
|
|
173
|
+
expect(edgeTypes.has("aligns_with")).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should build adjacency map for all nodes", () => {
|
|
177
|
+
const graph = buildKnowledgeGraph(
|
|
178
|
+
makeEntities(),
|
|
179
|
+
makeFeatures(),
|
|
180
|
+
makeDecisions(),
|
|
181
|
+
makeTraces(),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Every node should appear in the adjacency map
|
|
185
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
186
|
+
expect(graph.adjacency.has(nodeId)).toBe(true);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should handle empty inputs", () => {
|
|
191
|
+
const graph = buildKnowledgeGraph([], [], [], []);
|
|
192
|
+
expect(graph.nodes.size).toBe(0);
|
|
193
|
+
expect(graph.edges).toHaveLength(0);
|
|
194
|
+
expect(graph.adjacency.size).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("computePageRank", () => {
|
|
199
|
+
it("should compute scores that sum to approximately 1", () => {
|
|
200
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
201
|
+
const scores = computePageRank(graph);
|
|
202
|
+
|
|
203
|
+
let sum = 0;
|
|
204
|
+
for (const score of scores.values()) {
|
|
205
|
+
sum += score;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
expect(sum).toBeCloseTo(1.0, 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should assign higher rank to more connected nodes", () => {
|
|
212
|
+
const graph = buildKnowledgeGraph(
|
|
213
|
+
makeEntities(),
|
|
214
|
+
makeFeatures(),
|
|
215
|
+
makeDecisions(),
|
|
216
|
+
makeTraces(),
|
|
217
|
+
);
|
|
218
|
+
const scores = computePageRank(graph);
|
|
219
|
+
|
|
220
|
+
// runPipeline is connected to many things (feature, decision, module)
|
|
221
|
+
// so it should have relatively high rank
|
|
222
|
+
const runPipelineScore = scores.get("entity:runPipeline") ?? 0;
|
|
223
|
+
expect(runPipelineScore).toBeGreaterThan(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should converge after iterations", () => {
|
|
227
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
228
|
+
const scores10 = computePageRank(graph, 10);
|
|
229
|
+
const scores50 = computePageRank(graph, 50);
|
|
230
|
+
|
|
231
|
+
// Scores should be similar after convergence
|
|
232
|
+
for (const [id, score10] of scores10) {
|
|
233
|
+
const score50 = scores50.get(id) ?? 0;
|
|
234
|
+
expect(Math.abs(score10 - score50)).toBeLessThan(0.01);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should return empty map for empty graph", () => {
|
|
239
|
+
const graph: KnowledgeGraph = {
|
|
240
|
+
nodes: new Map(),
|
|
241
|
+
edges: [],
|
|
242
|
+
adjacency: new Map(),
|
|
243
|
+
};
|
|
244
|
+
const scores = computePageRank(graph);
|
|
245
|
+
expect(scores.size).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should update graph node pageRank values", () => {
|
|
249
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
250
|
+
computePageRank(graph);
|
|
251
|
+
|
|
252
|
+
for (const node of graph.nodes.values()) {
|
|
253
|
+
expect(node.pageRank).toBeGreaterThan(0);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("mapToArticles", () => {
|
|
259
|
+
it("should map top 20% entities to wiki/entities/", () => {
|
|
260
|
+
const graph = buildKnowledgeGraph(
|
|
261
|
+
makeEntities(),
|
|
262
|
+
makeFeatures(),
|
|
263
|
+
makeDecisions(),
|
|
264
|
+
makeTraces(),
|
|
265
|
+
);
|
|
266
|
+
computePageRank(graph);
|
|
267
|
+
|
|
268
|
+
const communities = new Map<number, string[]>();
|
|
269
|
+
communities.set(0, ["module:verify", "entity:runPipeline"]);
|
|
270
|
+
communities.set(1, ["module:cache", "entity:CacheManager"]);
|
|
271
|
+
|
|
272
|
+
const articleMap = mapToArticles(graph, communities);
|
|
273
|
+
|
|
274
|
+
// At least one entity should map to wiki/entities/
|
|
275
|
+
const entityPaths = [...articleMap.values()].filter((p) =>
|
|
276
|
+
p.startsWith("wiki/entities/"),
|
|
277
|
+
);
|
|
278
|
+
expect(entityPaths.length).toBeGreaterThan(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should map communities to wiki/modules/", () => {
|
|
282
|
+
const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
|
|
283
|
+
computePageRank(graph);
|
|
284
|
+
|
|
285
|
+
const communities = new Map<number, string[]>();
|
|
286
|
+
communities.set(0, ["module:verify"]);
|
|
287
|
+
communities.set(1, ["module:cache"]);
|
|
288
|
+
|
|
289
|
+
const articleMap = mapToArticles(graph, communities);
|
|
290
|
+
|
|
291
|
+
const modulePaths = [...articleMap.values()].filter((p) =>
|
|
292
|
+
p.startsWith("wiki/modules/"),
|
|
293
|
+
);
|
|
294
|
+
expect(modulePaths.length).toBe(2);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should map features to wiki/features/", () => {
|
|
298
|
+
const graph = buildKnowledgeGraph(makeEntities(), makeFeatures(), [], []);
|
|
299
|
+
computePageRank(graph);
|
|
300
|
+
|
|
301
|
+
const articleMap = mapToArticles(graph, new Map());
|
|
302
|
+
const featurePaths = [...articleMap.values()].filter((p) =>
|
|
303
|
+
p.startsWith("wiki/features/"),
|
|
304
|
+
);
|
|
305
|
+
expect(featurePaths.length).toBe(1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should map decisions to wiki/decisions/", () => {
|
|
309
|
+
const graph = buildKnowledgeGraph(
|
|
310
|
+
makeEntities(),
|
|
311
|
+
[],
|
|
312
|
+
makeDecisions(),
|
|
313
|
+
[],
|
|
314
|
+
);
|
|
315
|
+
computePageRank(graph);
|
|
316
|
+
|
|
317
|
+
const articleMap = mapToArticles(graph, new Map());
|
|
318
|
+
const decisionPaths = [...articleMap.values()].filter((p) =>
|
|
319
|
+
p.startsWith("wiki/decisions/"),
|
|
320
|
+
);
|
|
321
|
+
expect(decisionPaths.length).toBe(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should sanitize names in article paths", () => {
|
|
325
|
+
const graph = buildKnowledgeGraph(
|
|
326
|
+
makeEntities(),
|
|
327
|
+
makeFeatures(),
|
|
328
|
+
makeDecisions(),
|
|
329
|
+
makeTraces(),
|
|
330
|
+
);
|
|
331
|
+
computePageRank(graph);
|
|
332
|
+
|
|
333
|
+
const articleMap = mapToArticles(
|
|
334
|
+
graph,
|
|
335
|
+
new Map([[0, ["module:verify"]]]),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
for (const path of articleMap.values()) {
|
|
339
|
+
// Paths should not contain spaces or special chars
|
|
340
|
+
expect(path).toMatch(/^[a-zA-Z0-9/_.-]+$/);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createEmptyState, saveState } from "../state";
|
|
5
|
+
|
|
6
|
+
// ── Import under test ──────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const { onPostCommit } = await import("../hooks");
|
|
9
|
+
|
|
10
|
+
// ── Test Helpers ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
|
|
14
|
+
function createTmpDir(): string {
|
|
15
|
+
const dir = join(
|
|
16
|
+
import.meta.dir,
|
|
17
|
+
`tmp-wiki-hooks-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
18
|
+
);
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set up a minimal wiki directory with state file for testing.
|
|
25
|
+
*/
|
|
26
|
+
function setupWikiDir(root: string): string {
|
|
27
|
+
const mainaDir = join(root, ".maina");
|
|
28
|
+
const wikiDir = join(mainaDir, "wiki");
|
|
29
|
+
mkdirSync(join(wikiDir, "modules"), { recursive: true });
|
|
30
|
+
mkdirSync(join(wikiDir, "entities"), { recursive: true });
|
|
31
|
+
mkdirSync(join(wikiDir, "features"), { recursive: true });
|
|
32
|
+
mkdirSync(join(wikiDir, "decisions"), { recursive: true });
|
|
33
|
+
mkdirSync(join(wikiDir, "architecture"), { recursive: true });
|
|
34
|
+
mkdirSync(join(wikiDir, "raw"), { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Create state file (marks wiki as initialized)
|
|
37
|
+
const state = createEmptyState();
|
|
38
|
+
state.lastFullCompile = new Date().toISOString();
|
|
39
|
+
saveState(wikiDir, state);
|
|
40
|
+
|
|
41
|
+
return mainaDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a minimal source file so the compiler has something to work with.
|
|
46
|
+
*/
|
|
47
|
+
function createSampleSource(root: string): void {
|
|
48
|
+
const srcDir = join(root, "packages", "core", "src");
|
|
49
|
+
mkdirSync(srcDir, { recursive: true });
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(srcDir, "index.ts"),
|
|
52
|
+
"export function hello(): string { return 'hello'; }\n",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
tmpDir = createTmpDir();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
try {
|
|
62
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe("Wiki Post-Commit Hook", () => {
|
|
71
|
+
it("triggers incremental compile when wiki is initialized", async () => {
|
|
72
|
+
createSampleSource(tmpDir);
|
|
73
|
+
const mainaDir = setupWikiDir(tmpDir);
|
|
74
|
+
|
|
75
|
+
// Should complete without throwing
|
|
76
|
+
await onPostCommit(mainaDir, tmpDir);
|
|
77
|
+
|
|
78
|
+
// Verify it ran by checking state was updated
|
|
79
|
+
const { loadState } = await import("../state");
|
|
80
|
+
const wikiDir = join(mainaDir, "wiki");
|
|
81
|
+
const state = loadState(wikiDir);
|
|
82
|
+
expect(state).not.toBeNull();
|
|
83
|
+
// The compilation should have updated lastIncrementalCompile
|
|
84
|
+
expect(state?.lastIncrementalCompile).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("skips when wiki directory does not exist", async () => {
|
|
88
|
+
const mainaDir = join(tmpDir, ".maina");
|
|
89
|
+
mkdirSync(mainaDir, { recursive: true });
|
|
90
|
+
// No wiki directory — should skip silently
|
|
91
|
+
|
|
92
|
+
await onPostCommit(mainaDir, tmpDir);
|
|
93
|
+
|
|
94
|
+
// Should complete without error — nothing to assert except no throw
|
|
95
|
+
expect(true).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("skips when wiki not initialized (no .state.json)", async () => {
|
|
99
|
+
const mainaDir = join(tmpDir, ".maina");
|
|
100
|
+
const wikiDir = join(mainaDir, "wiki");
|
|
101
|
+
mkdirSync(wikiDir, { recursive: true });
|
|
102
|
+
// Wiki directory exists but no .state.json
|
|
103
|
+
|
|
104
|
+
await onPostCommit(mainaDir, tmpDir);
|
|
105
|
+
|
|
106
|
+
// Should complete without error
|
|
107
|
+
expect(true).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("swallows errors gracefully", async () => {
|
|
111
|
+
// Pass a completely invalid directory to trigger internal errors
|
|
112
|
+
const bogusDir = join(tmpDir, "nonexistent", ".maina");
|
|
113
|
+
|
|
114
|
+
// Should NOT throw — errors are swallowed
|
|
115
|
+
await onPostCommit(bogusDir, tmpDir);
|
|
116
|
+
|
|
117
|
+
expect(true).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|