kitfly 0.1.2
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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/VERSION +1 -0
- package/package.json +63 -0
- package/schemas/README.md +32 -0
- package/schemas/site.schema.json +5 -0
- package/schemas/theme.schema.json +5 -0
- package/schemas/v0/site.schema.json +172 -0
- package/schemas/v0/theme.schema.json +210 -0
- package/scripts/build-all.ts +121 -0
- package/scripts/build.ts +601 -0
- package/scripts/bundle.ts +781 -0
- package/scripts/dev.ts +777 -0
- package/scripts/generate-checksums.sh +78 -0
- package/scripts/release/export-release-key.sh +28 -0
- package/scripts/release/release-guard-tag-version.sh +79 -0
- package/scripts/release/sign-release-assets.sh +123 -0
- package/scripts/release/upload-release-assets.sh +76 -0
- package/scripts/release/upload-release-provenance.sh +52 -0
- package/scripts/release/verify-public-key.sh +48 -0
- package/scripts/release/verify-signatures.sh +117 -0
- package/scripts/version-sync.ts +82 -0
- package/src/__tests__/build.test.ts +240 -0
- package/src/__tests__/bundle.test.ts +786 -0
- package/src/__tests__/cli.test.ts +706 -0
- package/src/__tests__/crucible.test.ts +1043 -0
- package/src/__tests__/engine.test.ts +157 -0
- package/src/__tests__/init.test.ts +450 -0
- package/src/__tests__/pipeline.test.ts +1087 -0
- package/src/__tests__/productbook.test.ts +1206 -0
- package/src/__tests__/runbook.test.ts +974 -0
- package/src/__tests__/server-registry.test.ts +1251 -0
- package/src/__tests__/servicebook.test.ts +1248 -0
- package/src/__tests__/shared.test.ts +2005 -0
- package/src/__tests__/styles.test.ts +14 -0
- package/src/__tests__/theme-schema.test.ts +47 -0
- package/src/__tests__/theme.test.ts +554 -0
- package/src/cli.ts +582 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/update.ts +444 -0
- package/src/engine.ts +20 -0
- package/src/logger.ts +15 -0
- package/src/migrations/0000_schema_versioning.ts +67 -0
- package/src/migrations/0001_server_port.ts +52 -0
- package/src/migrations/0002_brand_logo.ts +49 -0
- package/src/migrations/index.ts +26 -0
- package/src/migrations/schema.ts +24 -0
- package/src/server-registry.ts +405 -0
- package/src/shared.ts +1239 -0
- package/src/site/styles.css +931 -0
- package/src/site/template.html +193 -0
- package/src/templates/crucible.ts +1163 -0
- package/src/templates/driver.ts +876 -0
- package/src/templates/handbook.ts +339 -0
- package/src/templates/minimal.ts +139 -0
- package/src/templates/pipeline.ts +966 -0
- package/src/templates/productbook.ts +1032 -0
- package/src/templates/runbook.ts +829 -0
- package/src/templates/schema.ts +119 -0
- package/src/templates/servicebook.ts +1242 -0
- package/src/theme.ts +245 -0
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Runbook template definition
|
|
3
|
+
*
|
|
4
|
+
* Covers: src/templates/runbook.ts
|
|
5
|
+
* Strategy: import the template directly, verify structure and generated content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { runbook } from "../templates/runbook.ts";
|
|
10
|
+
import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Build a standard TemplateContext for testing */
|
|
17
|
+
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
18
|
+
const branding: BrandingConfig = {
|
|
19
|
+
siteName: "Test Runbook",
|
|
20
|
+
brandName: "Acme Corp",
|
|
21
|
+
brandUrl: "https://acme.example.com",
|
|
22
|
+
primaryColor: "#2563eb",
|
|
23
|
+
footerText: "Footer text",
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
name: "test-runbook",
|
|
27
|
+
branding,
|
|
28
|
+
template: runbook,
|
|
29
|
+
year: 2026,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Resolve the content of a TemplateFile to a string */
|
|
35
|
+
function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
|
|
36
|
+
return typeof file.content === "function" ? file.content(ctx) : file.content;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Find a file entry by its path */
|
|
40
|
+
function findFile(path: string): TemplateFile {
|
|
41
|
+
const f = runbook.files.find((entry) => entry.path === path);
|
|
42
|
+
if (!f) throw new Error(`Template file not found: ${path}`);
|
|
43
|
+
return f;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Template Definition Structure
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe("runbook template definition", () => {
|
|
51
|
+
it("has correct identity metadata", () => {
|
|
52
|
+
expect(runbook.id).toBe("runbook");
|
|
53
|
+
expect(runbook.name).toBe("Runbook");
|
|
54
|
+
expect(runbook.version).toBe(1);
|
|
55
|
+
expect(runbook.extends).toBe("minimal");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("has a non-empty description", () => {
|
|
59
|
+
expect(runbook.description).toBeTruthy();
|
|
60
|
+
expect(runbook.description.length).toBeGreaterThan(10);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defines exactly four sections", () => {
|
|
64
|
+
expect(runbook.sections).toHaveLength(4);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("defines the expected section names", () => {
|
|
68
|
+
const names = runbook.sections.map((s) => s.name);
|
|
69
|
+
expect(names).toEqual(["Procedures", "Troubleshooting", "Reference", "Incidents"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("defines the expected section paths", () => {
|
|
73
|
+
const paths = runbook.sections.map((s) => s.path);
|
|
74
|
+
expect(paths).toEqual([
|
|
75
|
+
"content/procedures",
|
|
76
|
+
"content/troubleshooting",
|
|
77
|
+
"content/reference",
|
|
78
|
+
"content/incidents",
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("every section has a description", () => {
|
|
83
|
+
for (const section of runbook.sections) {
|
|
84
|
+
expect(section.description).toBeTruthy();
|
|
85
|
+
expect(typeof section.description).toBe("string");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("defines a non-empty files array", () => {
|
|
90
|
+
expect(runbook.files.length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("every file has a non-empty path", () => {
|
|
94
|
+
for (const file of runbook.files) {
|
|
95
|
+
expect(file.path).toBeTruthy();
|
|
96
|
+
expect(typeof file.path).toBe("string");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("every file has content (string or function)", () => {
|
|
101
|
+
for (const file of runbook.files) {
|
|
102
|
+
expect(["string", "function"]).toContain(typeof file.content);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("has no duplicate file paths", () => {
|
|
107
|
+
const paths = runbook.files.map((f) => f.path);
|
|
108
|
+
const unique = new Set(paths);
|
|
109
|
+
expect(unique.size).toBe(paths.length);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// site.yaml Generation
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
describe("runbook site.yaml", () => {
|
|
118
|
+
const ctx = makeCtx();
|
|
119
|
+
const file = findFile("site.yaml");
|
|
120
|
+
|
|
121
|
+
it("exists in the file list", () => {
|
|
122
|
+
expect(file).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("includes the site title from context", () => {
|
|
126
|
+
const content = resolveContent(file, ctx);
|
|
127
|
+
expect(content).toContain('title: "Test Runbook"');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("includes the brand name from context", () => {
|
|
131
|
+
const content = resolveContent(file, ctx);
|
|
132
|
+
expect(content).toContain('name: "Acme Corp"');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("includes the brand url from context", () => {
|
|
136
|
+
const content = resolveContent(file, ctx);
|
|
137
|
+
expect(content).toContain('url: "https://acme.example.com"');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("includes sections block with all four sections", () => {
|
|
141
|
+
const content = resolveContent(file, ctx);
|
|
142
|
+
expect(content).toContain("sections:");
|
|
143
|
+
expect(content).toContain('"Procedures"');
|
|
144
|
+
expect(content).toContain('"Troubleshooting"');
|
|
145
|
+
expect(content).toContain('"Reference"');
|
|
146
|
+
expect(content).toContain('"Incidents"');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("includes section paths", () => {
|
|
150
|
+
const content = resolveContent(file, ctx);
|
|
151
|
+
expect(content).toContain('"content/procedures"');
|
|
152
|
+
expect(content).toContain('"content/troubleshooting"');
|
|
153
|
+
expect(content).toContain('"content/reference"');
|
|
154
|
+
expect(content).toContain('"content/incidents"');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("sets home page to index.md", () => {
|
|
158
|
+
const content = resolveContent(file, ctx);
|
|
159
|
+
expect(content).toContain('home: "index.md"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("includes documentation link", () => {
|
|
163
|
+
const content = resolveContent(file, ctx);
|
|
164
|
+
expect(content).toContain("https://github.com/3leaps/kitfly");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("responds to different branding values", () => {
|
|
168
|
+
const customCtx = makeCtx({
|
|
169
|
+
branding: {
|
|
170
|
+
siteName: "Widget Ops",
|
|
171
|
+
brandName: "Widget Inc",
|
|
172
|
+
brandUrl: "/",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const content = resolveContent(file, customCtx);
|
|
176
|
+
expect(content).toContain('title: "Widget Ops"');
|
|
177
|
+
expect(content).toContain('name: "Widget Inc"');
|
|
178
|
+
expect(content).toContain('url: "/"');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// index.md Generation
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe("runbook index.md", () => {
|
|
187
|
+
const ctx = makeCtx();
|
|
188
|
+
const file = findFile("index.md");
|
|
189
|
+
|
|
190
|
+
it("exists in the file list", () => {
|
|
191
|
+
expect(file).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("starts with YAML frontmatter", () => {
|
|
195
|
+
const content = resolveContent(file, ctx);
|
|
196
|
+
expect(content).toMatch(/^---\n/);
|
|
197
|
+
expect(content).toContain("title: Home");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("includes site name in frontmatter description", () => {
|
|
201
|
+
const content = resolveContent(file, ctx);
|
|
202
|
+
expect(content).toContain("description: Test Runbook - Operational Runbook");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("includes site name in the H1 heading", () => {
|
|
206
|
+
const content = resolveContent(file, ctx);
|
|
207
|
+
expect(content).toContain("# Test Runbook");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("references brand name in the body", () => {
|
|
211
|
+
const content = resolveContent(file, ctx);
|
|
212
|
+
expect(content).toContain("Operational runbook for Acme Corp");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("contains quick links to key sections", () => {
|
|
216
|
+
const content = resolveContent(file, ctx);
|
|
217
|
+
expect(content).toContain("## Quick Links");
|
|
218
|
+
expect(content).toContain("[Procedures](/content/procedures/deployment)");
|
|
219
|
+
expect(content).toContain("[Troubleshooting](/content/troubleshooting/common-issues)");
|
|
220
|
+
expect(content).toContain("[Reference](/content/reference/interfaces/api-template)");
|
|
221
|
+
expect(content).toContain("[Incidents](/content/incidents/escalation)");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("contains an on-call quick reference table", () => {
|
|
225
|
+
const content = resolveContent(file, ctx);
|
|
226
|
+
expect(content).toContain("## On-Call Quick Reference");
|
|
227
|
+
expect(content).toContain("| Severity | Response Time | Escalation |");
|
|
228
|
+
expect(content).toContain("P1 - Critical");
|
|
229
|
+
expect(content).toContain("P2 - High");
|
|
230
|
+
expect(content).toContain("P3 - Medium");
|
|
231
|
+
expect(content).toContain("P4 - Low");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("links to escalation procedures", () => {
|
|
235
|
+
const content = resolveContent(file, ctx);
|
|
236
|
+
expect(content).toContain("[Escalation](/content/incidents/escalation)");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("includes a last-updated date in ISO format", () => {
|
|
240
|
+
const content = resolveContent(file, ctx);
|
|
241
|
+
expect(content).toMatch(/\*Last updated: \d{4}-\d{2}-\d{2}\*/);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Procedures Section Files
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
describe("runbook procedures section", () => {
|
|
250
|
+
const ctx = makeCtx();
|
|
251
|
+
|
|
252
|
+
describe("deployment.md", () => {
|
|
253
|
+
const file = findFile("content/procedures/deployment.md");
|
|
254
|
+
|
|
255
|
+
it("exists in the file list", () => {
|
|
256
|
+
expect(file).toBeDefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("has YAML frontmatter with title and description", () => {
|
|
260
|
+
const content = resolveContent(file, ctx);
|
|
261
|
+
expect(content).toMatch(/^---\n/);
|
|
262
|
+
expect(content).toContain("title: Deployment Procedure");
|
|
263
|
+
expect(content).toContain("description: Standard deployment process for Acme Corp");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("includes objective section", () => {
|
|
267
|
+
const content = resolveContent(file, ctx);
|
|
268
|
+
expect(content).toContain("## Objective");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("includes prerequisites as checkboxes", () => {
|
|
272
|
+
const content = resolveContent(file, ctx);
|
|
273
|
+
expect(content).toContain("## Prerequisites");
|
|
274
|
+
expect(content).toContain("- [ ] Code reviewed and approved");
|
|
275
|
+
expect(content).toContain("- [ ] Tests passing in CI");
|
|
276
|
+
expect(content).toContain("- [ ] Change ticket approved");
|
|
277
|
+
expect(content).toContain("- [ ] Rollback plan documented");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("includes numbered deployment steps", () => {
|
|
281
|
+
const content = resolveContent(file, ctx);
|
|
282
|
+
expect(content).toContain("### 1. Pre-deployment Checks");
|
|
283
|
+
expect(content).toContain("### 2. Create Deployment");
|
|
284
|
+
expect(content).toContain("### 3. Monitor Rollout");
|
|
285
|
+
expect(content).toContain("### 4. Post-deployment Validation");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("includes rollback section", () => {
|
|
289
|
+
const content = resolveContent(file, ctx);
|
|
290
|
+
expect(content).toContain("## Rollback");
|
|
291
|
+
expect(content).toContain("rollback.sh");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("includes bash code blocks", () => {
|
|
295
|
+
const content = resolveContent(file, ctx);
|
|
296
|
+
expect(content).toContain("```bash");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("links to related pages", () => {
|
|
300
|
+
const content = resolveContent(file, ctx);
|
|
301
|
+
expect(content).toContain("[Pre-deploy Checklist](/content/reference/checklists/pre-deploy)");
|
|
302
|
+
expect(content).toContain("[Incident Escalation](/content/incidents/escalation)");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Troubleshooting Section Files
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
describe("runbook troubleshooting section", () => {
|
|
312
|
+
const ctx = makeCtx();
|
|
313
|
+
|
|
314
|
+
describe("common-issues.md", () => {
|
|
315
|
+
const file = findFile("content/troubleshooting/common-issues.md");
|
|
316
|
+
|
|
317
|
+
it("exists in the file list", () => {
|
|
318
|
+
expect(file).toBeDefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("has YAML frontmatter", () => {
|
|
322
|
+
const content = resolveContent(file, ctx);
|
|
323
|
+
expect(content).toMatch(/^---\n/);
|
|
324
|
+
expect(content).toContain("title: Common Issues");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("documents Connection Timeout issue", () => {
|
|
328
|
+
const content = resolveContent(file, ctx);
|
|
329
|
+
expect(content).toContain("## Connection Timeout");
|
|
330
|
+
expect(content).toContain("**Symptoms**:");
|
|
331
|
+
expect(content).toContain("**Possible Causes**:");
|
|
332
|
+
expect(content).toContain("**Resolution**:");
|
|
333
|
+
expect(content).toContain("**Escalation**:");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("documents High Memory Usage issue", () => {
|
|
337
|
+
const content = resolveContent(file, ctx);
|
|
338
|
+
expect(content).toContain("## High Memory Usage");
|
|
339
|
+
expect(content).toContain("OOM kills");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("documents Authentication Failures issue", () => {
|
|
343
|
+
const content = resolveContent(file, ctx);
|
|
344
|
+
expect(content).toContain("## Authentication Failures");
|
|
345
|
+
expect(content).toContain("401 errors");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("includes escalation link", () => {
|
|
349
|
+
const content = resolveContent(file, ctx);
|
|
350
|
+
expect(content).toContain("[on-call](/content/incidents/escalation)");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("is a static template (does not use context)", () => {
|
|
354
|
+
const ctx2 = makeCtx({
|
|
355
|
+
branding: { siteName: "Other", brandName: "Other" },
|
|
356
|
+
});
|
|
357
|
+
const content1 = resolveContent(file, ctx);
|
|
358
|
+
const content2 = resolveContent(file, ctx2);
|
|
359
|
+
expect(content1).toBe(content2);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Reference Section Files
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
describe("runbook reference section", () => {
|
|
369
|
+
const ctx = makeCtx();
|
|
370
|
+
|
|
371
|
+
describe("interfaces/api-template.md", () => {
|
|
372
|
+
const file = findFile("content/reference/interfaces/api-template.md");
|
|
373
|
+
|
|
374
|
+
it("exists in the file list", () => {
|
|
375
|
+
expect(file).toBeDefined();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("has YAML frontmatter", () => {
|
|
379
|
+
const content = resolveContent(file, ctx);
|
|
380
|
+
expect(content).toMatch(/^---\n/);
|
|
381
|
+
expect(content).toContain("title: API Integration Template");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("includes overview table with fields", () => {
|
|
385
|
+
const content = resolveContent(file, ctx);
|
|
386
|
+
expect(content).toContain("## Overview");
|
|
387
|
+
expect(content).toContain("| Field | Value |");
|
|
388
|
+
expect(content).toContain("**Service**");
|
|
389
|
+
expect(content).toContain("**Type**");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("includes authentication section", () => {
|
|
393
|
+
const content = resolveContent(file, ctx);
|
|
394
|
+
expect(content).toContain("## Authentication");
|
|
395
|
+
expect(content).toContain("API Key / OAuth 2.0 / Basic Auth");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("includes endpoints section with code examples", () => {
|
|
399
|
+
const content = resolveContent(file, ctx);
|
|
400
|
+
expect(content).toContain("## Endpoints");
|
|
401
|
+
expect(content).toContain("### Primary Endpoint");
|
|
402
|
+
expect(content).toContain("POST https://api.vendor.com/v1/resource");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("includes error handling table", () => {
|
|
406
|
+
const content = resolveContent(file, ctx);
|
|
407
|
+
expect(content).toContain("## Error Handling");
|
|
408
|
+
expect(content).toContain("| Code | Meaning | Action |");
|
|
409
|
+
expect(content).toContain("400");
|
|
410
|
+
expect(content).toContain("401");
|
|
411
|
+
expect(content).toContain("429");
|
|
412
|
+
expect(content).toContain("500");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("includes rate limits section", () => {
|
|
416
|
+
const content = resolveContent(file, ctx);
|
|
417
|
+
expect(content).toContain("## Rate Limits");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("links to contacts directory", () => {
|
|
421
|
+
const content = resolveContent(file, ctx);
|
|
422
|
+
expect(content).toContain("[Contacts](/content/reference/contacts/directory)");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("is a static template (does not use context)", () => {
|
|
426
|
+
const ctx2 = makeCtx({
|
|
427
|
+
branding: { siteName: "Other", brandName: "Other" },
|
|
428
|
+
});
|
|
429
|
+
expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("contacts/directory.md", () => {
|
|
434
|
+
const file = findFile("content/reference/contacts/directory.md");
|
|
435
|
+
|
|
436
|
+
it("exists in the file list", () => {
|
|
437
|
+
expect(file).toBeDefined();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("has frontmatter referencing brand name", () => {
|
|
441
|
+
const content = resolveContent(file, ctx);
|
|
442
|
+
expect(content).toContain("title: Contact Directory");
|
|
443
|
+
expect(content).toContain("description: Team and vendor contacts for Acme Corp");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("includes internal team contacts", () => {
|
|
447
|
+
const content = resolveContent(file, ctx);
|
|
448
|
+
expect(content).toContain("## Internal Team");
|
|
449
|
+
expect(content).toContain("### On-Call");
|
|
450
|
+
expect(content).toContain("### Team Leads");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("includes on-call contact table", () => {
|
|
454
|
+
const content = resolveContent(file, ctx);
|
|
455
|
+
expect(content).toContain("| Role | Contact | Escalation |");
|
|
456
|
+
expect(content).toContain("Primary On-Call");
|
|
457
|
+
expect(content).toContain("Secondary On-Call");
|
|
458
|
+
expect(content).toContain("Engineering Lead");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("includes vendor contacts", () => {
|
|
462
|
+
const content = resolveContent(file, ctx);
|
|
463
|
+
expect(content).toContain("## Vendor Contacts");
|
|
464
|
+
expect(content).toContain("### Cloud Provider");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("links to escalation procedures", () => {
|
|
468
|
+
const content = resolveContent(file, ctx);
|
|
469
|
+
expect(content).toContain("[Escalation Procedures](/content/incidents/escalation)");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("checklists/pre-deploy.md", () => {
|
|
474
|
+
const file = findFile("content/reference/checklists/pre-deploy.md");
|
|
475
|
+
|
|
476
|
+
it("exists in the file list", () => {
|
|
477
|
+
expect(file).toBeDefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("has YAML frontmatter", () => {
|
|
481
|
+
const content = resolveContent(file, ctx);
|
|
482
|
+
expect(content).toMatch(/^---\n/);
|
|
483
|
+
expect(content).toContain("title: Pre-Deployment Checklist");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("includes code readiness checklist", () => {
|
|
487
|
+
const content = resolveContent(file, ctx);
|
|
488
|
+
expect(content).toContain("## Code Readiness");
|
|
489
|
+
expect(content).toContain("- [ ] All tests passing in CI");
|
|
490
|
+
expect(content).toContain("- [ ] Code review approved");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("includes change management checklist", () => {
|
|
494
|
+
const content = resolveContent(file, ctx);
|
|
495
|
+
expect(content).toContain("## Change Management");
|
|
496
|
+
expect(content).toContain("- [ ] Change ticket created and approved");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("includes environment verification checklist", () => {
|
|
500
|
+
const content = resolveContent(file, ctx);
|
|
501
|
+
expect(content).toContain("## Environment Verification");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("includes monitoring readiness checklist", () => {
|
|
505
|
+
const content = resolveContent(file, ctx);
|
|
506
|
+
expect(content).toContain("## Monitoring Readiness");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("includes go/no-go decision table", () => {
|
|
510
|
+
const content = resolveContent(file, ctx);
|
|
511
|
+
expect(content).toContain("## Go/No-Go");
|
|
512
|
+
expect(content).toContain("| Criteria | Status |");
|
|
513
|
+
expect(content).toContain("GO / ");
|
|
514
|
+
expect(content).toContain("NO-GO");
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("links to deployment procedure", () => {
|
|
518
|
+
const content = resolveContent(file, ctx);
|
|
519
|
+
expect(content).toContain("[Deployment Procedure](/content/procedures/deployment)");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("is a static template (does not use context)", () => {
|
|
523
|
+
const ctx2 = makeCtx({
|
|
524
|
+
branding: { siteName: "Other", brandName: "Other" },
|
|
525
|
+
});
|
|
526
|
+
expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("analytics/dashboards.md", () => {
|
|
531
|
+
const file = findFile("content/reference/analytics/dashboards.md");
|
|
532
|
+
|
|
533
|
+
it("exists in the file list", () => {
|
|
534
|
+
expect(file).toBeDefined();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("has frontmatter referencing brand name", () => {
|
|
538
|
+
const content = resolveContent(file, ctx);
|
|
539
|
+
expect(content).toContain("title: Dashboards & Metrics");
|
|
540
|
+
expect(content).toContain("description: Key metrics and dashboard links for Acme Corp");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("includes primary dashboards table", () => {
|
|
544
|
+
const content = resolveContent(file, ctx);
|
|
545
|
+
expect(content).toContain("## Primary Dashboards");
|
|
546
|
+
expect(content).toContain("| Dashboard | URL | Purpose |");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("includes key performance indicators", () => {
|
|
550
|
+
const content = resolveContent(file, ctx);
|
|
551
|
+
expect(content).toContain("## Key Performance Indicators");
|
|
552
|
+
expect(content).toContain("### Availability");
|
|
553
|
+
expect(content).toContain("### Business Metrics");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("includes SLA definitions", () => {
|
|
557
|
+
const content = resolveContent(file, ctx);
|
|
558
|
+
expect(content).toContain("## SLA Definitions");
|
|
559
|
+
expect(content).toContain("| Tier | Availability | Response Time |");
|
|
560
|
+
expect(content).toContain("Critical");
|
|
561
|
+
expect(content).toContain("Standard");
|
|
562
|
+
expect(content).toContain("Best Effort");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("includes alert thresholds table", () => {
|
|
566
|
+
const content = resolveContent(file, ctx);
|
|
567
|
+
expect(content).toContain("## Alert Thresholds");
|
|
568
|
+
expect(content).toContain("| Alert | Warning | Critical | Action |");
|
|
569
|
+
expect(content).toContain("CPU");
|
|
570
|
+
expect(content).toContain("Memory");
|
|
571
|
+
expect(content).toContain("Error Rate");
|
|
572
|
+
expect(content).toContain("Latency P95");
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// Incidents Section Files
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
describe("runbook incidents section", () => {
|
|
582
|
+
const ctx = makeCtx();
|
|
583
|
+
|
|
584
|
+
describe("escalation.md", () => {
|
|
585
|
+
const file = findFile("content/incidents/escalation.md");
|
|
586
|
+
|
|
587
|
+
it("exists in the file list", () => {
|
|
588
|
+
expect(file).toBeDefined();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("has YAML frontmatter", () => {
|
|
592
|
+
const content = resolveContent(file, ctx);
|
|
593
|
+
expect(content).toMatch(/^---\n/);
|
|
594
|
+
expect(content).toContain("title: Escalation Procedures");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("defines severity levels", () => {
|
|
598
|
+
const content = resolveContent(file, ctx);
|
|
599
|
+
expect(content).toContain("## Severity Levels");
|
|
600
|
+
expect(content).toContain("| Level | Definition | Response Time | Examples |");
|
|
601
|
+
expect(content).toContain("**P1**");
|
|
602
|
+
expect(content).toContain("**P2**");
|
|
603
|
+
expect(content).toContain("**P3**");
|
|
604
|
+
expect(content).toContain("**P4**");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("includes escalation matrix with steps for P1", () => {
|
|
608
|
+
const content = resolveContent(file, ctx);
|
|
609
|
+
expect(content).toContain("## Escalation Matrix");
|
|
610
|
+
expect(content).toContain("### P1 - Critical");
|
|
611
|
+
expect(content).toContain("Page on-call engineer");
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("includes escalation matrix with steps for P2", () => {
|
|
615
|
+
const content = resolveContent(file, ctx);
|
|
616
|
+
expect(content).toContain("### P2 - High");
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("includes escalation matrix for P3/P4", () => {
|
|
620
|
+
const content = resolveContent(file, ctx);
|
|
621
|
+
expect(content).toContain("### P3/P4 - Medium/Low");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("includes communication section", () => {
|
|
625
|
+
const content = resolveContent(file, ctx);
|
|
626
|
+
expect(content).toContain("## Communication");
|
|
627
|
+
expect(content).toContain("### Internal Updates");
|
|
628
|
+
expect(content).toContain("### External Communication");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("includes post-incident process", () => {
|
|
632
|
+
const content = resolveContent(file, ctx);
|
|
633
|
+
expect(content).toContain("## Post-Incident");
|
|
634
|
+
expect(content).toContain("Post-mortem meeting");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("links to post-mortem template", () => {
|
|
638
|
+
const content = resolveContent(file, ctx);
|
|
639
|
+
expect(content).toContain("[Post-Mortem Template](/content/incidents/post-mortem-template)");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("is a static template (does not use context)", () => {
|
|
643
|
+
const ctx2 = makeCtx({
|
|
644
|
+
branding: { siteName: "Other", brandName: "Other" },
|
|
645
|
+
});
|
|
646
|
+
expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe("post-mortem-template.md", () => {
|
|
651
|
+
const file = findFile("content/incidents/post-mortem-template.md");
|
|
652
|
+
|
|
653
|
+
it("exists in the file list", () => {
|
|
654
|
+
expect(file).toBeDefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("has YAML frontmatter with title", () => {
|
|
658
|
+
const content = resolveContent(file, ctx);
|
|
659
|
+
expect(content).toMatch(/^---\n/);
|
|
660
|
+
expect(content).toContain("title: Post-Mortem Template");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("includes summary table", () => {
|
|
664
|
+
const content = resolveContent(file, ctx);
|
|
665
|
+
expect(content).toContain("## Summary");
|
|
666
|
+
expect(content).toContain("| Field | Value |");
|
|
667
|
+
expect(content).toContain("**Date**");
|
|
668
|
+
expect(content).toContain("**Duration**");
|
|
669
|
+
expect(content).toContain("**Severity**");
|
|
670
|
+
expect(content).toContain("**Impact**");
|
|
671
|
+
expect(content).toContain("**Status**");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("includes timeline table", () => {
|
|
675
|
+
const content = resolveContent(file, ctx);
|
|
676
|
+
expect(content).toContain("## Timeline");
|
|
677
|
+
expect(content).toContain("| Time (UTC) | Event |");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("includes root cause and contributing factors", () => {
|
|
681
|
+
const content = resolveContent(file, ctx);
|
|
682
|
+
expect(content).toContain("## Root Cause");
|
|
683
|
+
expect(content).toContain("## Contributing Factors");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("includes resolution section", () => {
|
|
687
|
+
const content = resolveContent(file, ctx);
|
|
688
|
+
expect(content).toContain("## Resolution");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("includes lessons learned with subsections", () => {
|
|
692
|
+
const content = resolveContent(file, ctx);
|
|
693
|
+
expect(content).toContain("## Lessons Learned");
|
|
694
|
+
expect(content).toContain("### What Went Well");
|
|
695
|
+
expect(content).toContain("### What Could Be Improved");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("includes action items table", () => {
|
|
699
|
+
const content = resolveContent(file, ctx);
|
|
700
|
+
expect(content).toContain("## Action Items");
|
|
701
|
+
expect(content).toContain("| Item | Owner | Due Date | Status |");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("includes references section", () => {
|
|
705
|
+
const content = resolveContent(file, ctx);
|
|
706
|
+
expect(content).toContain("## References");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("is a static template (does not use context)", () => {
|
|
710
|
+
const ctx2 = makeCtx({
|
|
711
|
+
branding: { siteName: "Other", brandName: "Other" },
|
|
712
|
+
});
|
|
713
|
+
expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// CUSTOMIZING.md
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
describe("runbook CUSTOMIZING.md", () => {
|
|
723
|
+
const ctx = makeCtx();
|
|
724
|
+
const file = findFile("CUSTOMIZING.md");
|
|
725
|
+
|
|
726
|
+
it("exists in the file list", () => {
|
|
727
|
+
expect(file).toBeDefined();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("has frontmatter with template metadata", () => {
|
|
731
|
+
const content = resolveContent(file, ctx);
|
|
732
|
+
expect(content).toMatch(/^---\n/);
|
|
733
|
+
expect(content).toContain("template: runbook");
|
|
734
|
+
expect(content).toContain("template_version: 1");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("includes the site name in the heading", () => {
|
|
738
|
+
const content = resolveContent(file, ctx);
|
|
739
|
+
expect(content).toContain("# Customizing Test Runbook");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("uses the project name in the directory tree", () => {
|
|
743
|
+
const content = resolveContent(file, ctx);
|
|
744
|
+
expect(content).toContain("test-runbook/");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("documents site.yaml configuration", () => {
|
|
748
|
+
const content = resolveContent(file, ctx);
|
|
749
|
+
expect(content).toContain("### site.yaml");
|
|
750
|
+
expect(content).toContain('title: "Test Runbook"');
|
|
751
|
+
expect(content).toContain('name: "Acme Corp"');
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("documents theme.yaml customization", () => {
|
|
755
|
+
const content = resolveContent(file, ctx);
|
|
756
|
+
expect(content).toContain("### theme.yaml");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("includes the current year in footer example", () => {
|
|
760
|
+
const content = resolveContent(file, ctx);
|
|
761
|
+
expect(content).toContain("2026 Acme Corp");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("documents the site structure tree", () => {
|
|
765
|
+
const content = resolveContent(file, ctx);
|
|
766
|
+
expect(content).toContain("## Site Structure");
|
|
767
|
+
expect(content).toContain("content/");
|
|
768
|
+
expect(content).toContain("procedures/");
|
|
769
|
+
expect(content).toContain("troubleshooting/");
|
|
770
|
+
expect(content).toContain("reference/");
|
|
771
|
+
expect(content).toContain("incidents/");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("documents adding content types", () => {
|
|
775
|
+
const content = resolveContent(file, ctx);
|
|
776
|
+
expect(content).toContain("### New Procedure");
|
|
777
|
+
expect(content).toContain("### New Troubleshooting Guide");
|
|
778
|
+
expect(content).toContain("### New Interface/API Doc");
|
|
779
|
+
expect(content).toContain("### New Section");
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("documents procedure format", () => {
|
|
783
|
+
const content = resolveContent(file, ctx);
|
|
784
|
+
expect(content).toContain("## Objective");
|
|
785
|
+
expect(content).toContain("## Prerequisites");
|
|
786
|
+
expect(content).toContain("## Steps");
|
|
787
|
+
expect(content).toContain("## Verification");
|
|
788
|
+
expect(content).toContain("## Rollback");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("documents linking conventions", () => {
|
|
792
|
+
const content = resolveContent(file, ctx);
|
|
793
|
+
expect(content).toContain("### Internal Links");
|
|
794
|
+
expect(content).toContain("### External Links");
|
|
795
|
+
expect(content).toContain("### Images");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("documents important limitations", () => {
|
|
799
|
+
const content = resolveContent(file, ctx);
|
|
800
|
+
expect(content).toContain("## Important Limitations");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("documents conventions for procedures and troubleshooting", () => {
|
|
804
|
+
const content = resolveContent(file, ctx);
|
|
805
|
+
expect(content).toContain("### Procedures");
|
|
806
|
+
expect(content).toContain("### Troubleshooting");
|
|
807
|
+
expect(content).toContain("### Checklists");
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("includes brand assets table", () => {
|
|
811
|
+
const content = resolveContent(file, ctx);
|
|
812
|
+
expect(content).toContain("## Brand Assets");
|
|
813
|
+
expect(content).toContain("| Asset | Location | Recommended Size |");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("includes getting help section", () => {
|
|
817
|
+
const content = resolveContent(file, ctx);
|
|
818
|
+
expect(content).toContain("## Getting Help");
|
|
819
|
+
expect(content).toContain("https://github.com/3leaps/kitfly");
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("responds to different context values", () => {
|
|
823
|
+
const customCtx = makeCtx({
|
|
824
|
+
name: "my-project",
|
|
825
|
+
branding: {
|
|
826
|
+
siteName: "My Ops Hub",
|
|
827
|
+
brandName: "My Brand",
|
|
828
|
+
brandUrl: "/",
|
|
829
|
+
},
|
|
830
|
+
year: 2027,
|
|
831
|
+
});
|
|
832
|
+
const content = resolveContent(file, customCtx);
|
|
833
|
+
expect(content).toContain("# Customizing My Ops Hub");
|
|
834
|
+
expect(content).toContain("my-project/");
|
|
835
|
+
expect(content).toContain('name: "My Brand"');
|
|
836
|
+
expect(content).toContain("2027 My Brand");
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// ---------------------------------------------------------------------------
|
|
841
|
+
// File Coverage Completeness
|
|
842
|
+
// ---------------------------------------------------------------------------
|
|
843
|
+
|
|
844
|
+
describe("runbook file coverage", () => {
|
|
845
|
+
const expectedFiles = [
|
|
846
|
+
"site.yaml",
|
|
847
|
+
"index.md",
|
|
848
|
+
"content/procedures/deployment.md",
|
|
849
|
+
"content/troubleshooting/common-issues.md",
|
|
850
|
+
"content/reference/interfaces/api-template.md",
|
|
851
|
+
"content/reference/contacts/directory.md",
|
|
852
|
+
"content/reference/checklists/pre-deploy.md",
|
|
853
|
+
"content/reference/analytics/dashboards.md",
|
|
854
|
+
"content/incidents/escalation.md",
|
|
855
|
+
"content/incidents/post-mortem-template.md",
|
|
856
|
+
"CUSTOMIZING.md",
|
|
857
|
+
];
|
|
858
|
+
|
|
859
|
+
it("defines exactly the expected set of files", () => {
|
|
860
|
+
const actualPaths = runbook.files.map((f) => f.path).sort();
|
|
861
|
+
const expected = [...expectedFiles].sort();
|
|
862
|
+
expect(actualPaths).toEqual(expected);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("all content generators produce non-empty strings", () => {
|
|
866
|
+
const ctx = makeCtx();
|
|
867
|
+
for (const file of runbook.files) {
|
|
868
|
+
const content = resolveContent(file, ctx);
|
|
869
|
+
expect(content.length).toBeGreaterThan(0);
|
|
870
|
+
expect(typeof content).toBe("string");
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("all markdown files start with YAML frontmatter or a markdown heading", () => {
|
|
875
|
+
const ctx = makeCtx();
|
|
876
|
+
for (const file of runbook.files) {
|
|
877
|
+
if (!file.path.endsWith(".md")) continue;
|
|
878
|
+
const content = resolveContent(file, ctx);
|
|
879
|
+
const startsWithFrontmatter = content.startsWith("---\n");
|
|
880
|
+
const startsWithHeading = content.startsWith("#");
|
|
881
|
+
expect(startsWithFrontmatter || startsWithHeading).toBe(true);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("site.yaml starts with a YAML comment", () => {
|
|
886
|
+
const ctx = makeCtx();
|
|
887
|
+
const file = findFile("site.yaml");
|
|
888
|
+
const content = resolveContent(file, ctx);
|
|
889
|
+
expect(content.startsWith("#")).toBe(true);
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
// Branding Substitution Across All Context-Dependent Files
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
describe("runbook branding substitution", () => {
|
|
898
|
+
it("context-dependent files use brandName, not a hardcoded value", () => {
|
|
899
|
+
const ctx1 = makeCtx({
|
|
900
|
+
branding: {
|
|
901
|
+
siteName: "Alpha Ops",
|
|
902
|
+
brandName: "Alpha Corp",
|
|
903
|
+
brandUrl: "/",
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
const ctx2 = makeCtx({
|
|
907
|
+
branding: {
|
|
908
|
+
siteName: "Beta Ops",
|
|
909
|
+
brandName: "Beta Corp",
|
|
910
|
+
brandUrl: "/",
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// Files that use ctx.branding.brandName in their content
|
|
915
|
+
const brandDependentFiles = [
|
|
916
|
+
"content/procedures/deployment.md",
|
|
917
|
+
"content/reference/contacts/directory.md",
|
|
918
|
+
"content/reference/analytics/dashboards.md",
|
|
919
|
+
];
|
|
920
|
+
|
|
921
|
+
for (const path of brandDependentFiles) {
|
|
922
|
+
const file = findFile(path);
|
|
923
|
+
const content1 = resolveContent(file, ctx1);
|
|
924
|
+
const content2 = resolveContent(file, ctx2);
|
|
925
|
+
|
|
926
|
+
expect(content1).toContain("Alpha Corp");
|
|
927
|
+
expect(content1).not.toContain("Beta Corp");
|
|
928
|
+
expect(content2).toContain("Beta Corp");
|
|
929
|
+
expect(content2).not.toContain("Alpha Corp");
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("context-dependent files use siteName where appropriate", () => {
|
|
934
|
+
const ctx1 = makeCtx({
|
|
935
|
+
branding: {
|
|
936
|
+
siteName: "Gamma Hub",
|
|
937
|
+
brandName: "Gamma Inc",
|
|
938
|
+
brandUrl: "/",
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// site.yaml and index.md use siteName
|
|
943
|
+
const siteYaml = resolveContent(findFile("site.yaml"), ctx1);
|
|
944
|
+
expect(siteYaml).toContain("Gamma Hub");
|
|
945
|
+
|
|
946
|
+
const indexMd = resolveContent(findFile("index.md"), ctx1);
|
|
947
|
+
expect(indexMd).toContain("Gamma Hub");
|
|
948
|
+
|
|
949
|
+
const customizing = resolveContent(findFile("CUSTOMIZING.md"), ctx1);
|
|
950
|
+
expect(customizing).toContain("Gamma Hub");
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it("static templates produce identical output regardless of context", () => {
|
|
954
|
+
const ctx1 = makeCtx({
|
|
955
|
+
branding: { siteName: "A", brandName: "A Corp" },
|
|
956
|
+
});
|
|
957
|
+
const ctx2 = makeCtx({
|
|
958
|
+
branding: { siteName: "B", brandName: "B Corp" },
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const staticFiles = [
|
|
962
|
+
"content/troubleshooting/common-issues.md",
|
|
963
|
+
"content/reference/interfaces/api-template.md",
|
|
964
|
+
"content/reference/checklists/pre-deploy.md",
|
|
965
|
+
"content/incidents/escalation.md",
|
|
966
|
+
"content/incidents/post-mortem-template.md",
|
|
967
|
+
];
|
|
968
|
+
|
|
969
|
+
for (const path of staticFiles) {
|
|
970
|
+
const file = findFile(path);
|
|
971
|
+
expect(resolveContent(file, ctx1)).toBe(resolveContent(file, ctx2));
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
});
|