forked from theDakshJaitly/mex
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpublic-api.test.ts
More file actions
236 lines (209 loc) · 8.02 KB
/
public-api.test.ts
File metadata and controls
236 lines (209 loc) · 8.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* Public API smoke test.
*
* This file imports ONLY from src/index.ts — the same surface that
* package.json's `exports` field publishes. Its job is to fail when someone
* accidentally renames, removes, or reshapes a public-facing export.
*
* See COMPATIBILITY.md at the repo root for the contract this test enforces.
* Any change here is a breaking change — bump the major version and update
* the doc.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
// functions
appendEvent,
readEvents,
eventLogPath,
runDriftCheck,
checkHeartbeat,
runHeartbeat,
parseFrontmatter,
findConfig,
createConfig,
// runtime constants
EVENT_KINDS,
DEFAULT_STALENESS_THRESHOLDS,
DEFAULT_SCAFFOLD_PATTERNS,
DEFAULT_HEARTBEAT_PATTERNS,
// types (compile-time only — verified by usage below)
type MexConfig,
type EventEntry,
type EventKind,
type LogOpts,
type DriftReport,
type HeartbeatResult,
type CreateConfigInput,
type RunDriftCheckOpts,
type StalenessThresholds,
type ScaffoldFrontmatter,
} from "../src/index.js";
let tmpDir: string;
let config: MexConfig;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), "mex-public-api-"));
mkdirSync(join(tmpDir, ".mex"), { recursive: true });
config = createConfig({
projectRoot: tmpDir,
scaffoldRoot: join(tmpDir, ".mex"),
});
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
describe("public API — function exports", () => {
it("exports the functions embedders depend on", () => {
expect(typeof appendEvent).toBe("function");
expect(typeof readEvents).toBe("function");
expect(typeof eventLogPath).toBe("function");
expect(typeof runDriftCheck).toBe("function");
expect(typeof checkHeartbeat).toBe("function");
expect(typeof runHeartbeat).toBe("function");
expect(typeof parseFrontmatter).toBe("function");
expect(typeof findConfig).toBe("function");
expect(typeof createConfig).toBe("function");
});
});
describe("public API — runtime constants", () => {
it("exports EVENT_KINDS as an array of valid kinds", () => {
expect(Array.isArray(EVENT_KINDS)).toBe(true);
expect(EVENT_KINDS).toContain("decision");
expect(EVENT_KINDS).toContain("note");
expect(EVENT_KINDS).toContain("risk");
expect(EVENT_KINDS).toContain("todo");
});
it("exports DEFAULT_STALENESS_THRESHOLDS with the documented shape", () => {
const t: StalenessThresholds = DEFAULT_STALENESS_THRESHOLDS;
expect(typeof t.warnDays).toBe("number");
expect(typeof t.errorDays).toBe("number");
expect(typeof t.warnCommits).toBe("number");
expect(typeof t.errorCommits).toBe("number");
});
it("exports DEFAULT_SCAFFOLD_PATTERNS as a non-empty list", () => {
expect(Array.isArray(DEFAULT_SCAFFOLD_PATTERNS)).toBe(true);
expect(DEFAULT_SCAFFOLD_PATTERNS.length).toBeGreaterThan(0);
});
it("exports DEFAULT_HEARTBEAT_PATTERNS as a non-empty list", () => {
expect(Array.isArray(DEFAULT_HEARTBEAT_PATTERNS)).toBe(true);
expect(DEFAULT_HEARTBEAT_PATTERNS.length).toBeGreaterThan(0);
});
});
describe("public API — createConfig", () => {
it("builds a usable MexConfig from minimal input", () => {
const input: CreateConfigInput = {
projectRoot: tmpDir,
scaffoldRoot: join(tmpDir, ".mex"),
};
const c = createConfig(input);
expect(c.projectRoot).toBe(tmpDir);
expect(c.scaffoldRoot).toBe(join(tmpDir, ".mex"));
expect(c.aiTools).toEqual([]);
});
it("rejects relative paths to prevent silent breakage", () => {
expect(() =>
createConfig({ projectRoot: "relative/path", scaffoldRoot: join(tmpDir, ".mex") }),
).toThrow(/projectRoot must be an absolute path/);
expect(() =>
createConfig({ projectRoot: tmpDir, scaffoldRoot: "relative/path" }),
).toThrow(/scaffoldRoot must be an absolute path/);
});
});
describe("public API — appendEvent / readEvents round-trip", () => {
it("persists a decision event and reads it back with the documented shape", () => {
const written: EventEntry = appendEvent(config, "JWT over sessions", {
kind: "decision",
files: ["src/auth.ts"],
});
expect(written.kind).toBe("decision");
expect(written.message).toBe("JWT over sessions");
// Normalize separators so the test is path-agnostic across OSes —
// appendEvent uses `path.relative` internally, which emits backslashes on Windows.
expect(written.files.map((f) => f.replace(/\\/g, "/"))).toEqual(["src/auth.ts"]);
expect(typeof written.timestamp).toBe("string");
const events = readEvents(config);
expect(events).toHaveLength(1);
expect(events[0].message).toBe("JWT over sessions");
expect(events[0].kind).toBe("decision");
// eventLogPath should point at a real file under scaffoldRoot
expect(eventLogPath(config)).toContain(".mex");
});
it("accepts every kind in EVENT_KINDS", () => {
for (const kind of EVENT_KINDS) {
const k: EventKind = kind;
appendEvent(config, `event for ${k}`, { kind: k });
}
expect(readEvents(config)).toHaveLength(EVENT_KINDS.length);
});
it("persists and reads back the optional trace field", () => {
const tracePath = ".mex/traces/2026-05-15-jwt.md";
const opts: LogOpts = {
kind: "decision",
trace: tracePath,
};
const written = appendEvent(config, "Use JWT over sessions", opts);
expect(written.trace).toBe(tracePath);
const events = readEvents(config);
expect(events).toHaveLength(1);
expect(events[0].trace).toBe(tracePath);
});
it("omits the trace field when not provided", () => {
const written = appendEvent(config, "Plain note", { kind: "note" });
expect(written.trace).toBeUndefined();
const events = readEvents(config);
expect(events).toHaveLength(1);
expect(events[0].trace).toBeUndefined();
});
});
describe("public API — parseFrontmatter", () => {
it("reads YAML frontmatter from a markdown file", () => {
const file = join(tmpDir, "page.md");
writeFileSync(
file,
"---\nname: example\ndescription: a doc\nlast_updated: 2026-05-14\n---\n\nbody\n",
);
const fm: ScaffoldFrontmatter | null = parseFrontmatter(file);
expect(fm).not.toBeNull();
expect(fm?.name).toBe("example");
expect(fm?.description).toBe("a doc");
expect(fm?.last_updated).toBe("2026-05-14");
});
it("returns null for files that don't exist", () => {
expect(parseFrontmatter(join(tmpDir, "missing.md"))).toBeNull();
});
});
describe("public API — runDriftCheck", () => {
it("runs on an empty scaffold and returns a DriftReport", async () => {
// Minimum scaffold so runDriftCheck has something to scan
writeFileSync(join(tmpDir, ".mex/ROUTER.md"), "# Router\n");
const report: DriftReport = await runDriftCheck(config);
expect(typeof report.score).toBe("number");
expect(Array.isArray(report.issues)).toBe(true);
expect(typeof report.filesChecked).toBe("number");
expect(typeof report.timestamp).toBe("string");
});
it("accepts scaffoldPatterns override without throwing", async () => {
const opts: RunDriftCheckOpts = {
scaffoldPatterns: [...DEFAULT_SCAFFOLD_PATTERNS, "traces/**/*.md"],
};
const report = await runDriftCheck(config, opts);
expect(report).toBeDefined();
});
});
describe("public API — heartbeat", () => {
it("checkHeartbeat returns the documented HeartbeatResult shape", () => {
const result: HeartbeatResult = checkHeartbeat(config);
expect(typeof result.ok).toBe("boolean");
expect(Array.isArray(result.staleFiles)).toBe(true);
expect(typeof result.memoryCleanupDue).toBe("boolean");
expect(Array.isArray(result.oldDailyMemoryFiles)).toBe(true);
});
it("checkHeartbeat accepts a scaffoldPatterns override", () => {
const result = checkHeartbeat(config, new Date(), {
scaffoldPatterns: [...DEFAULT_HEARTBEAT_PATTERNS, "traces/**/*.md"],
});
expect(result).toBeDefined();
});
});