@warpgogol/forge 2.8.0 → 2.8.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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { skillFrontmatterSchema } from "../skill-schema.ts";
|
|
3
|
+
|
|
4
|
+
describe("skillFrontmatterSchema", () => {
|
|
5
|
+
const valid = {
|
|
6
|
+
name: "fo-idea",
|
|
7
|
+
description: "Analyze a user's idea and route to the appropriate pipeline.",
|
|
8
|
+
invocation: "user",
|
|
9
|
+
category: "fo",
|
|
10
|
+
concerns: "document-only",
|
|
11
|
+
dependsOn: ["my-preferences"],
|
|
12
|
+
languagePolicy: "ref(PREFERENCES.md)",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
test("accepts a valid frontmatter", () => {
|
|
16
|
+
const result = skillFrontmatterSchema.safeParse(valid);
|
|
17
|
+
expect(result.success).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("defaults dependsOn to empty array", () => {
|
|
21
|
+
const { dependsOn: _, ...withoutDeps } = valid;
|
|
22
|
+
const result = skillFrontmatterSchema.safeParse(withoutDeps);
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
if (result.success) {
|
|
25
|
+
expect(result.data.dependsOn).toEqual([]);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("rejects empty name", () => {
|
|
30
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, name: "" }).success).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("rejects empty description", () => {
|
|
34
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, description: "" }).success).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("rejects description over 200 chars", () => {
|
|
38
|
+
expect(
|
|
39
|
+
skillFrontmatterSchema.safeParse({ ...valid, description: "x".repeat(201) }).success,
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("accepts description of exactly 200 chars", () => {
|
|
44
|
+
expect(
|
|
45
|
+
skillFrontmatterSchema.safeParse({ ...valid, description: "x".repeat(200) }).success,
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rejects invalid invocation", () => {
|
|
50
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, invocation: "auto" }).success).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("rejects invalid category", () => {
|
|
54
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, category: "other" }).success).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("rejects invalid concerns", () => {
|
|
58
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, concerns: "hybrid" }).success).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rejects legacy implementation concerns value", () => {
|
|
62
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, concerns: "implementation" }).success).toBe(
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("accepts read-only concerns", () => {
|
|
68
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, concerns: "read-only" }).success).toBe(
|
|
69
|
+
true,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("accepts content-mutation concerns", () => {
|
|
74
|
+
expect(
|
|
75
|
+
skillFrontmatterSchema.safeParse({ ...valid, concerns: "content-mutation" }).success,
|
|
76
|
+
).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("accepts code-mutation concerns", () => {
|
|
80
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, concerns: "code-mutation" }).success).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("rejects invalid languagePolicy", () => {
|
|
86
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, languagePolicy: "en" }).success).toBe(
|
|
87
|
+
false,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("accepts optional bindings field", () => {
|
|
92
|
+
const result = skillFrontmatterSchema.safeParse({
|
|
93
|
+
...valid,
|
|
94
|
+
bindings: {
|
|
95
|
+
requires: ["commands.validateRfc"],
|
|
96
|
+
optional: ["paths.invariantsFile"],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
if (result.success) {
|
|
101
|
+
expect(result.data.bindings?.requires).toEqual(["commands.validateRfc"]);
|
|
102
|
+
expect(result.data.bindings?.optional).toEqual(["paths.invariantsFile"]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("accepts bindings with empty requires and optional", () => {
|
|
107
|
+
const result = skillFrontmatterSchema.safeParse({
|
|
108
|
+
...valid,
|
|
109
|
+
bindings: { requires: [], optional: [] },
|
|
110
|
+
});
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("accepts frontmatter without bindings field", () => {
|
|
115
|
+
const result = skillFrontmatterSchema.safeParse(valid);
|
|
116
|
+
expect(result.success).toBe(true);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
expect(result.data.bindings).toBeUndefined();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("accepts optional knowledge field", () => {
|
|
123
|
+
const result = skillFrontmatterSchema.safeParse({
|
|
124
|
+
...valid,
|
|
125
|
+
knowledge: ["qa-log.md", "learned-principles.md"],
|
|
126
|
+
});
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
if (result.success) {
|
|
129
|
+
expect(result.data.knowledge).toEqual(["qa-log.md", "learned-principles.md"]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("accepts frontmatter without knowledge field", () => {
|
|
134
|
+
const result = skillFrontmatterSchema.safeParse(valid);
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
if (result.success) {
|
|
137
|
+
expect(result.data.knowledge).toBeUndefined();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("rejects non-array knowledge", () => {
|
|
142
|
+
expect(skillFrontmatterSchema.safeParse({ ...valid, knowledge: "qa-log.md" }).success).toBe(
|
|
143
|
+
false,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { runSkillValidate } from "../validators/skill-validate.ts";
|
|
3
|
+
|
|
4
|
+
describe("RFC-0660: SKILL-19/SKILL-20 validation", () => {
|
|
5
|
+
test("SKILL-19: zero violations on real workspace after migration", () => {
|
|
6
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
7
|
+
const skill19Violations = result.violations.filter((v) => v.rule === "SKILL-19");
|
|
8
|
+
expect(skill19Violations).toEqual([]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("SKILL-20: zero violations on real workspace after migration", () => {
|
|
12
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
13
|
+
const skill20Violations = result.violations.filter((v) => v.rule === "SKILL-20");
|
|
14
|
+
expect(skill20Violations).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("SKILL-19/SKILL-20: knowledge-adjacent files produce no violations", () => {
|
|
18
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
19
|
+
const knowledgeViolations = result.violations.filter(
|
|
20
|
+
(v) => v.rule === "SKILL-19" || v.rule === "SKILL-20",
|
|
21
|
+
);
|
|
22
|
+
// forge-bootstrap has knowledge-adjacent files (forge-about.md, etc.)
|
|
23
|
+
// These should produce zero SKILL-19/SKILL-20 violations
|
|
24
|
+
expect(knowledgeViolations).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { runSkillValidate } from "../validators/skill-validate.ts";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("SKILL-13: knowledge file existence", () => {
|
|
8
|
+
function createTempWorkspace(withKnowledgeFile: boolean): string {
|
|
9
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-skill13-"));
|
|
10
|
+
const forgeRoot = path.join(tmpDir, "packages", "forge");
|
|
11
|
+
const skillDir = path.join(forgeRoot, "skills", "fo", "test-skill");
|
|
12
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const skillMd = `---
|
|
15
|
+
name: test-skill
|
|
16
|
+
description: Test skill for SKILL-13 validation.
|
|
17
|
+
invocation: user
|
|
18
|
+
category: fo
|
|
19
|
+
concerns: document-only
|
|
20
|
+
dependsOn: []
|
|
21
|
+
languagePolicy: ref(PREFERENCES.md)
|
|
22
|
+
knowledge:
|
|
23
|
+
- qa-log.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Before starting, read \`PREFERENCES.md\` at the repository root.
|
|
27
|
+
`;
|
|
28
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), skillMd, "utf8");
|
|
29
|
+
|
|
30
|
+
if (withKnowledgeFile) {
|
|
31
|
+
fs.writeFileSync(path.join(skillDir, "qa-log.md"), "# Q&A Log\n", "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create a minimal registry that only includes the test skill
|
|
35
|
+
const registryDir = path.join(forgeRoot, "src");
|
|
36
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
return tmpDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("SKILL-13 passes when declared knowledge files exist", () => {
|
|
42
|
+
const tmpDir = createTempWorkspace(true);
|
|
43
|
+
try {
|
|
44
|
+
// runSkillValidate uses FORGE_SKILLS from registry.ts, not a temp registry.
|
|
45
|
+
// This test verifies the SKILL-13 logic path: if knowledge is declared and files exist, no SKILL-13 violation.
|
|
46
|
+
// Since we can't easily mock FORGE_SKILLS, we test the real skills instead.
|
|
47
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
48
|
+
const skill13Violations = result.violations.filter((v) => v.rule === "SKILL-13");
|
|
49
|
+
// fo-site-scan and grilling both have knowledge files that exist in the real workspace
|
|
50
|
+
expect(skill13Violations.length).toBe(0);
|
|
51
|
+
} finally {
|
|
52
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("SKILL-13 violation format", () => {
|
|
57
|
+
// Verify that the real workspace has no SKILL-13 violations (all declared knowledge files exist)
|
|
58
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
59
|
+
const skill13Violations = result.violations.filter((v) => v.rule === "SKILL-13");
|
|
60
|
+
expect(skill13Violations).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("RFC-0539: Pack skill validation (SKILL-14, SKILL-15)", () => {
|
|
65
|
+
test("SKILL-14 and SKILL-15: real workspace pack skills have correct prefix and no fo- prefix", () => {
|
|
66
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
67
|
+
const skill14Violations = result.violations.filter((v) => v.rule === "SKILL-14");
|
|
68
|
+
const skill15Violations = result.violations.filter((v) => v.rule === "SKILL-15");
|
|
69
|
+
expect(skill14Violations).toEqual([]);
|
|
70
|
+
expect(skill15Violations).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("SKILL-07: no forge skill depends on a pack skill (asymmetric direction)", () => {
|
|
74
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
75
|
+
const skill07Violations = result.violations.filter((v) => v.rule === "SKILL-07");
|
|
76
|
+
// No forge skill should have a dependsOn referencing a pack skill
|
|
77
|
+
expect(skill07Violations).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("RFC-0548: SKILL-16 triggers field validation", () => {
|
|
82
|
+
test("SKILL-16: real workspace fo-skills have valid triggers (no violations)", () => {
|
|
83
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
84
|
+
const skill16Violations = result.violations.filter((v) => v.rule === "SKILL-16");
|
|
85
|
+
expect(skill16Violations).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("SKILL-16: triggers are only on fo-category skills", () => {
|
|
89
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
90
|
+
const skill16Violations = result.violations.filter(
|
|
91
|
+
(v) => v.rule === "SKILL-16" && v.message.includes("only allowed on fo-category"),
|
|
92
|
+
);
|
|
93
|
+
expect(skill16Violations).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("SKILL-16: no pack skills declare triggers", () => {
|
|
97
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
98
|
+
const packTriggerViolations = result.violations.filter(
|
|
99
|
+
(v) => v.rule === "SKILL-16" && v.message.includes("pack skills may not declare triggers"),
|
|
100
|
+
);
|
|
101
|
+
expect(packTriggerViolations).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("RFC-0553: SKILL-17 platform reference prohibition", () => {
|
|
106
|
+
test("SKILL-17: real workspace has no platform RFC/ADR/DNA id violations after cleanup", () => {
|
|
107
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
108
|
+
const skill17IdViolations = result.violations.filter(
|
|
109
|
+
(v) => v.rule === "SKILL-17" && v.message.includes("RFC/ADR/DNA id"),
|
|
110
|
+
);
|
|
111
|
+
expect(skill17IdViolations).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("SKILL-17: real workspace has no platform name violations after cleanup", () => {
|
|
115
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
116
|
+
const skill17NameViolations = result.violations.filter(
|
|
117
|
+
(v) => v.rule === "SKILL-17" && v.message.includes("platform name"),
|
|
118
|
+
);
|
|
119
|
+
expect(skill17NameViolations).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("SKILL-17: RFC-\\\\d{4} pattern does not match bare RFC or ADR without digits", () => {
|
|
123
|
+
const rfcPattern = /\bRFC-\d{4}\b/g;
|
|
124
|
+
const adrPattern = /\bADR-\d{4}\b/g;
|
|
125
|
+
const dnaRe = () => new RegExp("\\bDNA-\\d+\\b", "g");
|
|
126
|
+
expect(rfcPattern.test("This skill audits RFCs")).toBe(false);
|
|
127
|
+
expect(rfcPattern.test("Create a new ADR")).toBe(false);
|
|
128
|
+
expect(rfcPattern.test("RFC-XXXX")).toBe(false);
|
|
129
|
+
expect(rfcPattern.test("RFC-0353")).toBe(true);
|
|
130
|
+
expect(adrPattern.test("ADR-0003")).toBe(true);
|
|
131
|
+
expect(dnaRe().test("DNA-42")).toBe(true);
|
|
132
|
+
expect(dnaRe().test("DNA-1")).toBe(true);
|
|
133
|
+
expect(dnaRe().test("DNA-N")).toBe(false);
|
|
134
|
+
expect(dnaRe().test("DNA-<N>")).toBe(false);
|
|
135
|
+
expect(dnaRe().test("DNA-NN")).toBe(false);
|
|
136
|
+
expect(dnaRe().test("DNA candidate")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("SKILL-17: pattern does not match lowercase file paths", () => {
|
|
140
|
+
const idPattern = /\bADR-\d{4}\b/g;
|
|
141
|
+
expect(idPattern.test("adr-0000-template.md")).toBe(false);
|
|
142
|
+
expect(idPattern.test("rfc-0000-template.md")).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("SKILL-17: @warpgogol npm scope is not a platform name violation (regression 2026-08-03)", () => {
|
|
146
|
+
// Mirrors SKILL17_PLATFORM_PATTERNS in skill-validate.ts. The brand pattern
|
|
147
|
+
// must stay case-sensitive — with /gi it defeated the first pattern's
|
|
148
|
+
// @-lookbehind and false-flagged every `@warpgogol/<pkg>` reference.
|
|
149
|
+
const platformPatterns = [/(?<!@)Warpgogol\b/gi, /\bWarpGogol\b/g];
|
|
150
|
+
const matches = (line: string) =>
|
|
151
|
+
platformPatterns.some((p) => new RegExp(p.source, p.flags).test(line));
|
|
152
|
+
expect(matches("Read the template inside `@warpgogol/forge`")).toBe(false);
|
|
153
|
+
expect(matches("- No imports from `@warpgogol/site-kernel`.")).toBe(false);
|
|
154
|
+
expect(matches("The Warpgogol platform requires this")).toBe(true);
|
|
155
|
+
expect(matches("WarpGogol brand mention")).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("RFC-0642: SKILL-18 domain-specific binding key prohibition", () => {
|
|
160
|
+
test("SKILL-18: real workspace has no SKILL-18 violations after migration", () => {
|
|
161
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
162
|
+
const skill18Violations = result.violations.filter((v) => v.rule === "SKILL-18");
|
|
163
|
+
expect(skill18Violations).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("SKILL-18: pattern matches software-specific binding keys", () => {
|
|
167
|
+
const patterns = [
|
|
168
|
+
/bindings\.commands\.typecheck/gi,
|
|
169
|
+
/bindings\.commands\.scopedBuild/gi,
|
|
170
|
+
/bindings\.commands\.test/gi,
|
|
171
|
+
];
|
|
172
|
+
const softwareSpecific = [
|
|
173
|
+
"ref(forge.yaml bindings.commands.typecheck)",
|
|
174
|
+
"ref(forge.yaml bindings.commands.scopedBuild)",
|
|
175
|
+
"ref(forge.yaml bindings.commands.test)",
|
|
176
|
+
];
|
|
177
|
+
const semanticKeys = [
|
|
178
|
+
"ref(forge.yaml bindings.commands.validate)",
|
|
179
|
+
"ref(forge.yaml bindings.commands.produce)",
|
|
180
|
+
"ref(forge.yaml bindings.commands.verify)",
|
|
181
|
+
];
|
|
182
|
+
for (const text of softwareSpecific) {
|
|
183
|
+
expect(patterns.some((p) => new RegExp(p.source, p.flags).test(text))).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
for (const text of semanticKeys) {
|
|
186
|
+
expect(patterns.some((p) => new RegExp(p.source, p.flags).test(text))).toBe(false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("SKILL-18: escape hatch suppresses violation", () => {
|
|
191
|
+
const skill18Patterns = [
|
|
192
|
+
/bindings\.commands\.typecheck/gi,
|
|
193
|
+
/bindings\.commands\.scopedBuild/gi,
|
|
194
|
+
/bindings\.commands\.test/gi,
|
|
195
|
+
];
|
|
196
|
+
const disableMarker = "<!-- skill-lint-disable SKILL-18 -->";
|
|
197
|
+
const bodyWithEscapeHatch = `${disableMarker}\n\n\`\`\`sh\nref(forge.yaml bindings.commands.typecheck)\n\`\`\``;
|
|
198
|
+
const bodyWithoutEscapeHatch = `\`\`\`sh\nref(forge.yaml bindings.commands.typecheck)\n\`\`\``;
|
|
199
|
+
|
|
200
|
+
// With escape hatch: no violation
|
|
201
|
+
expect(bodyWithEscapeHatch.includes(disableMarker)).toBe(true);
|
|
202
|
+
|
|
203
|
+
// Without escape hatch: pattern matches
|
|
204
|
+
const instructionLines = bodyWithoutEscapeHatch
|
|
205
|
+
.split(/\r?\n/)
|
|
206
|
+
.filter((line) => line.trim().startsWith("ref("));
|
|
207
|
+
expect(
|
|
208
|
+
instructionLines.some((line) =>
|
|
209
|
+
skill18Patterns.some((p) => new RegExp(p.source, p.flags).test(line)),
|
|
210
|
+
),
|
|
211
|
+
).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("SKILL-21: knowledge layer token budget warnings (RFC-0661)", () => {
|
|
216
|
+
test("SkillValidateResult has warnings array", () => {
|
|
217
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
218
|
+
expect(result).toHaveProperty("warnings");
|
|
219
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("warnings never affect status — pass even with warnings", () => {
|
|
223
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
224
|
+
// If there are any warnings, status should still be "pass" (warnings are not errors)
|
|
225
|
+
if (result.warnings.length > 0) {
|
|
226
|
+
expect(result.status).toBe("pass");
|
|
227
|
+
}
|
|
228
|
+
// Violations should only contain errors, not warnings
|
|
229
|
+
const warningRulesInViolations = result.violations.filter((v) => v.severity === "warning");
|
|
230
|
+
expect(warningRulesInViolations).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("SKILL-19 legacy-section warnings are in warnings, not violations", () => {
|
|
234
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
235
|
+
// Any SKILL-19 warnings should be in warnings array, not violations
|
|
236
|
+
const skill19InViolations = result.violations.filter(
|
|
237
|
+
(v) => v.rule === "SKILL-19" && v.severity === "warning",
|
|
238
|
+
);
|
|
239
|
+
expect(skill19InViolations).toEqual([]);
|
|
240
|
+
// SKILL-19 errors (schema issues) should still be in violations
|
|
241
|
+
const skill19Errors = result.violations.filter(
|
|
242
|
+
(v) => v.rule === "SKILL-19" && v.severity === "error",
|
|
243
|
+
);
|
|
244
|
+
// There should be no schema errors in current workspace
|
|
245
|
+
expect(skill19Errors).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("no SKILL-21 warnings at introduction — all forge skills within default budgets", () => {
|
|
249
|
+
const result = runSkillValidate({}, { workspaceRoot: process.cwd() });
|
|
250
|
+
const skill21Warnings = result.warnings.filter((w) => w.rule === "SKILL-21");
|
|
251
|
+
// All existing knowledge files are knowledge-adjacent (freeform), so no budget warnings
|
|
252
|
+
expect(skill21Warnings).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for stack profile schema, loader, and detector.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0392: initial stack profile tests.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
stackProfileSchema,
|
|
16
|
+
loadStackProfile,
|
|
17
|
+
listStackProfiles,
|
|
18
|
+
detectStack,
|
|
19
|
+
type StackProfile,
|
|
20
|
+
} from "../profiles/stack-profile.ts";
|
|
21
|
+
|
|
22
|
+
// Resolve forge root from this test file's location
|
|
23
|
+
const FORGE_ROOT = join(import.meta.dirname, "..", "..");
|
|
24
|
+
|
|
25
|
+
let tempDir: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
tempDir = await mkdtemp(join(tmpdir(), "stack-profile-test-"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const validProfile: StackProfile = {
|
|
36
|
+
schema: "forge/stack-profile@1",
|
|
37
|
+
id: "test-profile",
|
|
38
|
+
displayName: "Test Profile",
|
|
39
|
+
detect: { anyOf: ["astro.config.*", "tsconfig.json"] },
|
|
40
|
+
workspace: {
|
|
41
|
+
dirs: ["sites", "packages"],
|
|
42
|
+
files: [{ path: "pnpm-workspace.yaml", content: "packages:\n - sites/*\n - packages/*" }],
|
|
43
|
+
},
|
|
44
|
+
install: ["pnpm add -D typescript"],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
test("stackProfileSchema validates a valid profile", () => {
|
|
48
|
+
const result = stackProfileSchema.safeParse(validProfile);
|
|
49
|
+
expect(result.success).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("stackProfileSchema rejects missing schema field", () => {
|
|
53
|
+
const bad = { ...validProfile, schema: "wrong" };
|
|
54
|
+
const result = stackProfileSchema.safeParse(bad);
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("stackProfileSchema rejects empty detect.anyOf", () => {
|
|
59
|
+
const bad = { ...validProfile, detect: { anyOf: [] } };
|
|
60
|
+
const result = stackProfileSchema.safeParse(bad);
|
|
61
|
+
expect(result.success).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("loadStackProfile parses valid YAML", async () => {
|
|
65
|
+
const yaml = `
|
|
66
|
+
schema: forge/stack-profile@1
|
|
67
|
+
id: my-profile
|
|
68
|
+
displayName: My Profile
|
|
69
|
+
detect:
|
|
70
|
+
anyOf: ["package.json"]
|
|
71
|
+
workspace:
|
|
72
|
+
dirs: ["src"]
|
|
73
|
+
files:
|
|
74
|
+
- path: package.json
|
|
75
|
+
content: "{}"
|
|
76
|
+
install: []
|
|
77
|
+
`;
|
|
78
|
+
const profilePath = join(tempDir, "test.yaml");
|
|
79
|
+
await writeFile(profilePath, yaml, "utf8");
|
|
80
|
+
const profile = loadStackProfile(profilePath);
|
|
81
|
+
expect(profile.id).toBe("my-profile");
|
|
82
|
+
expect(profile.workspace.dirs).toEqual(["src"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("loadStackProfile throws on invalid YAML", async () => {
|
|
86
|
+
const yaml = `schema: forge/stack-profile@1\nid: ""`;
|
|
87
|
+
const profilePath = join(tempDir, "bad.yaml");
|
|
88
|
+
await writeFile(profilePath, yaml, "utf8");
|
|
89
|
+
expect(() => loadStackProfile(profilePath)).toThrow("failed schema validation");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("listStackProfiles loads all YAML files from profiles dir", async () => {
|
|
93
|
+
const profilesDir = join(tempDir, "profiles");
|
|
94
|
+
await mkdir(profilesDir, { recursive: true });
|
|
95
|
+
await writeFile(
|
|
96
|
+
join(profilesDir, "a.yaml"),
|
|
97
|
+
`schema: forge/stack-profile@1\nid: a\ndisplayName: A\ndetect:\n anyOf: ["a.txt"]\nworkspace:\n dirs: ["src"]\n files: []\ninstall: []`,
|
|
98
|
+
"utf8",
|
|
99
|
+
);
|
|
100
|
+
await writeFile(
|
|
101
|
+
join(profilesDir, "b.yaml"),
|
|
102
|
+
`schema: forge/stack-profile@1\nid: b\ndisplayName: B\ndetect:\n anyOf: ["b.txt"]\nworkspace:\n dirs: ["src"]\n files: []\ninstall: []`,
|
|
103
|
+
"utf8",
|
|
104
|
+
);
|
|
105
|
+
const profiles = listStackProfiles(tempDir);
|
|
106
|
+
expect(profiles.length).toBe(2);
|
|
107
|
+
expect(profiles.map((p) => p.id).sort()).toEqual(["a", "b"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("listStackProfiles returns empty array when profiles dir missing", () => {
|
|
111
|
+
const profiles = listStackProfiles(tempDir);
|
|
112
|
+
expect(profiles).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("detectStack matches on anyOf glob", async () => {
|
|
116
|
+
await writeFile(join(tempDir, "astro.config.mjs"), "export default {}", "utf8");
|
|
117
|
+
const profiles: StackProfile[] = [
|
|
118
|
+
{ ...validProfile, id: "astro", detect: { anyOf: ["astro.config.*"] } },
|
|
119
|
+
{ ...validProfile, id: "phaser", detect: { anyOf: ["phaser.config.*"] } },
|
|
120
|
+
];
|
|
121
|
+
const match = detectStack(tempDir, profiles);
|
|
122
|
+
expect(match?.id).toBe("astro");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("detectStack returns null when no pattern matches", async () => {
|
|
126
|
+
const profiles: StackProfile[] = [
|
|
127
|
+
{ ...validProfile, id: "rust", detect: { anyOf: ["Cargo.toml"] } },
|
|
128
|
+
];
|
|
129
|
+
const match = detectStack(tempDir, profiles);
|
|
130
|
+
expect(match).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("detectStack matches on exact filename", async () => {
|
|
134
|
+
await writeFile(join(tempDir, "tsconfig.json"), "{}", "utf8");
|
|
135
|
+
const profiles: StackProfile[] = [
|
|
136
|
+
{ ...validProfile, id: "ts", detect: { anyOf: ["tsconfig.json"] } },
|
|
137
|
+
];
|
|
138
|
+
const match = detectStack(tempDir, profiles);
|
|
139
|
+
expect(match?.id).toBe("ts");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("shipped phaser-turborepo profile validates", () => {
|
|
143
|
+
const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-turborepo.yaml"));
|
|
144
|
+
expect(profile.id).toBe("phaser-turborepo");
|
|
145
|
+
expect(profile.workspace.dirs.length).toBeGreaterThan(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("shipped godot-csharp profile validates", () => {
|
|
149
|
+
const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "godot-csharp.yaml"));
|
|
150
|
+
expect(profile.id).toBe("godot-csharp");
|
|
151
|
+
expect(profile.workspace.dirs.length).toBeGreaterThan(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("listStackProfiles finds all shipped profiles", () => {
|
|
155
|
+
const profiles = listStackProfiles(FORGE_ROOT);
|
|
156
|
+
const ids = profiles.map((p) => p.id).sort();
|
|
157
|
+
expect(ids).toContain("phaser-turborepo");
|
|
158
|
+
expect(ids).toContain("godot-csharp");
|
|
159
|
+
expect(ids).toContain("forge-shell");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("all shipped profiles include @warpgogol/forge in install steps or package.json template", () => {
|
|
163
|
+
const profiles = listStackProfiles(FORGE_ROOT);
|
|
164
|
+
for (const profile of profiles) {
|
|
165
|
+
// knowledge-typescript-turborepo uses @warpgogol/werkstatt-knowledge plugin, not forge
|
|
166
|
+
if (profile.id === "knowledge-typescript-turborepo") continue;
|
|
167
|
+
const hasForgeInInstall = profile.install.some((cmd) => cmd.includes("@warpgogol/forge"));
|
|
168
|
+
const pkgFile = profile.workspace.files.find((f) => f.path === "package.json");
|
|
169
|
+
const hasForgeInPkg = pkgFile?.content.includes("@warpgogol/forge") ?? false;
|
|
170
|
+
expect(hasForgeInInstall || hasForgeInPkg, `profile ${profile.id}`).toBe(true);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("all shipped profiles include operator-profile.md in .gitignore content", () => {
|
|
175
|
+
const profiles = listStackProfiles(FORGE_ROOT);
|
|
176
|
+
for (const profile of profiles) {
|
|
177
|
+
// knowledge-typescript-turborepo uses a different plugin and gitignore template
|
|
178
|
+
if (profile.id === "knowledge-typescript-turborepo") continue;
|
|
179
|
+
const gitignoreFile = profile.workspace.files.find((f) => f.path === ".gitignore");
|
|
180
|
+
expect(gitignoreFile).toBeDefined();
|
|
181
|
+
expect(gitignoreFile?.content).toContain("operator-profile.md");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("stackProfileSchema accepts optional scriptDir field (ADR-0043)", () => {
|
|
186
|
+
const withScriptDir = { ...validProfile, scriptDir: "tools/scripts" };
|
|
187
|
+
const result = stackProfileSchema.safeParse(withScriptDir);
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect((result.data as StackProfile).scriptDir).toBe("tools/scripts");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("stackProfileSchema accepts profile without scriptDir (defaults to scripts/)", () => {
|
|
193
|
+
const result = stackProfileSchema.safeParse(validProfile);
|
|
194
|
+
expect(result.success).toBe(true);
|
|
195
|
+
expect((result.data as StackProfile).scriptDir).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("stackProfileSchema rejects empty scriptDir string", () => {
|
|
199
|
+
const bad = { ...validProfile, scriptDir: "" };
|
|
200
|
+
const result = stackProfileSchema.safeParse(bad);
|
|
201
|
+
expect(result.success).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("RFC-0854: all shipped profiles with package.json template declare engines.node >=24 <25", () => {
|
|
205
|
+
const profiles = listStackProfiles(FORGE_ROOT);
|
|
206
|
+
for (const profile of profiles) {
|
|
207
|
+
const pkgFile = profile.workspace.files.find((f) => f.path === "package.json");
|
|
208
|
+
if (!pkgFile) continue;
|
|
209
|
+
if (!pkgFile.content.includes("engines")) continue;
|
|
210
|
+
expect(pkgFile.content, `profile ${profile.id} package.json`).toContain(">=24 <25");
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("RFC-0854: all shipped profiles with CI workflow declare node-version: 24", () => {
|
|
215
|
+
const profiles = listStackProfiles(FORGE_ROOT);
|
|
216
|
+
for (const profile of profiles) {
|
|
217
|
+
const ciFile = profile.workspace.files.find((f) => f.path === ".github/workflows/ci.yml");
|
|
218
|
+
if (!ciFile) continue;
|
|
219
|
+
expect(ciFile.content, `profile ${profile.id} CI workflow`).toContain('node-version: "24"');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { toKebabCase } from "../utils/string-utils.ts";
|
|
3
|
+
|
|
4
|
+
describe("toKebabCase", () => {
|
|
5
|
+
test("lowercases simple input", () => {
|
|
6
|
+
expect(toKebabCase("Hello")).toBe("hello");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("replaces spaces with hyphens", () => {
|
|
10
|
+
expect(toKebabCase("hello world")).toBe("hello-world");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("replaces non-alphanumeric with hyphens", () => {
|
|
14
|
+
expect(toKebabCase("hello_world")).toBe("hello-world");
|
|
15
|
+
expect(toKebabCase("hello.world")).toBe("hello-world");
|
|
16
|
+
expect(toKebabCase("hello@world!")).toBe("hello-world");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("strips leading/trailing hyphens", () => {
|
|
20
|
+
expect(toKebabCase("--hello--")).toBe("hello");
|
|
21
|
+
expect(toKebabCase(" hello ")).toBe("hello");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("collapses multiple separators", () => {
|
|
25
|
+
expect(toKebabCase("hello world")).toBe("hello-world");
|
|
26
|
+
expect(toKebabCase("hello___world")).toBe("hello-world");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("empty string stays empty", () => {
|
|
30
|
+
expect(toKebabCase("")).toBe("");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("all-symbols string becomes empty", () => {
|
|
34
|
+
expect(toKebabCase("---")).toBe("");
|
|
35
|
+
expect(toKebabCase("@#$%")).toBe("");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("preserves digits", () => {
|
|
39
|
+
expect(toKebabCase("hello123")).toBe("hello123");
|
|
40
|
+
expect(toKebabCase("hello 123 world")).toBe("hello-123-world");
|
|
41
|
+
});
|
|
42
|
+
});
|