@codemcp/ade-core 0.0.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/.prettierignore +1 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/catalog/facets/architecture.d.ts +2 -0
- package/dist/catalog/facets/architecture.js +424 -0
- package/dist/catalog/facets/backpressure.d.ts +2 -0
- package/dist/catalog/facets/backpressure.js +123 -0
- package/dist/catalog/facets/practices.d.ts +2 -0
- package/dist/catalog/facets/practices.js +163 -0
- package/dist/catalog/facets/process.d.ts +2 -0
- package/dist/catalog/facets/process.js +47 -0
- package/dist/catalog/index.d.ts +14 -0
- package/dist/catalog/index.js +71 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +29 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +41 -0
- package/dist/resolver.d.ts +7 -0
- package/dist/resolver.js +142 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +2 -0
- package/dist/writers/git-hooks.d.ts +2 -0
- package/dist/writers/git-hooks.js +7 -0
- package/dist/writers/instruction.d.ts +2 -0
- package/dist/writers/instruction.js +6 -0
- package/dist/writers/knowledge.d.ts +2 -0
- package/dist/writers/knowledge.js +9 -0
- package/dist/writers/setup-note.d.ts +2 -0
- package/dist/writers/setup-note.js +7 -0
- package/dist/writers/skills.d.ts +2 -0
- package/dist/writers/skills.js +7 -0
- package/dist/writers/workflows.d.ts +2 -0
- package/dist/writers/workflows.js +16 -0
- package/eslint.config.mjs +40 -0
- package/nodemon.json +7 -0
- package/package.json +34 -0
- package/src/catalog/catalog.spec.ts +531 -0
- package/src/catalog/facets/architecture.ts +438 -0
- package/src/catalog/facets/backpressure.ts +143 -0
- package/src/catalog/facets/practices.ts +173 -0
- package/src/catalog/facets/process.ts +50 -0
- package/src/catalog/index.ts +86 -0
- package/src/config.spec.ts +165 -0
- package/src/config.ts +39 -0
- package/src/index.ts +49 -0
- package/src/registry.spec.ts +144 -0
- package/src/registry.ts +68 -0
- package/src/resolver.spec.ts +581 -0
- package/src/resolver.ts +170 -0
- package/src/types.ts +151 -0
- package/src/writers/git-hooks.ts +9 -0
- package/src/writers/instruction.spec.ts +42 -0
- package/src/writers/instruction.ts +8 -0
- package/src/writers/knowledge.spec.ts +26 -0
- package/src/writers/knowledge.ts +15 -0
- package/src/writers/setup-note.ts +9 -0
- package/src/writers/skills.spec.ts +109 -0
- package/src/writers/skills.ts +9 -0
- package/src/writers/workflows.spec.ts +72 -0
- package/src/writers/workflows.ts +26 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.vitest.json +7 -0
- package/vitest.config.ts +5 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getDefaultCatalog,
|
|
4
|
+
getFacet,
|
|
5
|
+
getOption,
|
|
6
|
+
sortFacets,
|
|
7
|
+
getVisibleOptions
|
|
8
|
+
} from "./index.js";
|
|
9
|
+
import { createDefaultRegistry, getProvisionWriter } from "../registry.js";
|
|
10
|
+
|
|
11
|
+
describe("catalog", () => {
|
|
12
|
+
describe("getDefaultCatalog", () => {
|
|
13
|
+
it("returns a catalog containing at least the 'process' facet", () => {
|
|
14
|
+
const catalog = getDefaultCatalog();
|
|
15
|
+
const process = getFacet(catalog, "process");
|
|
16
|
+
expect(process).toBeDefined();
|
|
17
|
+
expect(process!.id).toBe("process");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getFacet / getOption", () => {
|
|
22
|
+
it("process facet's 'codemcp-workflows' option has a recipe referencing the 'workflows' writer", () => {
|
|
23
|
+
const catalog = getDefaultCatalog();
|
|
24
|
+
const process = getFacet(catalog, "process")!;
|
|
25
|
+
const option = getOption(process, "codemcp-workflows");
|
|
26
|
+
|
|
27
|
+
expect(option).toBeDefined();
|
|
28
|
+
expect(option!.recipe.some((p) => p.writer === "workflows")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("process facet's 'native-agents-md' option has a recipe referencing only the 'instruction' writer", () => {
|
|
32
|
+
const catalog = getDefaultCatalog();
|
|
33
|
+
const process = getFacet(catalog, "process")!;
|
|
34
|
+
const option = getOption(process, "native-agents-md");
|
|
35
|
+
|
|
36
|
+
expect(option).toBeDefined();
|
|
37
|
+
const writers = option!.recipe.map((p) => p.writer);
|
|
38
|
+
expect(writers).toEqual(["instruction"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns undefined for a nonexistent facet id", () => {
|
|
42
|
+
const catalog = getDefaultCatalog();
|
|
43
|
+
expect(getFacet(catalog, "nonexistent")).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns undefined for a nonexistent option id", () => {
|
|
47
|
+
const catalog = getDefaultCatalog();
|
|
48
|
+
const process = getFacet(catalog, "process")!;
|
|
49
|
+
expect(getOption(process, "nonexistent")).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("architecture facet", () => {
|
|
54
|
+
it("exists in the default catalog", () => {
|
|
55
|
+
const catalog = getDefaultCatalog();
|
|
56
|
+
const architecture = getFacet(catalog, "architecture");
|
|
57
|
+
expect(architecture).toBeDefined();
|
|
58
|
+
expect(architecture!.required).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is single-select", () => {
|
|
62
|
+
const catalog = getDefaultCatalog();
|
|
63
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
64
|
+
expect(architecture.multiSelect).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("has tanstack option with skills for architecture, design, code, testing, and playwright", () => {
|
|
68
|
+
const catalog = getDefaultCatalog();
|
|
69
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
70
|
+
const tanstack = getOption(architecture, "tanstack");
|
|
71
|
+
|
|
72
|
+
expect(tanstack).toBeDefined();
|
|
73
|
+
const skillsProvisions = tanstack!.recipe.filter(
|
|
74
|
+
(p) => p.writer === "skills"
|
|
75
|
+
);
|
|
76
|
+
expect(skillsProvisions).toHaveLength(1);
|
|
77
|
+
|
|
78
|
+
const skills = (
|
|
79
|
+
skillsProvisions[0].config as { skills: { name: string }[] }
|
|
80
|
+
).skills;
|
|
81
|
+
const names = skills.map((s) => s.name);
|
|
82
|
+
expect(names).toContain("tanstack-architecture");
|
|
83
|
+
expect(names).toContain("tanstack-design");
|
|
84
|
+
expect(names).toContain("tanstack-code");
|
|
85
|
+
expect(names).toContain("tanstack-testing");
|
|
86
|
+
expect(names).toContain("playwright-cli");
|
|
87
|
+
|
|
88
|
+
// playwright-cli should be an external skill (has source, no body)
|
|
89
|
+
const playwright = skills.find(
|
|
90
|
+
(s: Record<string, unknown>) => s.name === "playwright-cli"
|
|
91
|
+
) as Record<string, unknown>;
|
|
92
|
+
expect(playwright.source).toBe(
|
|
93
|
+
"microsoft/playwright-cli/skills/playwright-cli"
|
|
94
|
+
);
|
|
95
|
+
expect(playwright).not.toHaveProperty("body");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("nodejs-backend option", () => {
|
|
100
|
+
it("has nodejs-backend option with skills for architecture, design, code, and testing", () => {
|
|
101
|
+
const catalog = getDefaultCatalog();
|
|
102
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
103
|
+
const nodejsBackend = getOption(architecture, "nodejs-backend");
|
|
104
|
+
|
|
105
|
+
expect(nodejsBackend).toBeDefined();
|
|
106
|
+
const skillsProvisions = nodejsBackend!.recipe.filter(
|
|
107
|
+
(p) => p.writer === "skills"
|
|
108
|
+
);
|
|
109
|
+
expect(skillsProvisions).toHaveLength(1);
|
|
110
|
+
|
|
111
|
+
const skills = (
|
|
112
|
+
skillsProvisions[0].config as { skills: { name: string }[] }
|
|
113
|
+
).skills;
|
|
114
|
+
const names = skills.map((s) => s.name);
|
|
115
|
+
expect(names).toContain("nodejs-backend-architecture");
|
|
116
|
+
expect(names).toContain("nodejs-backend-design");
|
|
117
|
+
expect(names).toContain("nodejs-backend-code");
|
|
118
|
+
expect(names).toContain("nodejs-backend-testing");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("nodejs-backend option declares docsets for tRPC, Drizzle, Express, and Zod", () => {
|
|
122
|
+
const catalog = getDefaultCatalog();
|
|
123
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
124
|
+
const nodejsBackend = getOption(architecture, "nodejs-backend")!;
|
|
125
|
+
|
|
126
|
+
expect(nodejsBackend.docsets).toBeDefined();
|
|
127
|
+
const ids = nodejsBackend.docsets!.map((d) => d.id);
|
|
128
|
+
expect(ids).toContain("trpc-docs");
|
|
129
|
+
expect(ids).toContain("drizzle-orm-docs");
|
|
130
|
+
expect(ids).toContain("express-docs");
|
|
131
|
+
expect(ids).toContain("zod-docs");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("each nodejs-backend docset has required fields", () => {
|
|
135
|
+
const catalog = getDefaultCatalog();
|
|
136
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
137
|
+
const nodejsBackend = getOption(architecture, "nodejs-backend")!;
|
|
138
|
+
|
|
139
|
+
for (const docset of nodejsBackend.docsets!) {
|
|
140
|
+
expect(docset.id).toBeTruthy();
|
|
141
|
+
expect(docset.label).toBeTruthy();
|
|
142
|
+
expect(docset.origin).toMatch(/^https:\/\//);
|
|
143
|
+
expect(docset.description).toBeTruthy();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("java-backend option", () => {
|
|
149
|
+
it("has java-backend option with skills for architecture, design, code, and testing", () => {
|
|
150
|
+
const catalog = getDefaultCatalog();
|
|
151
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
152
|
+
const javaBackend = getOption(architecture, "java-backend");
|
|
153
|
+
|
|
154
|
+
expect(javaBackend).toBeDefined();
|
|
155
|
+
const skillsProvisions = javaBackend!.recipe.filter(
|
|
156
|
+
(p) => p.writer === "skills"
|
|
157
|
+
);
|
|
158
|
+
expect(skillsProvisions).toHaveLength(1);
|
|
159
|
+
|
|
160
|
+
const skills = (
|
|
161
|
+
skillsProvisions[0].config as { skills: { name: string }[] }
|
|
162
|
+
).skills;
|
|
163
|
+
const names = skills.map((s) => s.name);
|
|
164
|
+
expect(names).toContain("java-backend-architecture");
|
|
165
|
+
expect(names).toContain("java-backend-design");
|
|
166
|
+
expect(names).toContain("java-backend-code");
|
|
167
|
+
expect(names).toContain("java-backend-testing");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("java-backend option declares docsets for Spring Boot, Spring Data JPA, Spring Security, and Lombok", () => {
|
|
171
|
+
const catalog = getDefaultCatalog();
|
|
172
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
173
|
+
const javaBackend = getOption(architecture, "java-backend")!;
|
|
174
|
+
|
|
175
|
+
expect(javaBackend.docsets).toBeDefined();
|
|
176
|
+
const ids = javaBackend.docsets!.map((d) => d.id);
|
|
177
|
+
expect(ids).toContain("spring-boot-docs");
|
|
178
|
+
expect(ids).toContain("spring-data-jpa-docs");
|
|
179
|
+
expect(ids).toContain("spring-security-docs");
|
|
180
|
+
expect(ids).toContain("lombok-docs");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("each java-backend docset has required fields", () => {
|
|
184
|
+
const catalog = getDefaultCatalog();
|
|
185
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
186
|
+
const javaBackend = getOption(architecture, "java-backend")!;
|
|
187
|
+
|
|
188
|
+
for (const docset of javaBackend.docsets!) {
|
|
189
|
+
expect(docset.id).toBeTruthy();
|
|
190
|
+
expect(docset.label).toBeTruthy();
|
|
191
|
+
expect(docset.origin).toMatch(/^https:\/\//);
|
|
192
|
+
expect(docset.description).toBeTruthy();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("architecture facet docsets", () => {
|
|
198
|
+
it("tanstack option declares docsets for Router, Query, Form, and Table", () => {
|
|
199
|
+
const catalog = getDefaultCatalog();
|
|
200
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
201
|
+
const tanstack = getOption(architecture, "tanstack")!;
|
|
202
|
+
|
|
203
|
+
expect(tanstack.docsets).toBeDefined();
|
|
204
|
+
const ids = tanstack.docsets!.map((d) => d.id);
|
|
205
|
+
expect(ids).toContain("tanstack-router-docs");
|
|
206
|
+
expect(ids).toContain("tanstack-query-docs");
|
|
207
|
+
expect(ids).toContain("tanstack-form-docs");
|
|
208
|
+
expect(ids).toContain("tanstack-table-docs");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("each docset has required fields", () => {
|
|
212
|
+
const catalog = getDefaultCatalog();
|
|
213
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
214
|
+
const tanstack = getOption(architecture, "tanstack")!;
|
|
215
|
+
|
|
216
|
+
for (const docset of tanstack.docsets!) {
|
|
217
|
+
expect(docset.id).toBeTruthy();
|
|
218
|
+
expect(docset.label).toBeTruthy();
|
|
219
|
+
expect(docset.origin).toMatch(/^https:\/\//);
|
|
220
|
+
expect(docset.description).toBeTruthy();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("practices facet", () => {
|
|
226
|
+
it("exists in the default catalog", () => {
|
|
227
|
+
const catalog = getDefaultCatalog();
|
|
228
|
+
const practices = getFacet(catalog, "practices");
|
|
229
|
+
expect(practices).toBeDefined();
|
|
230
|
+
expect(practices!.required).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("is multi-select", () => {
|
|
234
|
+
const catalog = getDefaultCatalog();
|
|
235
|
+
const practices = getFacet(catalog, "practices")!;
|
|
236
|
+
expect(practices.multiSelect).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("has conventional-commits option with a single skill", () => {
|
|
240
|
+
const catalog = getDefaultCatalog();
|
|
241
|
+
const practices = getFacet(catalog, "practices")!;
|
|
242
|
+
const option = getOption(practices, "conventional-commits");
|
|
243
|
+
|
|
244
|
+
expect(option).toBeDefined();
|
|
245
|
+
const skills = (
|
|
246
|
+
option!.recipe.find((p) => p.writer === "skills")!.config as {
|
|
247
|
+
skills: { name: string }[];
|
|
248
|
+
}
|
|
249
|
+
).skills;
|
|
250
|
+
expect(skills).toHaveLength(1);
|
|
251
|
+
expect(skills[0].name).toBe("conventional-commits");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("conventional-commits option declares the spec docset", () => {
|
|
255
|
+
const catalog = getDefaultCatalog();
|
|
256
|
+
const practices = getFacet(catalog, "practices")!;
|
|
257
|
+
const option = getOption(practices, "conventional-commits")!;
|
|
258
|
+
|
|
259
|
+
expect(option.docsets).toBeDefined();
|
|
260
|
+
expect(option.docsets).toHaveLength(1);
|
|
261
|
+
expect(option.docsets![0].id).toBe("conventional-commits-spec");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("has tdd-london option with a single skill", () => {
|
|
265
|
+
const catalog = getDefaultCatalog();
|
|
266
|
+
const practices = getFacet(catalog, "practices")!;
|
|
267
|
+
const option = getOption(practices, "tdd-london");
|
|
268
|
+
|
|
269
|
+
expect(option).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("has adr-nygard option with a single skill", () => {
|
|
273
|
+
const catalog = getDefaultCatalog();
|
|
274
|
+
const practices = getFacet(catalog, "practices")!;
|
|
275
|
+
const option = getOption(practices, "adr-nygard");
|
|
276
|
+
|
|
277
|
+
expect(option).toBeDefined();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("backpressure facet", () => {
|
|
282
|
+
it("exists in the default catalog", () => {
|
|
283
|
+
const catalog = getDefaultCatalog();
|
|
284
|
+
const backpressure = getFacet(catalog, "backpressure");
|
|
285
|
+
expect(backpressure).toBeDefined();
|
|
286
|
+
expect(backpressure!.required).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("is multi-select", () => {
|
|
290
|
+
const catalog = getDefaultCatalog();
|
|
291
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
292
|
+
expect(backpressure.multiSelect).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("depends on architecture facet", () => {
|
|
296
|
+
const catalog = getDefaultCatalog();
|
|
297
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
298
|
+
expect(backpressure.dependsOn).toContain("architecture");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("has per-architecture lint-build-precommit options with git-hooks provisions", () => {
|
|
302
|
+
const catalog = getDefaultCatalog();
|
|
303
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
304
|
+
|
|
305
|
+
for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) {
|
|
306
|
+
const option = getOption(
|
|
307
|
+
backpressure,
|
|
308
|
+
`lint-build-precommit-${archId}`
|
|
309
|
+
);
|
|
310
|
+
expect(option, `lint-build-precommit-${archId} missing`).toBeDefined();
|
|
311
|
+
expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true);
|
|
312
|
+
|
|
313
|
+
const gitHooksProvision = option!.recipe.find(
|
|
314
|
+
(p) => p.writer === "git-hooks"
|
|
315
|
+
)!;
|
|
316
|
+
const hooks = (
|
|
317
|
+
gitHooksProvision.config as { hooks: { phase: string }[] }
|
|
318
|
+
).hooks;
|
|
319
|
+
expect(hooks.some((h) => h.phase === "pre-commit")).toBe(true);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("lint-build-precommit options have an instruction provision for WIP commits", () => {
|
|
324
|
+
const catalog = getDefaultCatalog();
|
|
325
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
326
|
+
|
|
327
|
+
for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) {
|
|
328
|
+
const option = getOption(
|
|
329
|
+
backpressure,
|
|
330
|
+
`lint-build-precommit-${archId}`
|
|
331
|
+
)!;
|
|
332
|
+
expect(option.recipe.some((p) => p.writer === "instruction")).toBe(
|
|
333
|
+
true
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("lint-build-precommit options have a setup-note provision", () => {
|
|
339
|
+
const catalog = getDefaultCatalog();
|
|
340
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
341
|
+
|
|
342
|
+
for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) {
|
|
343
|
+
const option = getOption(
|
|
344
|
+
backpressure,
|
|
345
|
+
`lint-build-precommit-${archId}`
|
|
346
|
+
)!;
|
|
347
|
+
const note = option.recipe.find((p) => p.writer === "setup-note");
|
|
348
|
+
expect(
|
|
349
|
+
note,
|
|
350
|
+
`lint-build-precommit-${archId} missing setup-note`
|
|
351
|
+
).toBeDefined();
|
|
352
|
+
expect((note!.config as { text: string }).text).toBeTruthy();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("has per-architecture unit-test-prepush options with git-hooks provisions", () => {
|
|
357
|
+
const catalog = getDefaultCatalog();
|
|
358
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
359
|
+
|
|
360
|
+
for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) {
|
|
361
|
+
const option = getOption(backpressure, `unit-test-prepush-${archId}`);
|
|
362
|
+
expect(option, `unit-test-prepush-${archId} missing`).toBeDefined();
|
|
363
|
+
expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true);
|
|
364
|
+
|
|
365
|
+
const gitHooksProvision = option!.recipe.find(
|
|
366
|
+
(p) => p.writer === "git-hooks"
|
|
367
|
+
)!;
|
|
368
|
+
const hooks = (
|
|
369
|
+
gitHooksProvision.config as { hooks: { phase: string }[] }
|
|
370
|
+
).hooks;
|
|
371
|
+
expect(hooks.some((h) => h.phase === "pre-push")).toBe(true);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("hook scripts contain the swallow-on-success pattern", () => {
|
|
376
|
+
const catalog = getDefaultCatalog();
|
|
377
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
378
|
+
|
|
379
|
+
for (const option of backpressure.options) {
|
|
380
|
+
const gitHooksProvision = option.recipe.find(
|
|
381
|
+
(p) => p.writer === "git-hooks"
|
|
382
|
+
)!;
|
|
383
|
+
const hooks = (
|
|
384
|
+
gitHooksProvision.config as { hooks: { script: string }[] }
|
|
385
|
+
).hooks;
|
|
386
|
+
for (const hook of hooks) {
|
|
387
|
+
expect(hook.script).toContain("✓");
|
|
388
|
+
expect(hook.script).toContain("exit_code");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("all options have an available() function", () => {
|
|
394
|
+
const catalog = getDefaultCatalog();
|
|
395
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
396
|
+
|
|
397
|
+
for (const option of backpressure.options) {
|
|
398
|
+
expect(
|
|
399
|
+
typeof option.available,
|
|
400
|
+
`option ${option.id} missing available()`
|
|
401
|
+
).toBe("function");
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe("backpressure facet — available()", () => {
|
|
407
|
+
it("tanstack options are visible when architecture=tanstack", () => {
|
|
408
|
+
const catalog = getDefaultCatalog();
|
|
409
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
410
|
+
const architectureFacet = getFacet(catalog, "architecture")!;
|
|
411
|
+
const tanstackOption = getOption(architectureFacet, "tanstack")!;
|
|
412
|
+
|
|
413
|
+
const visible = getVisibleOptions(
|
|
414
|
+
backpressure,
|
|
415
|
+
{ architecture: "tanstack" },
|
|
416
|
+
catalog
|
|
417
|
+
);
|
|
418
|
+
const ids = visible.map((o) => o.id);
|
|
419
|
+
expect(ids).toContain("lint-build-precommit-tanstack");
|
|
420
|
+
expect(ids).toContain("unit-test-prepush-tanstack");
|
|
421
|
+
expect(ids).not.toContain("lint-build-precommit-java-backend");
|
|
422
|
+
expect(tanstackOption).toBeDefined(); // guard
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("java-backend options are visible when architecture=java-backend", () => {
|
|
426
|
+
const catalog = getDefaultCatalog();
|
|
427
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
428
|
+
|
|
429
|
+
const visible = getVisibleOptions(
|
|
430
|
+
backpressure,
|
|
431
|
+
{ architecture: "java-backend" },
|
|
432
|
+
catalog
|
|
433
|
+
);
|
|
434
|
+
const ids = visible.map((o) => o.id);
|
|
435
|
+
expect(ids).toContain("lint-build-precommit-java-backend");
|
|
436
|
+
expect(ids).toContain("unit-test-prepush-java-backend");
|
|
437
|
+
expect(ids).not.toContain("lint-build-precommit-tanstack");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("no options visible when architecture is not selected", () => {
|
|
441
|
+
const catalog = getDefaultCatalog();
|
|
442
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
443
|
+
|
|
444
|
+
const visible = getVisibleOptions(backpressure, {}, catalog);
|
|
445
|
+
expect(visible).toHaveLength(0);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("only the two matching options are visible per architecture", () => {
|
|
449
|
+
const catalog = getDefaultCatalog();
|
|
450
|
+
const backpressure = getFacet(catalog, "backpressure")!;
|
|
451
|
+
|
|
452
|
+
for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) {
|
|
453
|
+
const visible = getVisibleOptions(
|
|
454
|
+
backpressure,
|
|
455
|
+
{ architecture: archId },
|
|
456
|
+
catalog
|
|
457
|
+
);
|
|
458
|
+
expect(visible, `expected 2 options for ${archId}`).toHaveLength(2);
|
|
459
|
+
expect(visible.every((o) => o.id.endsWith(`-${archId}`))).toBe(true);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe("sortFacets", () => {
|
|
465
|
+
it("returns all facets", () => {
|
|
466
|
+
const catalog = getDefaultCatalog();
|
|
467
|
+
const sorted = sortFacets(catalog);
|
|
468
|
+
expect(sorted).toHaveLength(catalog.facets.length);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("places backpressure after architecture", () => {
|
|
472
|
+
const catalog = getDefaultCatalog();
|
|
473
|
+
const sorted = sortFacets(catalog);
|
|
474
|
+
const archIdx = sorted.findIndex((f) => f.id === "architecture");
|
|
475
|
+
const bpIdx = sorted.findIndex((f) => f.id === "backpressure");
|
|
476
|
+
expect(archIdx).toBeLessThan(bpIdx);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("facets without dependsOn are not placed after their dependents", () => {
|
|
480
|
+
const catalog = getDefaultCatalog();
|
|
481
|
+
const sorted = sortFacets(catalog);
|
|
482
|
+
for (const facet of sorted) {
|
|
483
|
+
const facetIdx = sorted.findIndex((f) => f.id === facet.id);
|
|
484
|
+
for (const depId of facet.dependsOn ?? []) {
|
|
485
|
+
const depIdx = sorted.findIndex((f) => f.id === depId);
|
|
486
|
+
expect(depIdx, `${depId} must come before ${facet.id}`).toBeLessThan(
|
|
487
|
+
facetIdx
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("getVisibleOptions", () => {
|
|
495
|
+
it("returns all options when none have available()", () => {
|
|
496
|
+
const catalog = getDefaultCatalog();
|
|
497
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
498
|
+
const visible = getVisibleOptions(architecture, {}, catalog);
|
|
499
|
+
expect(visible).toHaveLength(architecture.options.length);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("returns all options when available() returns true for all", () => {
|
|
503
|
+
const catalog = getDefaultCatalog();
|
|
504
|
+
const architecture = getFacet(catalog, "architecture")!;
|
|
505
|
+
const visible = getVisibleOptions(
|
|
506
|
+
architecture,
|
|
507
|
+
{ architecture: "tanstack" },
|
|
508
|
+
catalog
|
|
509
|
+
);
|
|
510
|
+
expect(visible).toHaveLength(architecture.options.length);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("catalog + registry integration", () => {
|
|
515
|
+
it("every recipe provision references a writer that exists in the default registry", () => {
|
|
516
|
+
const catalog = getDefaultCatalog();
|
|
517
|
+
const registry = createDefaultRegistry();
|
|
518
|
+
|
|
519
|
+
for (const facet of catalog.facets) {
|
|
520
|
+
for (const option of facet.options) {
|
|
521
|
+
for (const provision of option.recipe) {
|
|
522
|
+
expect(
|
|
523
|
+
getProvisionWriter(registry, provision.writer),
|
|
524
|
+
`writer "${provision.writer}" referenced in ${facet.id}/${option.id} must exist in default registry`
|
|
525
|
+
).toBeDefined();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|