@warpgogol/forge 2.7.0 → 2.8.1

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.
Files changed (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +51 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +217 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,524 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for RFC-0674/RFC-0677/RFC-0678/RFC-0679 lifecycle handlers — profile resolution, dry-run, build execution, validate with --artifact and violation parsing, determinism check, asset management.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0674: initial lifecycle handler tests — profile resolution, dry-run, build execution.</item>
7
+ <item>RFC-0677: added tests for --artifact filtering, violation parsing (json/plain), allPassed, empty-state.</item>
8
+ <item>RFC-0678: added tests for determinism check — dry-run, --artifact, no-hashable, cache hit, non-deterministic detection.</item>
9
+ <item>RFC-0679: added tests for asset list/check — dry-run, --type, missing, orphaned, --strict.</item>
10
+ <item>RFC-0680: added tests for release prepare/publish — dry-run, manifest generation, publish dry-run, no-release-config.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { test, expect, beforeEach, afterEach } from "vitest";
15
+ import { join } from "node:path";
16
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../src/types.ts";
19
+ import { resolveActiveProfile } from "./profile-resolve.ts";
20
+ import { runBuild } from "./build.ts";
21
+ import { runValidate, parseViolations } from "./validate.ts";
22
+ import { runDeterminismCheck } from "./determinism-check.ts";
23
+ import { runAssetsList } from "./assets-list.ts";
24
+ import { runAssetsCheck } from "./assets-check.ts";
25
+ import { runReleasePrepare } from "./release-prepare.ts";
26
+ import { runReleasePublish } from "./release-publish.ts";
27
+ import { runDev } from "./dev.ts";
28
+
29
+ const FORGE_ROOT = join(import.meta.dirname, "..", "..", "..");
30
+
31
+ function makeTempWorkspace(profileYaml?: string): string {
32
+ const dir = join(
33
+ tmpdir(),
34
+ `forge-lifecycle-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
35
+ );
36
+ mkdirSync(dir, { recursive: true });
37
+
38
+ if (profileYaml) {
39
+ writeFileSync(join(dir, "forge.yaml"), profileYaml);
40
+ }
41
+
42
+ return dir;
43
+ }
44
+
45
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
46
+ return {
47
+ workspaceRoot,
48
+ logger: {
49
+ section: () => {},
50
+ info: () => {},
51
+ warn: () => {},
52
+ error: () => {},
53
+ success: () => {},
54
+ },
55
+ dryRun: false,
56
+ outputFormat: "pretty",
57
+ forgeRoot: FORGE_ROOT,
58
+ };
59
+ }
60
+
61
+ const input = (flags: Record<string, unknown> = {}): ForgeCommandInput => ({
62
+ argv: [],
63
+ flags: flags as Record<string, boolean | string | string[]>,
64
+ });
65
+
66
+ const godotYaml = `profile: godot-csharp\n`;
67
+
68
+ let tmpDir: string;
69
+
70
+ beforeEach(() => {
71
+ tmpDir = makeTempWorkspace(godotYaml);
72
+ });
73
+
74
+ afterEach(() => {
75
+ rmSync(tmpDir, { recursive: true, force: true });
76
+ });
77
+
78
+ // ── resolveActiveProfile ──────────────────────────────────────────────────
79
+
80
+ test("resolveActiveProfile returns the godot-csharp profile when forge.yaml declares it", () => {
81
+ const resolved = resolveActiveProfile(tmpDir, FORGE_ROOT);
82
+ expect(resolved).not.toBeNull();
83
+ expect(resolved!.profile.id).toBe("godot-csharp");
84
+ });
85
+
86
+ test("resolveActiveProfile returns null when forge.yaml has no profile field", () => {
87
+ const dir = makeTempWorkspace(`project:\n name: test\n`);
88
+ const resolved = resolveActiveProfile(dir, FORGE_ROOT);
89
+ expect(resolved).toBeNull();
90
+ rmSync(dir, { recursive: true, force: true });
91
+ });
92
+
93
+ test("resolveActiveProfile returns null when forge.yaml is missing", () => {
94
+ const dir = makeTempWorkspace();
95
+ const resolved = resolveActiveProfile(dir, FORGE_ROOT);
96
+ expect(resolved).toBeNull();
97
+ });
98
+
99
+ test("resolveActiveProfile uses profileIdOverride when provided", () => {
100
+ const resolved = resolveActiveProfile(tmpDir, FORGE_ROOT, "godot-csharp");
101
+ expect(resolved).not.toBeNull();
102
+ expect(resolved!.profile.id).toBe("godot-csharp");
103
+ });
104
+
105
+ // ── runBuild ───────────────────────────────────────────────────────────────
106
+
107
+ test("runBuild --dry-run does not execute child process and prints resolved commands", async () => {
108
+ const result = await runBuild(input({ "dry-run": true }), makeContext(tmpDir));
109
+ expect(result.exitCode).toBeUndefined();
110
+ expect(result.data?.profileId).toBe("godot-csharp");
111
+ expect(result.data?.artifacts.length).toBeGreaterThan(0);
112
+ const game = result.data?.artifacts.find((a) => a.id === "game");
113
+ expect(game).toBeDefined();
114
+ });
115
+
116
+ test("runBuild returns exit 1 when no active profile found", async () => {
117
+ const dir = makeTempWorkspace();
118
+ const result = await runBuild(input({}), makeContext(dir));
119
+ expect(result.exitCode).toBe(1);
120
+ rmSync(dir, { recursive: true, force: true });
121
+ });
122
+
123
+ test("runBuild --dry-run resolves commands without execution", async () => {
124
+ const result = await runBuild(input({ "dry-run": true }), makeContext(tmpDir));
125
+ expect(result.data?.artifacts.length).toBeGreaterThan(0);
126
+ expect(result.summary).toContain("[dry-run]");
127
+ });
128
+
129
+ // ── runValidate ─────────────────────────────────────────────────────────────
130
+
131
+ test("runValidate --dry-run does not execute child process and prints resolved commands", async () => {
132
+ const result = await runValidate(input({ "dry-run": true }), makeContext(tmpDir));
133
+ expect(result.data?.profileId).toBe("godot-csharp");
134
+ expect(result.data?.artifacts.length).toBeGreaterThan(0);
135
+ const game = result.data?.artifacts.find((a) => a.id === "game");
136
+ expect(game).toBeDefined();
137
+ expect(game!.command).toBe("godot --path . --headless --quit");
138
+ });
139
+
140
+ test("runValidate returns exit 1 when no active profile found", async () => {
141
+ const dir = makeTempWorkspace();
142
+ const result = await runValidate(input({}), makeContext(dir));
143
+ expect(result.exitCode).toBe(1);
144
+ rmSync(dir, { recursive: true, force: true });
145
+ });
146
+
147
+ // ── runValidate RFC-0677: --artifact, violation parsing, allPassed ─────────
148
+
149
+ test("runValidate --artifact game filters to a single artifact in dry-run", async () => {
150
+ const result = await runValidate(
151
+ input({ "dry-run": true, artifact: "game" }),
152
+ makeContext(tmpDir),
153
+ );
154
+ expect(result.data?.artifacts.length).toBe(1);
155
+ expect(result.data?.artifacts[0].id).toBe("game");
156
+ });
157
+
158
+ test("runValidate --artifact unknown returns exit 1 with not-declared message", async () => {
159
+ const result = await runValidate(
160
+ input({ "dry-run": true, artifact: "nonexistent" }),
161
+ makeContext(tmpDir),
162
+ );
163
+ expect(result.exitCode).toBe(1);
164
+ expect(result.summary).toContain("not declared");
165
+ });
166
+
167
+ test("runValidate --dry-run includes passed and violations fields", async () => {
168
+ const result = await runValidate(input({ "dry-run": true }), makeContext(tmpDir));
169
+ expect(result.data?.allPassed).toBe(true);
170
+ const game = result.data?.artifacts.find((a) => a.id === "game");
171
+ expect(game?.passed).toBe(true);
172
+ expect(game?.violations).toEqual([]);
173
+ });
174
+
175
+ test("parseViolations extracts structured violations from JSON output", () => {
176
+ const jsonOutput = JSON.stringify([
177
+ { file: "Main.tscn", line: 12, severity: "error", message: "missing script" },
178
+ { file: "Player.cs", line: 5, severity: "warning", message: "deprecated method" },
179
+ ]);
180
+ const violations = parseViolations(jsonOutput, "json", undefined);
181
+ expect(violations.length).toBe(2);
182
+ expect(violations[0].file).toBe("Main.tscn");
183
+ expect(violations[0].line).toBe(12);
184
+ expect(violations[0].severity).toBe("error");
185
+ expect(violations[1].severity).toBe("warning");
186
+ });
187
+
188
+ test("parseViolations extracts structured violations from plain text with regex", () => {
189
+ const plainOutput = [
190
+ "Scenes/Main.tscn:12: error: scene missing script attribute",
191
+ "Scripts/Player.cs:5: warning: deprecated method call",
192
+ ].join("\n");
193
+ const pattern =
194
+ "^(?<file>[^:]+):(?<line>\\d+):\\s*(?<severity>error|warning):\\s*(?<message>.+)$";
195
+ const violations = parseViolations(plainOutput, "plain", pattern);
196
+ expect(violations.length).toBe(2);
197
+ expect(violations[0].file).toBe("Scenes/Main.tscn");
198
+ expect(violations[0].line).toBe(12);
199
+ expect(violations[0].severity).toBe("error");
200
+ expect(violations[0].message).toContain("missing script");
201
+ expect(violations[1].severity).toBe("warning");
202
+ });
203
+
204
+ test("parseViolations returns empty array when outputFormat is not set", () => {
205
+ const violations = parseViolations("some output", undefined, undefined);
206
+ expect(violations).toEqual([]);
207
+ });
208
+
209
+ test("parseViolations returns empty array on malformed JSON", () => {
210
+ const violations = parseViolations("not json", "json", undefined);
211
+ expect(violations).toEqual([]);
212
+ });
213
+
214
+ test("parseViolations returns empty array on malformed regex pattern", () => {
215
+ const violations = parseViolations("some output", "plain", "[invalid(");
216
+ expect(violations).toEqual([]);
217
+ });
218
+
219
+ // ── runDev ──────────────────────────────────────────────────────────────────
220
+
221
+ test("runDev --dry-run does not spawn child process and prints resolved command", async () => {
222
+ const result = await runDev(input({ "dry-run": true }), makeContext(tmpDir));
223
+ expect(result.data?.profileId).toBe("godot-csharp");
224
+ expect(result.data?.devServerCommand).toBe("godot --path . --editor");
225
+ expect(result.data?.port).toBe(6005);
226
+ expect(result.data?.exitCode).toBe(0);
227
+ });
228
+
229
+ test("runDev returns exit 1 when profile has no devServer", async () => {
230
+ // forge-shell profile has no devServer
231
+ const dir = makeTempWorkspace(`profile: forge-shell\n`);
232
+ const result = await runDev(input({}), makeContext(dir));
233
+ expect(result.exitCode).toBe(1);
234
+ expect(result.summary).toContain("does not declare a devServer");
235
+ rmSync(dir, { recursive: true, force: true });
236
+ });
237
+
238
+ test("runDev returns exit 1 when no active profile found", async () => {
239
+ const dir = makeTempWorkspace();
240
+ const result = await runDev(input({}), makeContext(dir));
241
+ expect(result.exitCode).toBe(1);
242
+ rmSync(dir, { recursive: true, force: true });
243
+ });
244
+
245
+ // ── runDeterminismCheck RFC-0678 ─────────────────────────────────────────────
246
+
247
+ test("runDeterminismCheck --dry-run prints resolved inputs without executing builds", async () => {
248
+ const result = await runDeterminismCheck(input({ "dry-run": true }), makeContext(tmpDir));
249
+ expect(result.data?.profileId).toBe("godot-csharp");
250
+ expect(result.data?.artifacts.length).toBe(1);
251
+ expect(result.data?.artifacts[0].artifactId).toBe("game");
252
+ expect(result.data?.artifacts[0].inputs).toContain("Scenes/**/*.tscn");
253
+ expect(result.data?.artifacts[0].inputs).toContain("project.godot");
254
+ expect(result.summary).toContain("[dry-run]");
255
+ });
256
+
257
+ test("runDeterminismCheck --artifact game filters to a single artifact", async () => {
258
+ const result = await runDeterminismCheck(
259
+ input({ "dry-run": true, artifact: "game" }),
260
+ makeContext(tmpDir),
261
+ );
262
+ expect(result.data?.artifacts.length).toBe(1);
263
+ expect(result.data?.artifacts[0].artifactId).toBe("game");
264
+ });
265
+
266
+ test("runDeterminismCheck --artifact unknown returns exit 1", async () => {
267
+ const result = await runDeterminismCheck(
268
+ input({ "dry-run": true, artifact: "nonexistent" }),
269
+ makeContext(tmpDir),
270
+ );
271
+ expect(result.exitCode).toBe(1);
272
+ expect(result.summary).toContain("not declared");
273
+ });
274
+
275
+ test("runDeterminismCheck returns exit 1 when no active profile found", async () => {
276
+ const dir = makeTempWorkspace();
277
+ const result = await runDeterminismCheck(input({}), makeContext(dir));
278
+ expect(result.exitCode).toBe(1);
279
+ rmSync(dir, { recursive: true, force: true });
280
+ });
281
+
282
+ test("runDeterminismCheck returns exit 0 when profile has no hashable artifacts", async () => {
283
+ // Create a custom profile with an artifact that has no determinism field
284
+ // We can't easily do this without modifying forge profiles, so instead
285
+ // test with a profile that has no artifacts at all — the handler returns
286
+ // exit 1 for "no artifacts" and exit 0 for "no hashable artifacts".
287
+ // Since forge-shell has no artifacts, we test the no-artifacts path.
288
+ const dir = makeTempWorkspace(`profile: forge-shell\n`);
289
+ const result = await runDeterminismCheck(input({}), makeContext(dir));
290
+ expect(result.exitCode).toBe(1);
291
+ expect(result.summary).toContain("does not declare any artifacts");
292
+ rmSync(dir, { recursive: true, force: true });
293
+ });
294
+
295
+ test("runDeterminismCheck cache hit skips double-build", async () => {
296
+ // Create input files matching the glob patterns
297
+ mkdirSync(join(tmpDir, "Scenes"), { recursive: true });
298
+ writeFileSync(join(tmpDir, "Scenes", "Main.tscn"), "[gd_scene]");
299
+ mkdirSync(join(tmpDir, "Scripts"), { recursive: true });
300
+ writeFileSync(join(tmpDir, "Scripts", "Main.cs"), "using Godot;");
301
+ writeFileSync(join(tmpDir, "project.godot"), "[application]\n");
302
+
303
+ // Create the output file that the produce command would create
304
+ mkdirSync(join(tmpDir, "bin"), { recursive: true });
305
+ writeFileSync(join(tmpDir, "bin", "Debug"), "fake-build-content");
306
+
307
+ // First run: the produce command "dotnet build" will fail, but the input hash
308
+ // is computed before the build. We can read it from the result.
309
+ const result1 = await runDeterminismCheck(input({}), makeContext(tmpDir));
310
+ expect(result1.exitCode).toBe(1); // build fails
311
+ const inputHash = result1.data?.artifacts[0].inputHash;
312
+ expect(inputHash).toBeTruthy();
313
+
314
+ // Write a cache entry with the correct input hash
315
+ const cachePath = join(tmpDir, "dist", ".determinism-cache.json");
316
+ mkdirSync(join(tmpDir, "dist"), { recursive: true });
317
+ const cache = {
318
+ entries: {
319
+ [`game:${inputHash}:dotnet build`]: {
320
+ inputHash,
321
+ produceCommand: "dotnet build",
322
+ outputHash: "sha256:cached-hash",
323
+ deterministic: true,
324
+ },
325
+ },
326
+ };
327
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2) + "\n");
328
+
329
+ // Second run: should hit the cache and skip the build
330
+ const result2 = await runDeterminismCheck(input({}), makeContext(tmpDir));
331
+ expect(result2.exitCode).toBe(0); // cached as deterministic
332
+ expect(result2.data?.artifacts[0].cached).toBe(true);
333
+ expect(result2.data?.artifacts[0].deterministic).toBe(true);
334
+ expect(result2.data?.artifacts[0].firstBuildHash).toBe("sha256:cached-hash");
335
+ }, 15000);
336
+
337
+ // ── runAssetsList RFC-0679 ───────────────────────────────────────────────────
338
+
339
+ test("runAssetsList --dry-run lists assets without hashing", async () => {
340
+ // Create asset files
341
+ mkdirSync(join(tmpDir, "Assets", "images"), { recursive: true });
342
+ writeFileSync(join(tmpDir, "Assets", "images", "bg.png"), "fake-png");
343
+ mkdirSync(join(tmpDir, "Assets", "audio"), { recursive: true });
344
+ writeFileSync(join(tmpDir, "Assets", "audio", "narration.wav"), "fake-wav");
345
+
346
+ const result = await runAssetsList(input({ "dry-run": true }), makeContext(tmpDir));
347
+ expect(result.data?.profileId).toBe("godot-csharp");
348
+ expect(result.data?.assets.length).toBe(2);
349
+ const image = result.data?.assets.find((a) => a.type === "image");
350
+ expect(image).toBeDefined();
351
+ expect(image!.path).toBe("Assets/images/bg.png");
352
+ expect(image!.hash).toBe(""); // dry-run skips hashing
353
+ expect(image!.size).toBe(0);
354
+ });
355
+
356
+ test("runAssetsList --type image filters by type", async () => {
357
+ mkdirSync(join(tmpDir, "Assets", "images"), { recursive: true });
358
+ writeFileSync(join(tmpDir, "Assets", "images", "bg.png"), "fake-png");
359
+ mkdirSync(join(tmpDir, "Assets", "audio"), { recursive: true });
360
+ writeFileSync(join(tmpDir, "Assets", "audio", "narration.wav"), "fake-wav");
361
+
362
+ const result = await runAssetsList(
363
+ input({ "dry-run": true, type: "image" }),
364
+ makeContext(tmpDir),
365
+ );
366
+ expect(result.data?.assets.length).toBe(1);
367
+ expect(result.data?.assets[0].type).toBe("image");
368
+ });
369
+
370
+ test("runAssetsList returns exit 1 when no active profile found", async () => {
371
+ const dir = makeTempWorkspace();
372
+ const result = await runAssetsList(input({}), makeContext(dir));
373
+ expect(result.exitCode).toBe(1);
374
+ rmSync(dir, { recursive: true, force: true });
375
+ });
376
+
377
+ test("runAssetsList returns exit 0 when profile has no assets declaration", async () => {
378
+ const dir = makeTempWorkspace(`profile: forge-shell\n`);
379
+ const result = await runAssetsList(input({}), makeContext(dir));
380
+ expect(result.exitCode).toBe(0);
381
+ expect(result.summary).toContain("does not declare assets");
382
+ rmSync(dir, { recursive: true, force: true });
383
+ });
384
+
385
+ // ── runAssetsCheck RFC-0679 ──────────────────────────────────────────────────
386
+
387
+ test("runAssetsCheck detects missing assets referenced by scenes", async () => {
388
+ // Create a scene that references a non-existent asset
389
+ mkdirSync(join(tmpDir, "Scenes"), { recursive: true });
390
+ writeFileSync(join(tmpDir, "Scenes", "Main.tscn"), '"res://Assets/images/missing.png"');
391
+
392
+ const result = await runAssetsCheck(input({ "dry-run": true }), makeContext(tmpDir));
393
+ expect(result.exitCode).toBe(1);
394
+ expect(result.data?.check.missing.length).toBeGreaterThan(0);
395
+ const missing = result.data?.check.missing.find((m) => m.path.includes("missing.png"));
396
+ expect(missing).toBeDefined();
397
+ expect(missing!.referencedBy).toContain("Scenes/Main.tscn");
398
+ });
399
+
400
+ test("runAssetsCheck detects orphaned assets not referenced by any scene", async () => {
401
+ // Create an asset with no scene referencing it
402
+ mkdirSync(join(tmpDir, "Assets", "audio"), { recursive: true });
403
+ writeFileSync(join(tmpDir, "Assets", "audio", "unused.wav"), "fake-wav");
404
+
405
+ const result = await runAssetsCheck(input({ "dry-run": true }), makeContext(tmpDir));
406
+ // Without --strict, orphaned assets are warnings (exit 0)
407
+ expect(result.data?.check.orphaned.length).toBeGreaterThan(0);
408
+ const orphaned = result.data?.check.orphaned.find((o) => o.path.includes("unused.wav"));
409
+ expect(orphaned).toBeDefined();
410
+ expect(orphaned!.type).toBe("audio");
411
+ });
412
+
413
+ test("runAssetsCheck --strict exits non-zero when orphaned assets are found", async () => {
414
+ mkdirSync(join(tmpDir, "Assets", "audio"), { recursive: true });
415
+ writeFileSync(join(tmpDir, "Assets", "audio", "unused.wav"), "fake-wav");
416
+
417
+ const result = await runAssetsCheck(
418
+ input({ "dry-run": true, strict: true }),
419
+ makeContext(tmpDir),
420
+ );
421
+ expect(result.exitCode).toBe(1);
422
+ expect(result.data?.check.orphaned.length).toBeGreaterThan(0);
423
+ });
424
+
425
+ test("runAssetsCheck --dry-run only checks file existence", async () => {
426
+ mkdirSync(join(tmpDir, "Assets", "images"), { recursive: true });
427
+ writeFileSync(join(tmpDir, "Assets", "images", "bg.png"), "fake-png");
428
+ mkdirSync(join(tmpDir, "Scenes"), { recursive: true });
429
+ writeFileSync(join(tmpDir, "Scenes", "Main.tscn"), '"Assets/images/bg.png"');
430
+
431
+ const result = await runAssetsCheck(input({ "dry-run": true }), makeContext(tmpDir));
432
+ expect(result.exitCode).toBe(0);
433
+ expect(result.data?.check.missing).toEqual([]);
434
+ expect(result.data?.check.orphaned).toEqual([]);
435
+ expect(result.data?.allOk).toBe(true);
436
+ });
437
+
438
+ test("runAssetsCheck returns exit 1 when no active profile found", async () => {
439
+ const dir = makeTempWorkspace();
440
+ const result = await runAssetsCheck(input({}), makeContext(dir));
441
+ expect(result.exitCode).toBe(1);
442
+ rmSync(dir, { recursive: true, force: true });
443
+ });
444
+
445
+ // ── runReleasePrepare RFC-0680 ───────────────────────────────────────────────
446
+
447
+ test("runReleasePrepare --dry-run prints resolved release steps", async () => {
448
+ // Create a built artifact at the produce.output path
449
+ mkdirSync(join(tmpDir, "bin"), { recursive: true });
450
+ writeFileSync(join(tmpDir, "bin", "Debug"), "fake-build-content");
451
+
452
+ const result = await runReleasePrepare(input({ "dry-run": true }), makeContext(tmpDir));
453
+ expect(result.data?.profileId).toBe("godot-csharp");
454
+ expect(result.data?.manifest.schemaVersion).toBe("1");
455
+ expect(result.data?.manifest.artifacts.length).toBeGreaterThan(0);
456
+ expect(result.data?.manifest.artifacts[0].artifactId).toBe("game");
457
+ expect(result.data?.manifest.determinismChecked).toBe(false);
458
+ expect(result.data?.manifest.validationPassed).toBe(true);
459
+ });
460
+
461
+ test("runReleasePrepare generates manifest with artifact hashes", async () => {
462
+ mkdirSync(join(tmpDir, "bin"), { recursive: true });
463
+ writeFileSync(join(tmpDir, "bin", "Debug"), "fake-build-content");
464
+
465
+ const result = await runReleasePrepare(input({}), makeContext(tmpDir));
466
+ expect(result.exitCode ?? 0).toBe(0);
467
+ expect(result.data?.manifest.artifacts.length).toBeGreaterThan(0);
468
+ expect(result.data?.manifest.artifacts[0].hash).toBeTruthy();
469
+ expect(result.data?.manifest.artifacts[0].size).toBeGreaterThan(0);
470
+ expect(result.data?.manifest.releaseId).toContain("godot-csharp-");
471
+ expect(result.data?.manifest.schemaVersion).toBe("1");
472
+ });
473
+
474
+ test("runReleasePrepare returns exit 1 when no release config", async () => {
475
+ const dir = makeTempWorkspace(`profile: forge-shell\n`);
476
+ const result = await runReleasePrepare(input({}), makeContext(dir));
477
+ expect(result.exitCode).toBe(1);
478
+ expect(result.summary).toContain("does not declare a release");
479
+ rmSync(dir, { recursive: true, force: true });
480
+ });
481
+
482
+ test("runReleasePrepare returns exit 1 when no active profile", async () => {
483
+ const dir = makeTempWorkspace();
484
+ const result = await runReleasePrepare(input({}), makeContext(dir));
485
+ expect(result.exitCode).toBe(1);
486
+ rmSync(dir, { recursive: true, force: true });
487
+ });
488
+
489
+ test("runReleasePrepare returns exit 1 when build output not found", async () => {
490
+ // No build/ directory created
491
+ const result = await runReleasePrepare(input({}), makeContext(tmpDir));
492
+ expect(result.exitCode).toBe(1);
493
+ expect(result.summary).toContain("no built output");
494
+ });
495
+
496
+ // ── runReleasePublish RFC-0680 ───────────────────────────────────────────────
497
+
498
+ test("runReleasePublish --dry-run does not upload anything", async () => {
499
+ // Prepare a release first
500
+ mkdirSync(join(tmpDir, "bin"), { recursive: true });
501
+ writeFileSync(join(tmpDir, "bin", "Debug"), "fake-build-content");
502
+ await runReleasePrepare(input({}), makeContext(tmpDir));
503
+
504
+ const result = await runReleasePublish(input({ "dry-run": true }), makeContext(tmpDir));
505
+ expect(result.exitCode ?? 0).toBe(0);
506
+ expect(result.data?.target).toBe("local");
507
+ expect(result.data?.publishedFiles.length).toBeGreaterThan(0);
508
+ // Should not have created a published/ directory
509
+ expect(result.summary).toContain("[dry-run]");
510
+ });
511
+
512
+ test("runReleasePublish returns exit 1 when no manifest found", async () => {
513
+ // No release prepared — release dir doesn't exist
514
+ const result = await runReleasePublish(input({}), makeContext(tmpDir));
515
+ expect(result.exitCode).toBe(1);
516
+ expect(result.summary).toContain("No release manifest found");
517
+ });
518
+
519
+ test("runReleasePublish returns exit 1 when no active profile", async () => {
520
+ const dir = makeTempWorkspace();
521
+ const result = await runReleasePublish(input({}), makeContext(dir));
522
+ expect(result.exitCode).toBe(1);
523
+ rmSync(dir, { recursive: true, force: true });
524
+ });
@@ -0,0 +1,93 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for note.frontmatter.validate command handler (RFC-0808).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0808: initial note.frontmatter.validate tests.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
14
+ import { runNoteFrontmatterValidate } from "../../../src/validators/note-frontmatter-validate.ts";
15
+
16
+ function createTempDir(): string {
17
+ return fs.mkdtempSync(path.join(os.tmpdir(), "forge-note-frontmatter-test-"));
18
+ }
19
+
20
+ function writeFile(dir: string, relPath: string, content: string): void {
21
+ const fullPath = path.join(dir, relPath);
22
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
23
+ fs.writeFileSync(fullPath, content);
24
+ }
25
+
26
+ describe("note.frontmatter.validate", () => {
27
+ let tempDir: string;
28
+
29
+ beforeEach(() => {
30
+ tempDir = createTempDir();
31
+ });
32
+
33
+ afterEach(() => {
34
+ fs.rmSync(tempDir, { recursive: true, force: true });
35
+ });
36
+
37
+ it("detects missing title field", () => {
38
+ writeFile(tempDir, "vault/note-1.md", "---\ntags: [a]\n---\nNo title here.");
39
+ writeFile(tempDir, "vault/note-2.md", "---\ntitle: Note 2\n---\nHas title.");
40
+
41
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
42
+
43
+ expect(result.data!.violations).toHaveLength(1);
44
+ expect(result.data!.violations[0].rule).toBe("NOTE-02");
45
+ expect(result.data!.violations[0].file).toBe("vault/note-1.md");
46
+ expect(result.data!.violations[0].field).toBe("title");
47
+ expect(result.exitCode).toBe(1);
48
+ });
49
+
50
+ it("falls back to first H1 for title", () => {
51
+ writeFile(tempDir, "vault/note-1.md", "---\ntags: [a]\n---\n# My Title\nContent.");
52
+
53
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
54
+
55
+ expect(result.data!.violations).toHaveLength(0);
56
+ expect(result.exitCode).toBe(0);
57
+ });
58
+
59
+ it("flags files without frontmatter", () => {
60
+ writeFile(tempDir, "vault/note-1.md", "No frontmatter here.");
61
+
62
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
63
+
64
+ expect(result.data!.violations).toHaveLength(1);
65
+ expect(result.data!.violations[0].message).toContain("no frontmatter");
66
+ expect(result.exitCode).toBe(1);
67
+ });
68
+
69
+ it("passes when title is present", () => {
70
+ writeFile(tempDir, "vault/note-1.md", "---\ntitle: Note 1\n---\nContent.");
71
+
72
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
73
+
74
+ expect(result.data!.violations).toHaveLength(0);
75
+ expect(result.exitCode).toBe(0);
76
+ });
77
+
78
+ it("handles empty vault", () => {
79
+ fs.mkdirSync(path.join(tempDir, "vault"), { recursive: true });
80
+
81
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
82
+
83
+ expect(result.data!.violations).toHaveLength(0);
84
+ expect(result.exitCode).toBe(0);
85
+ });
86
+
87
+ it("handles missing vault directory", () => {
88
+ const result = runNoteFrontmatterValidate({ flags: {} }, { workspaceRoot: tempDir });
89
+
90
+ expect(result.data!.violations).toHaveLength(0);
91
+ expect(result.exitCode).toBe(0);
92
+ });
93
+ });