@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.
Files changed (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -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 +54 -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 +221 -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/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,192 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for memory-scaffold.ts (RFC-0664) — scaffoldMemoryLayer and checkMemoryLayerHealth.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0664: initial memory scaffold and health check tests.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, rm, writeFile, readFile, mkdir } from "node:fs/promises";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import {
16
+ scaffoldMemoryLayer,
17
+ checkMemoryLayerHealth,
18
+ DEFAULT_MEMORY_BUDGET,
19
+ MEMORY_GITIGNORE_START,
20
+ MEMORY_GITIGNORE_END,
21
+ } from "../onboarding/memory-scaffold.ts";
22
+
23
+ let tempDir: string;
24
+
25
+ beforeEach(async () => {
26
+ tempDir = await mkdtemp(join(tmpdir(), "forge-memory-scaffold-test-"));
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await rm(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ test("scaffoldMemoryLayer creates MEMORY.md, daily/.gitkeep, and .gitignore block", () => {
34
+ const result = scaffoldMemoryLayer(tempDir);
35
+
36
+ expect(result.created).toContain(".agents/memory/MEMORY.md");
37
+ expect(result.created).toContain(".agents/memory/daily/.gitkeep");
38
+ expect(result.gitignoreUpdated).toBe(true);
39
+ expect(result.skipped).toEqual([]);
40
+
41
+ expect(existsSync(join(tempDir, ".agents", "memory", "MEMORY.md"))).toBe(true);
42
+ expect(existsSync(join(tempDir, ".agents", "memory", "daily", ".gitkeep"))).toBe(true);
43
+
44
+ const gitignore = readFileSync(join(tempDir, ".gitignore"), "utf8");
45
+ expect(gitignore).toContain(MEMORY_GITIGNORE_START);
46
+ expect(gitignore).toContain(MEMORY_GITIGNORE_END);
47
+ expect(gitignore).toContain(".agents/memory/daily/");
48
+ });
49
+
50
+ test("scaffoldMemoryLayer is idempotent — second run skips existing files", () => {
51
+ scaffoldMemoryLayer(tempDir);
52
+ const result = scaffoldMemoryLayer(tempDir);
53
+
54
+ expect(result.created).toEqual([]);
55
+ expect(result.gitignoreUpdated).toBe(false);
56
+ expect(result.skipped).toContain("MEMORY.md");
57
+ expect(result.skipped).toContain("daily/.gitkeep");
58
+ });
59
+
60
+ test("scaffoldMemoryLayer appends gitignore block to existing .gitignore without clobbering", async () => {
61
+ await writeFile(join(tempDir, ".gitignore"), "node_modules/\ndist/\n", "utf8");
62
+
63
+ scaffoldMemoryLayer(tempDir);
64
+
65
+ const gitignore = await readFile(join(tempDir, ".gitignore"), "utf8");
66
+ expect(gitignore).toContain("node_modules/");
67
+ expect(gitignore).toContain("dist/");
68
+ expect(gitignore).toContain(MEMORY_GITIGNORE_START);
69
+ expect(gitignore).toContain(".agents/memory/daily/");
70
+ expect(gitignore).toContain(MEMORY_GITIGNORE_END);
71
+ });
72
+
73
+ test("scaffoldMemoryLayer does not duplicate gitignore block on re-run", async () => {
74
+ await writeFile(join(tempDir, ".gitignore"), "node_modules/\n", "utf8");
75
+
76
+ scaffoldMemoryLayer(tempDir);
77
+ scaffoldMemoryLayer(tempDir);
78
+
79
+ const gitignore = await readFile(join(tempDir, ".gitignore"), "utf8");
80
+ const blockCount = (gitignore.match(new RegExp(MEMORY_GITIGNORE_START, "g")) || []).length;
81
+ expect(blockCount).toBe(1);
82
+ });
83
+
84
+ test("checkMemoryLayerHealth reports pass for healthy layer", () => {
85
+ scaffoldMemoryLayer(tempDir);
86
+
87
+ const health = checkMemoryLayerHealth(tempDir);
88
+
89
+ expect(health.memoryMdExists).toBe(true);
90
+ expect(health.gitignoreCoversDaily).toBe(true);
91
+ expect(health.dailyFileCount).toBe(0);
92
+ expect(health.budget).toBe(DEFAULT_MEMORY_BUDGET);
93
+ expect(health.memoryMdChars).toBeGreaterThan(0);
94
+ });
95
+
96
+ test("checkMemoryLayerHealth reports missing MEMORY.md", () => {
97
+ const health = checkMemoryLayerHealth(tempDir);
98
+
99
+ expect(health.memoryMdExists).toBe(false);
100
+ expect(health.memoryMdChars).toBe(0);
101
+ });
102
+
103
+ test("checkMemoryLayerHealth reports budget from forge.yaml when configured", async () => {
104
+ scaffoldMemoryLayer(tempDir);
105
+
106
+ // Write a minimal forge.yaml with memory budget override
107
+ const forgeYaml = `
108
+ schema: forge/config@1
109
+ project:
110
+ name: test-project
111
+ stack: []
112
+ packageManager: pnpm
113
+ paths:
114
+ rfcsDir: docs/rfcs
115
+ adrsDir: docs/adrs
116
+ plansDir: docs/plans
117
+ auditsDir: docs/audits
118
+ specsDir: docs/specs
119
+ skillsDir: .agents/skills
120
+ sessionsDir: docs/sessions
121
+ bindings:
122
+ schema: forge/bindings@1
123
+ commands:
124
+ validateRfc: null
125
+ validateAdr: null
126
+ implementStamp: null
127
+ typecheck: null
128
+ test: null
129
+ scopedBuild: null
130
+ specValidate: null
131
+ sessionSave: null
132
+ validate: null
133
+ produce: null
134
+ verify: null
135
+ preview: null
136
+ lint: null
137
+ paths:
138
+ invariantsFile: null
139
+ compassDocs: []
140
+ reviewsDir: null
141
+ handoffsDir: null
142
+ sessionsDir: null
143
+ terminology: {}
144
+ memory:
145
+ budget: 8192
146
+ `;
147
+ await mkdir(join(tempDir, "packages", "forge"), { recursive: true });
148
+ await writeFile(
149
+ join(tempDir, "packages", "forge", "package.json"),
150
+ JSON.stringify({ name: "@warpgogol/forge", version: "0.4.0" }),
151
+ "utf8",
152
+ );
153
+ await writeFile(join(tempDir, "forge.yaml"), forgeYaml, "utf8");
154
+
155
+ const health = checkMemoryLayerHealth(tempDir);
156
+
157
+ expect(health.budget).toBe(8192);
158
+ });
159
+
160
+ test("checkMemoryLayerHealth reports daily file count excluding .gitkeep", async () => {
161
+ scaffoldMemoryLayer(tempDir);
162
+
163
+ // Create a daily log file
164
+ await writeFile(
165
+ join(tempDir, ".agents", "memory", "daily", "2026-08-03.md"),
166
+ "- [10:00] test entry\n",
167
+ "utf8",
168
+ );
169
+
170
+ const health = checkMemoryLayerHealth(tempDir);
171
+
172
+ expect(health.dailyFileCount).toBe(1);
173
+ });
174
+
175
+ test("checkMemoryLayerHealth reports gitignore not covering daily when block absent", async () => {
176
+ scaffoldMemoryLayer(tempDir);
177
+
178
+ // Remove the gitignore block
179
+ await writeFile(join(tempDir, ".gitignore"), "node_modules/\n", "utf8");
180
+
181
+ const health = checkMemoryLayerHealth(tempDir);
182
+
183
+ expect(health.gitignoreCoversDaily).toBe(false);
184
+ });
185
+
186
+ test("checkMemoryLayerHealth uses default budget when forge.yaml absent", () => {
187
+ scaffoldMemoryLayer(tempDir);
188
+
189
+ const health = checkMemoryLayerHealth(tempDir);
190
+
191
+ expect(health.budget).toBe(DEFAULT_MEMORY_BUDGET);
192
+ });
@@ -0,0 +1,480 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for migration-adapter module — adapter detect/analyze/migrate, registry discovery, forge-protected file enforcement (RFC-0546).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0546: initial migration-adapter 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 { existsSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import {
16
+ nodeTypescriptPnpmAdapter,
17
+ phaserPnpmAdapter,
18
+ getAdapters,
19
+ detectAdapter,
20
+ detectAdapters,
21
+ FORGE_PROTECTED_PATHS,
22
+ DEFAULT_EXCLUDE_PATTERNS,
23
+ discoverIgnoredFiles,
24
+ formatSize,
25
+ } from "../migration-adapters/index.ts";
26
+
27
+ let tempDir: string;
28
+
29
+ beforeEach(async () => {
30
+ tempDir = await mkdtemp(join(tmpdir(), "forge-migration-test-"));
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await rm(tempDir, { recursive: true, force: true });
35
+ });
36
+
37
+ test("FORGE_PROTECTED_PATHS includes forge.yaml, .agents, docs/rfcs, docs/adrs, PREFERENCES.md", () => {
38
+ expect(FORGE_PROTECTED_PATHS).toContain("forge.yaml");
39
+ expect(FORGE_PROTECTED_PATHS).toContain(".agents");
40
+ expect(FORGE_PROTECTED_PATHS).toContain("docs/rfcs");
41
+ expect(FORGE_PROTECTED_PATHS).toContain("docs/adrs");
42
+ expect(FORGE_PROTECTED_PATHS).toContain("PREFERENCES.md");
43
+ });
44
+
45
+ test("DEFAULT_EXCLUDE_PATTERNS includes node_modules, dist, .next, .cache, .turbo, .git", () => {
46
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain("node_modules");
47
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain("dist");
48
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".next");
49
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".cache");
50
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".turbo");
51
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".git");
52
+ });
53
+
54
+ test("node-typescript-pnpm adapter detects Node+TS+pnpm project", async () => {
55
+ const sourceDir = join(tempDir, "source");
56
+ await mkdir(sourceDir, { recursive: true });
57
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
58
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
59
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
60
+
61
+ expect(nodeTypescriptPnpmAdapter.detect(sourceDir)).toBe(true);
62
+ });
63
+
64
+ test("node-typescript-pnpm adapter does not detect project without pnpm lockfile", async () => {
65
+ const sourceDir = join(tempDir, "source");
66
+ await mkdir(sourceDir, { recursive: true });
67
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
68
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
69
+
70
+ expect(nodeTypescriptPnpmAdapter.detect(sourceDir)).toBe(false);
71
+ });
72
+
73
+ test("node-typescript-pnpm adapter analyzes package.json and derives bindings", async () => {
74
+ const sourceDir = join(tempDir, "source");
75
+ await mkdir(sourceDir, { recursive: true });
76
+ await writeFile(
77
+ join(sourceDir, "package.json"),
78
+ JSON.stringify({
79
+ name: "@org/my-app",
80
+ scripts: {
81
+ "build:check": "tsc --noEmit",
82
+ test: "vitest",
83
+ build: "turbo run build",
84
+ },
85
+ }),
86
+ );
87
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
88
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
89
+
90
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
91
+ expect(analysis.stack).toContain("typescript");
92
+ expect(analysis.packageManager).toBe("pnpm");
93
+ expect(analysis.bindings.typecheck).toBe("tsc --noEmit");
94
+ expect(analysis.bindings.test).toBe("vitest");
95
+ expect(analysis.bindings.scopedBuild).toBe("turbo run build");
96
+ expect(analysis.placement).toBe("apps");
97
+ expect(analysis.appName).toBe("my-app");
98
+ });
99
+
100
+ test("node-typescript-pnpm adapter derives appName from directory when package.json name is absent", async () => {
101
+ const sourceDir = join(tempDir, "MyCoolApp");
102
+ await mkdir(sourceDir, { recursive: true });
103
+ await writeFile(join(sourceDir, "package.json"), "{}");
104
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
105
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
106
+
107
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
108
+ expect(analysis.appName).toBe("mycoolapp");
109
+ });
110
+
111
+ test("phaser-pnpm adapter detects project with phaser dependency", async () => {
112
+ const sourceDir = join(tempDir, "source");
113
+ await mkdir(sourceDir, { recursive: true });
114
+ await writeFile(
115
+ join(sourceDir, "package.json"),
116
+ JSON.stringify({
117
+ name: "phaser-game",
118
+ dependencies: { phaser: "^3.80.0" },
119
+ }),
120
+ );
121
+
122
+ expect(phaserPnpmAdapter.detect(sourceDir)).toBe(true);
123
+ });
124
+
125
+ test("phaser-pnpm adapter does not detect project without phaser dependency", async () => {
126
+ const sourceDir = join(tempDir, "source");
127
+ await mkdir(sourceDir, { recursive: true });
128
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "regular-app" }));
129
+
130
+ expect(phaserPnpmAdapter.detect(sourceDir)).toBe(false);
131
+ });
132
+
133
+ test("registry getAdapters returns both built-in adapters", () => {
134
+ const adapters = getAdapters();
135
+ expect(adapters).toHaveLength(2);
136
+ expect(adapters.map((a) => a.id)).toContain("node-typescript-pnpm");
137
+ expect(adapters.map((a) => a.id)).toContain("phaser-pnpm");
138
+ });
139
+
140
+ test("registry detectAdapter returns matching adapter for Node+TS+pnpm source", async () => {
141
+ const sourceDir = join(tempDir, "source");
142
+ await mkdir(sourceDir, { recursive: true });
143
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test" }));
144
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
145
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
146
+
147
+ const adapter = detectAdapter(sourceDir);
148
+ expect(adapter).not.toBeNull();
149
+ expect(adapter?.id).toBe("node-typescript-pnpm");
150
+ });
151
+
152
+ test("registry detectAdapter returns null for unrecognized source", async () => {
153
+ const sourceDir = join(tempDir, "source");
154
+ await mkdir(sourceDir, { recursive: true });
155
+ await writeFile(join(sourceDir, "README.md"), "not a project");
156
+
157
+ const adapter = detectAdapter(sourceDir);
158
+ expect(adapter).toBeNull();
159
+ });
160
+
161
+ test("registry detectAdapters returns all matching adapters", async () => {
162
+ const sourceDir = join(tempDir, "source");
163
+ await mkdir(sourceDir, { recursive: true });
164
+ await writeFile(
165
+ join(sourceDir, "package.json"),
166
+ JSON.stringify({
167
+ name: "phaser-ts-app",
168
+ dependencies: { phaser: "^3.80.0" },
169
+ scripts: { "build:check": "tsc --noEmit" },
170
+ }),
171
+ );
172
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
173
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
174
+
175
+ const matches = detectAdapters(sourceDir);
176
+ expect(matches.length).toBeGreaterThanOrEqual(1);
177
+ expect(matches.map((a) => a.id)).toContain("phaser-pnpm");
178
+ });
179
+
180
+ test("migrate copies source files to apps/<appName>/ and excludes node_modules", async () => {
181
+ const sourceDir = join(tempDir, "source");
182
+ const targetDir = join(tempDir, "target");
183
+
184
+ await mkdir(sourceDir, { recursive: true });
185
+ await mkdir(targetDir, { recursive: true });
186
+ await mkdir(join(sourceDir, "node_modules"), { recursive: true });
187
+ await mkdir(join(sourceDir, "src"), { recursive: true });
188
+
189
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
190
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
191
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
192
+ await writeFile(join(sourceDir, "src", "index.ts"), "console.log('hello');");
193
+ await writeFile(join(sourceDir, "node_modules", "evil.js"), "malware");
194
+
195
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
196
+ const result = nodeTypescriptPnpmAdapter.migrate(sourceDir, targetDir, analysis);
197
+
198
+ expect(result.filesCopied).toContain("package.json");
199
+ expect(result.filesCopied).toContain("tsconfig.json");
200
+ expect(result.filesCopied).toContain("src/index.ts");
201
+ expect(result.filesCopied).not.toContain("node_modules/evil.js");
202
+
203
+ const appDir = join(targetDir, "apps", "test-app");
204
+ expect(existsSync(join(appDir, "package.json"))).toBe(true);
205
+ expect(existsSync(join(appDir, "src", "index.ts"))).toBe(true);
206
+ expect(existsSync(join(appDir, "node_modules", "evil.js"))).toBe(false);
207
+ });
208
+
209
+ test("migrate skips forge-protected files", async () => {
210
+ const sourceDir = join(tempDir, "source");
211
+ const targetDir = join(tempDir, "target");
212
+
213
+ await mkdir(sourceDir, { recursive: true });
214
+ await mkdir(targetDir, { recursive: true });
215
+ await mkdir(join(sourceDir, "docs", "rfcs"), { recursive: true });
216
+
217
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
218
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
219
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
220
+ await writeFile(join(sourceDir, "forge.yaml"), "schema: forge/config@1");
221
+ await writeFile(join(sourceDir, "PREFERENCES.md"), "# prefs");
222
+ await writeFile(join(sourceDir, "docs", "rfcs", "rfc-0001.md"), "# RFC");
223
+
224
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
225
+ const result = nodeTypescriptPnpmAdapter.migrate(sourceDir, targetDir, analysis);
226
+
227
+ expect(result.filesSkipped).toContain("forge.yaml");
228
+ expect(result.filesSkipped).toContain("PREFERENCES.md");
229
+ expect(result.filesSkipped).toContain("docs/rfcs/rfc-0001.md");
230
+
231
+ const appDir = join(targetDir, "apps", "test-app");
232
+ expect(existsSync(join(appDir, "forge.yaml"))).toBe(false);
233
+ expect(existsSync(join(appDir, "PREFERENCES.md"))).toBe(false);
234
+ expect(existsSync(join(appDir, "docs", "rfcs", "rfc-0001.md"))).toBe(false);
235
+ });
236
+
237
+ test("migrate copies untracked/git-ignored files like .env to apps/<appName>/", async () => {
238
+ const sourceDir = join(tempDir, "source");
239
+ const targetDir = join(tempDir, "target");
240
+
241
+ await mkdir(sourceDir, { recursive: true });
242
+ await mkdir(targetDir, { recursive: true });
243
+ await mkdir(join(sourceDir, "src"), { recursive: true });
244
+
245
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
246
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
247
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
248
+ await writeFile(join(sourceDir, "src", "index.ts"), "console.log('hello');");
249
+ await writeFile(join(sourceDir, ".env"), "SECRET=hello");
250
+ await writeFile(join(sourceDir, ".env.local"), "API_KEY=secret");
251
+ await writeFile(join(sourceDir, ".gitignore"), ".env\n.env.local\nnode_modules\n");
252
+
253
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
254
+ const result = nodeTypescriptPnpmAdapter.migrate(sourceDir, targetDir, analysis);
255
+
256
+ expect(result.filesCopied).toContain(".env");
257
+ expect(result.filesCopied).toContain(".env.local");
258
+ expect(result.filesCopied).toContain(".gitignore");
259
+
260
+ const appDir = join(targetDir, "apps", "test-app");
261
+ expect(existsSync(join(appDir, ".env"))).toBe(true);
262
+ expect(existsSync(join(appDir, ".env.local"))).toBe(true);
263
+ expect(existsSync(join(appDir, ".gitignore"))).toBe(true);
264
+ });
265
+
266
+ test("migrate excludes .git directory from copy", async () => {
267
+ const sourceDir = join(tempDir, "source");
268
+ const targetDir = join(tempDir, "target");
269
+
270
+ await mkdir(sourceDir, { recursive: true });
271
+ await mkdir(targetDir, { recursive: true });
272
+ await mkdir(join(sourceDir, "src"), { recursive: true });
273
+
274
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
275
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
276
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
277
+ await writeFile(join(sourceDir, "src", "index.ts"), "console.log('hello');");
278
+
279
+ const { execSync } = await import("node:child_process");
280
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
281
+ execSync('git config user.email "test@test.com"', { cwd: sourceDir, stdio: "pipe" });
282
+ execSync('git config user.name "Test"', { cwd: sourceDir, stdio: "pipe" });
283
+ execSync("git add .", { cwd: sourceDir, stdio: "pipe" });
284
+ execSync("git commit -m initial", { cwd: sourceDir, stdio: "pipe" });
285
+
286
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
287
+ const result = nodeTypescriptPnpmAdapter.migrate(sourceDir, targetDir, analysis);
288
+
289
+ expect(result.filesCopied).not.toContain(".git");
290
+ expect(result.filesCopied).not.toContain(".git/HEAD");
291
+
292
+ const appDir = join(targetDir, "apps", "test-app");
293
+ expect(existsSync(join(appDir, ".git"))).toBe(false);
294
+ });
295
+
296
+ test("postSetup runs git init when analysis.gitHistory is false", async () => {
297
+ const sourceDir = join(tempDir, "source");
298
+ const targetDir = join(tempDir, "target");
299
+
300
+ await mkdir(sourceDir, { recursive: true });
301
+ await mkdir(targetDir, { recursive: true });
302
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
303
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
304
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
305
+
306
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
307
+ nodeTypescriptPnpmAdapter.postSetup(sourceDir, targetDir, analysis);
308
+
309
+ expect(existsSync(join(targetDir, ".git"))).toBe(true);
310
+ });
311
+
312
+ test("postSetup runs git init when source has no .git", async () => {
313
+ const sourceDir = join(tempDir, "source");
314
+ const targetDir = join(tempDir, "target");
315
+
316
+ await mkdir(sourceDir, { recursive: true });
317
+ await mkdir(targetDir, { recursive: true });
318
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
319
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
320
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
321
+
322
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
323
+ expect(analysis.gitHistory).toBe(false);
324
+ nodeTypescriptPnpmAdapter.postSetup(sourceDir, targetDir, analysis);
325
+
326
+ expect(existsSync(join(targetDir, ".git"))).toBe(true);
327
+ });
328
+
329
+ test("postSetup transfers git history via format-patch + git am when source has commits", async () => {
330
+ const sourceDir = join(tempDir, "source");
331
+ const targetDir = join(tempDir, "target");
332
+
333
+ await mkdir(sourceDir, { recursive: true });
334
+ await mkdir(targetDir, { recursive: true });
335
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
336
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
337
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
338
+
339
+ const { execSync } = await import("node:child_process");
340
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
341
+ execSync('git config user.email "test@test.com"', { cwd: sourceDir, stdio: "pipe" });
342
+ execSync('git config user.name "Test"', { cwd: sourceDir, stdio: "pipe" });
343
+ await writeFile(join(sourceDir, "README.md"), "hello");
344
+ execSync("git add .", { cwd: sourceDir, stdio: "pipe" });
345
+ execSync("git commit -m initial", { cwd: sourceDir, stdio: "pipe" });
346
+
347
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
348
+ expect(analysis.gitHistory).toBe(true);
349
+ nodeTypescriptPnpmAdapter.postSetup(sourceDir, targetDir, analysis);
350
+
351
+ expect(existsSync(join(targetDir, ".git"))).toBe(true);
352
+ const logResult = execSync("git log --oneline", { cwd: targetDir, stdio: "pipe" }).toString();
353
+ expect(logResult).toContain("initial");
354
+ });
355
+
356
+ test("postSetup falls back to git init when source .git is empty (no commits)", async () => {
357
+ const sourceDir = join(tempDir, "source");
358
+ const targetDir = join(tempDir, "target");
359
+
360
+ await mkdir(sourceDir, { recursive: true });
361
+ await mkdir(targetDir, { recursive: true });
362
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test-app" }));
363
+ await writeFile(join(sourceDir, "tsconfig.json"), "{}");
364
+ await writeFile(join(sourceDir, "pnpm-lock.yaml"), "");
365
+
366
+ const { execSync } = await import("node:child_process");
367
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
368
+
369
+ const analysis = nodeTypescriptPnpmAdapter.analyze(sourceDir);
370
+ expect(analysis.gitHistory).toBe(true);
371
+ nodeTypescriptPnpmAdapter.postSetup(sourceDir, targetDir, analysis);
372
+
373
+ expect(existsSync(join(targetDir, ".git"))).toBe(true);
374
+ });
375
+
376
+ test("discoverIgnoredFiles returns empty for non-git project", async () => {
377
+ const sourceDir = join(tempDir, "source");
378
+ await mkdir(sourceDir, { recursive: true });
379
+ await writeFile(join(sourceDir, ".env"), "SECRET=hello");
380
+
381
+ const categories = discoverIgnoredFiles(sourceDir);
382
+ expect(categories).toHaveLength(0);
383
+ });
384
+
385
+ test("discoverIgnoredFiles categorizes .env as config", async () => {
386
+ const sourceDir = join(tempDir, "source");
387
+ await mkdir(sourceDir, { recursive: true });
388
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test" }));
389
+ await writeFile(join(sourceDir, ".env"), "SECRET=hello");
390
+ await writeFile(join(sourceDir, ".env.local"), "API_KEY=secret");
391
+ await writeFile(join(sourceDir, ".gitignore"), ".env\n.env.local\n");
392
+
393
+ const { execSync } = await import("node:child_process");
394
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
395
+ execSync("git add .gitignore package.json", { cwd: sourceDir, stdio: "pipe" });
396
+ execSync('git config user.email "t@t.com"', { cwd: sourceDir, stdio: "pipe" });
397
+ execSync('git config user.name "T"', { cwd: sourceDir, stdio: "pipe" });
398
+ execSync("git commit -m init", { cwd: sourceDir, stdio: "pipe" });
399
+
400
+ const categories = discoverIgnoredFiles(sourceDir);
401
+ const configCat = categories.find((c) => c.id === "config");
402
+ expect(configCat).toBeDefined();
403
+ expect(configCat!.paths).toContain(".env");
404
+ expect(configCat!.paths).toContain(".env.local");
405
+ expect(configCat!.fileCount).toBe(2);
406
+ });
407
+
408
+ test("discoverIgnoredFiles categorizes .input/ directories as data", async () => {
409
+ const sourceDir = join(tempDir, "source");
410
+ await mkdir(sourceDir, { recursive: true });
411
+ await mkdir(join(sourceDir, ".input", "videos"), { recursive: true });
412
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test" }));
413
+ await writeFile(join(sourceDir, ".input", "videos", "clip.mp4"), "fake-video");
414
+ await writeFile(join(sourceDir, ".gitignore"), ".input/\n");
415
+
416
+ const { execSync } = await import("node:child_process");
417
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
418
+ execSync("git add .gitignore package.json", { cwd: sourceDir, stdio: "pipe" });
419
+ execSync('git config user.email "t@t.com"', { cwd: sourceDir, stdio: "pipe" });
420
+ execSync('git config user.name "T"', { cwd: sourceDir, stdio: "pipe" });
421
+ execSync("git commit -m init", { cwd: sourceDir, stdio: "pipe" });
422
+
423
+ const categories = discoverIgnoredFiles(sourceDir);
424
+ const dataCat = categories.find((c) => c.id === "data");
425
+ expect(dataCat).toBeDefined();
426
+ expect(dataCat!.paths.some((p) => p.includes(".input"))).toBe(true);
427
+ expect(dataCat!.fileCount).toBeGreaterThanOrEqual(1);
428
+ });
429
+
430
+ test("discoverIgnoredFiles categorizes storage/ as runtime-state", async () => {
431
+ const sourceDir = join(tempDir, "source");
432
+ await mkdir(sourceDir, { recursive: true });
433
+ await mkdir(join(sourceDir, "storage"), { recursive: true });
434
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test" }));
435
+ await writeFile(join(sourceDir, "storage", "state.json"), "{}");
436
+ await writeFile(join(sourceDir, ".gitignore"), "storage/\n");
437
+
438
+ const { execSync } = await import("node:child_process");
439
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
440
+ execSync("git add .gitignore package.json", { cwd: sourceDir, stdio: "pipe" });
441
+ execSync('git config user.email "t@t.com"', { cwd: sourceDir, stdio: "pipe" });
442
+ execSync('git config user.name "T"', { cwd: sourceDir, stdio: "pipe" });
443
+ execSync("git commit -m init", { cwd: sourceDir, stdio: "pipe" });
444
+
445
+ const categories = discoverIgnoredFiles(sourceDir);
446
+ const stateCat = categories.find((c) => c.id === "runtime-state");
447
+ expect(stateCat).toBeDefined();
448
+ expect(stateCat!.paths.some((p) => p.includes("storage"))).toBe(true);
449
+ });
450
+
451
+ test("discoverIgnoredFiles returns categories sorted by priority (config first)", async () => {
452
+ const sourceDir = join(tempDir, "source");
453
+ await mkdir(sourceDir, { recursive: true });
454
+ await mkdir(join(sourceDir, "storage"), { recursive: true });
455
+ await mkdir(join(sourceDir, ".input"), { recursive: true });
456
+ await writeFile(join(sourceDir, "package.json"), JSON.stringify({ name: "test" }));
457
+ await writeFile(join(sourceDir, ".env"), "SECRET=hello");
458
+ await writeFile(join(sourceDir, ".input", "data.bin"), "data");
459
+ await writeFile(join(sourceDir, "storage", "state.json"), "{}");
460
+ await writeFile(join(sourceDir, ".gitignore"), ".env\n.input/\nstorage/\n");
461
+
462
+ const { execSync } = await import("node:child_process");
463
+ execSync("git init", { cwd: sourceDir, stdio: "pipe" });
464
+ execSync("git add .gitignore package.json", { cwd: sourceDir, stdio: "pipe" });
465
+ execSync('git config user.email "t@t.com"', { cwd: sourceDir, stdio: "pipe" });
466
+ execSync('git config user.name "T"', { cwd: sourceDir, stdio: "pipe" });
467
+ execSync("git commit -m init", { cwd: sourceDir, stdio: "pipe" });
468
+
469
+ const categories = discoverIgnoredFiles(sourceDir);
470
+ expect(categories.length).toBeGreaterThanOrEqual(3);
471
+ expect(categories[0].id).toBe("config");
472
+ });
473
+
474
+ test("formatSize formats bytes correctly", () => {
475
+ expect(formatSize(0)).toBe("0 B");
476
+ expect(formatSize(512)).toBe("512 B");
477
+ expect(formatSize(1024)).toBe("1.0 KB");
478
+ expect(formatSize(1024 * 1024)).toBe("1.0 MB");
479
+ expect(formatSize(1024 * 1024 * 1024)).toBe("1.0 GB");
480
+ });