@cleocode/core 2026.4.11 → 2026.4.13

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 (184) hide show
  1. package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
  2. package/dist/codebase-map/analyzers/architecture.js +0 -1
  3. package/dist/codebase-map/analyzers/architecture.js.map +1 -1
  4. package/dist/conduit/local-transport.d.ts +18 -8
  5. package/dist/conduit/local-transport.d.ts.map +1 -1
  6. package/dist/conduit/local-transport.js +23 -13
  7. package/dist/conduit/local-transport.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +19 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +6 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.js +175 -68950
  16. package/dist/index.js.map +1 -7
  17. package/dist/init.d.ts +1 -2
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -2
  20. package/dist/init.js.map +1 -1
  21. package/dist/internal.d.ts +8 -3
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +13 -6
  24. package/dist/internal.js.map +1 -1
  25. package/dist/memory/learnings.d.ts +2 -2
  26. package/dist/memory/patterns.d.ts +6 -6
  27. package/dist/output.d.ts +32 -11
  28. package/dist/output.d.ts.map +1 -1
  29. package/dist/output.js +67 -67
  30. package/dist/output.js.map +1 -1
  31. package/dist/paths.js +80 -14
  32. package/dist/paths.js.map +1 -1
  33. package/dist/skills/dynamic-skill-generator.d.ts +0 -2
  34. package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
  35. package/dist/skills/dynamic-skill-generator.js.map +1 -1
  36. package/dist/store/agent-registry-accessor.d.ts +203 -12
  37. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  38. package/dist/store/agent-registry-accessor.js +618 -100
  39. package/dist/store/agent-registry-accessor.js.map +1 -1
  40. package/dist/store/api-key-kdf.d.ts +73 -0
  41. package/dist/store/api-key-kdf.d.ts.map +1 -0
  42. package/dist/store/api-key-kdf.js +84 -0
  43. package/dist/store/api-key-kdf.js.map +1 -0
  44. package/dist/store/cleanup-legacy.js +171 -0
  45. package/dist/store/cleanup-legacy.js.map +1 -0
  46. package/dist/store/conduit-sqlite.d.ts +184 -0
  47. package/dist/store/conduit-sqlite.d.ts.map +1 -0
  48. package/dist/store/conduit-sqlite.js +570 -0
  49. package/dist/store/conduit-sqlite.js.map +1 -0
  50. package/dist/store/global-salt.d.ts +78 -0
  51. package/dist/store/global-salt.d.ts.map +1 -0
  52. package/dist/store/global-salt.js +147 -0
  53. package/dist/store/global-salt.js.map +1 -0
  54. package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
  55. package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
  56. package/dist/store/migrate-signaldock-to-conduit.js +555 -0
  57. package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
  58. package/dist/store/nexus-sqlite.js +28 -3
  59. package/dist/store/nexus-sqlite.js.map +1 -1
  60. package/dist/store/signaldock-sqlite.d.ts +122 -19
  61. package/dist/store/signaldock-sqlite.d.ts.map +1 -1
  62. package/dist/store/signaldock-sqlite.js +401 -251
  63. package/dist/store/signaldock-sqlite.js.map +1 -1
  64. package/dist/store/sqlite-backup.js +122 -4
  65. package/dist/store/sqlite-backup.js.map +1 -1
  66. package/dist/system/backup.d.ts +0 -26
  67. package/dist/system/backup.d.ts.map +1 -1
  68. package/dist/system/runtime.d.ts +0 -2
  69. package/dist/system/runtime.d.ts.map +1 -1
  70. package/dist/system/runtime.js +3 -3
  71. package/dist/system/runtime.js.map +1 -1
  72. package/dist/tasks/add.d.ts +1 -1
  73. package/dist/tasks/add.d.ts.map +1 -1
  74. package/dist/tasks/add.js +98 -23
  75. package/dist/tasks/add.js.map +1 -1
  76. package/dist/tasks/complete.d.ts.map +1 -1
  77. package/dist/tasks/complete.js +4 -1
  78. package/dist/tasks/complete.js.map +1 -1
  79. package/dist/tasks/find.d.ts.map +1 -1
  80. package/dist/tasks/find.js +4 -1
  81. package/dist/tasks/find.js.map +1 -1
  82. package/dist/tasks/labels.d.ts.map +1 -1
  83. package/dist/tasks/labels.js +4 -1
  84. package/dist/tasks/labels.js.map +1 -1
  85. package/dist/tasks/relates.d.ts.map +1 -1
  86. package/dist/tasks/relates.js +16 -4
  87. package/dist/tasks/relates.js.map +1 -1
  88. package/dist/tasks/show.d.ts.map +1 -1
  89. package/dist/tasks/show.js +4 -1
  90. package/dist/tasks/show.js.map +1 -1
  91. package/dist/tasks/update.d.ts.map +1 -1
  92. package/dist/tasks/update.js +32 -6
  93. package/dist/tasks/update.js.map +1 -1
  94. package/dist/validation/engine.d.ts.map +1 -1
  95. package/dist/validation/engine.js +16 -4
  96. package/dist/validation/engine.js.map +1 -1
  97. package/dist/validation/param-utils.d.ts +5 -3
  98. package/dist/validation/param-utils.d.ts.map +1 -1
  99. package/dist/validation/param-utils.js +8 -6
  100. package/dist/validation/param-utils.js.map +1 -1
  101. package/dist/validation/protocols/_shared.d.ts.map +1 -1
  102. package/dist/validation/protocols/_shared.js +13 -6
  103. package/dist/validation/protocols/_shared.js.map +1 -1
  104. package/package.json +9 -7
  105. package/src/adapters/__tests__/manager.test.ts +0 -1
  106. package/src/codebase-map/analyzers/architecture.ts +0 -1
  107. package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
  108. package/src/conduit/__tests__/local-transport.test.ts +14 -12
  109. package/src/conduit/local-transport.ts +23 -13
  110. package/src/config.ts +0 -1
  111. package/src/errors.ts +24 -0
  112. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
  113. package/src/init.ts +1 -2
  114. package/src/internal.ts +96 -2
  115. package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
  116. package/src/memory/__tests__/engine-compat.test.ts +2 -2
  117. package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
  118. package/src/observability/__tests__/index.test.ts +4 -4
  119. package/src/observability/__tests__/log-filter.test.ts +4 -4
  120. package/src/output.ts +73 -75
  121. package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
  122. package/src/sessions/__tests__/session-grade.test.ts +2 -2
  123. package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
  124. package/src/skills/dynamic-skill-generator.ts +0 -2
  125. package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
  126. package/src/store/__tests__/api-key-kdf.test.ts +113 -0
  127. package/src/store/__tests__/backup-crypto.test.ts +101 -0
  128. package/src/store/__tests__/backup-pack.test.ts +491 -0
  129. package/src/store/__tests__/backup-unpack.test.ts +298 -0
  130. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  131. package/src/store/__tests__/global-salt.test.ts +195 -0
  132. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  133. package/src/store/__tests__/regenerators.test.ts +234 -0
  134. package/src/store/__tests__/restore-conflict-report.test.ts +274 -0
  135. package/src/store/__tests__/restore-json-merge.test.ts +521 -0
  136. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  137. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  138. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  139. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  140. package/src/store/__tests__/t310-readiness.test.ts +111 -0
  141. package/src/store/__tests__/t311-integration.test.ts +661 -0
  142. package/src/store/agent-registry-accessor.ts +847 -140
  143. package/src/store/api-key-kdf.ts +104 -0
  144. package/src/store/backup-crypto.ts +209 -0
  145. package/src/store/backup-pack.ts +739 -0
  146. package/src/store/backup-unpack.ts +583 -0
  147. package/src/store/conduit-sqlite.ts +655 -0
  148. package/src/store/global-salt.ts +175 -0
  149. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  150. package/src/store/regenerators.ts +243 -0
  151. package/src/store/restore-conflict-report.ts +317 -0
  152. package/src/store/restore-json-merge.ts +653 -0
  153. package/src/store/signaldock-sqlite.ts +431 -254
  154. package/src/store/sqlite-backup.ts +185 -10
  155. package/src/store/t310-readiness.ts +119 -0
  156. package/src/system/backup.ts +2 -62
  157. package/src/system/runtime.ts +4 -6
  158. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  159. package/src/tasks/add.ts +99 -9
  160. package/src/tasks/complete.ts +4 -1
  161. package/src/tasks/find.ts +4 -1
  162. package/src/tasks/labels.ts +4 -1
  163. package/src/tasks/relates.ts +16 -4
  164. package/src/tasks/show.ts +4 -1
  165. package/src/tasks/update.ts +32 -3
  166. package/src/validation/__tests__/error-hints.test.ts +97 -0
  167. package/src/validation/engine.ts +16 -1
  168. package/src/validation/param-utils.ts +10 -7
  169. package/src/validation/protocols/_shared.ts +14 -6
  170. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  171. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  172. package/src/validation/protocols/cant/consensus.cant +74 -0
  173. package/src/validation/protocols/cant/contribution.cant +82 -0
  174. package/src/validation/protocols/cant/decomposition.cant +92 -0
  175. package/src/validation/protocols/cant/implementation.cant +67 -0
  176. package/src/validation/protocols/cant/provenance.cant +88 -0
  177. package/src/validation/protocols/cant/release.cant +96 -0
  178. package/src/validation/protocols/cant/research.cant +66 -0
  179. package/src/validation/protocols/cant/specification.cant +67 -0
  180. package/src/validation/protocols/cant/testing.cant +88 -0
  181. package/src/validation/protocols/cant/validation.cant +65 -0
  182. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  183. package/templates/config.template.json +0 -1
  184. package/templates/global-config.template.json +0 -1
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Tests for T352 dry-run JSON file generators.
3
+ *
4
+ * Verifies:
5
+ * 1. All generators return the correct `filename` field.
6
+ * 2. No generator writes anything to disk.
7
+ * 3. Machine-local fields reflect the `projectRoot` argument.
8
+ * 4. Different `projectRoot` values produce distinguishable output.
9
+ * 5. `regenerateAllJson` returns all three files.
10
+ *
11
+ * @task T352
12
+ * @epic T311
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
19
+ import {
20
+ regenerateAllJson,
21
+ regenerateConfigJson,
22
+ regenerateProjectContextJson,
23
+ regenerateProjectInfoJson,
24
+ } from '../regenerators.js';
25
+
26
+ describe('T352 regenerators (dry-run init JSON generators)', () => {
27
+ let tmpRoot: string;
28
+
29
+ beforeEach(() => {
30
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cleo-t352-'));
31
+ });
32
+
33
+ afterEach(() => {
34
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
35
+ });
36
+
37
+ // ── regenerateConfigJson ────────────────────────────────────────────
38
+
39
+ describe('regenerateConfigJson', () => {
40
+ it('returns filename="config.json"', () => {
41
+ const result = regenerateConfigJson(tmpRoot);
42
+ expect(result.filename).toBe('config.json');
43
+ });
44
+
45
+ it('returns a non-null object as content', () => {
46
+ const result = regenerateConfigJson(tmpRoot);
47
+ expect(result.content).toBeTypeOf('object');
48
+ expect(result.content).not.toBeNull();
49
+ });
50
+
51
+ it('does NOT write config.json to disk', () => {
52
+ regenerateConfigJson(tmpRoot);
53
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo', 'config.json'))).toBe(false);
54
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo'))).toBe(false);
55
+ });
56
+
57
+ it('content has expected top-level keys from createDefaultConfig', () => {
58
+ const result = regenerateConfigJson(tmpRoot);
59
+ const content = result.content as Record<string, unknown>;
60
+ // These keys mirror createDefaultConfig() in scaffold.ts
61
+ expect(content).toHaveProperty('version');
62
+ expect(content).toHaveProperty('output');
63
+ expect(content).toHaveProperty('backup');
64
+ expect(content).toHaveProperty('hierarchy');
65
+ expect(content).toHaveProperty('session');
66
+ expect(content).toHaveProperty('lifecycle');
67
+ });
68
+
69
+ it('is stable across two calls to the same projectRoot (modulo timestamps)', () => {
70
+ const a = regenerateConfigJson(tmpRoot);
71
+ const b = regenerateConfigJson(tmpRoot);
72
+ // All non-timestamp fields must be identical
73
+ expect(typeof a.content).toBe(typeof b.content);
74
+ expect((a.content as Record<string, unknown>)['version']).toBe(
75
+ (b.content as Record<string, unknown>)['version'],
76
+ );
77
+ });
78
+ });
79
+
80
+ // ── regenerateProjectInfoJson ───────────────────────────────────────
81
+
82
+ describe('regenerateProjectInfoJson', () => {
83
+ it('returns filename="project-info.json"', () => {
84
+ const result = regenerateProjectInfoJson(tmpRoot);
85
+ expect(result.filename).toBe('project-info.json');
86
+ });
87
+
88
+ it('returns a non-null object as content', () => {
89
+ const result = regenerateProjectInfoJson(tmpRoot);
90
+ expect(result.content).toBeTypeOf('object');
91
+ expect(result.content).not.toBeNull();
92
+ });
93
+
94
+ it('does NOT write project-info.json to disk', () => {
95
+ regenerateProjectInfoJson(tmpRoot);
96
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo', 'project-info.json'))).toBe(false);
97
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo'))).toBe(false);
98
+ });
99
+
100
+ it('content includes required machine-local fields', () => {
101
+ const result = regenerateProjectInfoJson(tmpRoot);
102
+ const content = result.content as Record<string, unknown>;
103
+ expect(content).toHaveProperty('projectHash');
104
+ expect(content).toHaveProperty('projectId');
105
+ expect(content).toHaveProperty('cleoVersion');
106
+ expect(content).toHaveProperty('lastUpdated');
107
+ expect(content).toHaveProperty('schemas');
108
+ });
109
+
110
+ it('projectHash reflects the resolved projectRoot path', () => {
111
+ const result = regenerateProjectInfoJson(tmpRoot);
112
+ const content = result.content as Record<string, unknown>;
113
+ // projectHash must be a non-empty string (SHA-256 prefix)
114
+ expect(typeof content['projectHash']).toBe('string');
115
+ expect((content['projectHash'] as string).length).toBeGreaterThan(0);
116
+ });
117
+
118
+ it('produces different projectHash for different projectRoots', () => {
119
+ const root2 = fs.mkdtempSync(path.join(os.tmpdir(), 'cleo-t352-other-'));
120
+ try {
121
+ const a = regenerateProjectInfoJson(tmpRoot);
122
+ const b = regenerateProjectInfoJson(root2);
123
+ expect((a.content as Record<string, unknown>)['projectHash']).not.toBe(
124
+ (b.content as Record<string, unknown>)['projectHash'],
125
+ );
126
+ } finally {
127
+ fs.rmSync(root2, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ it('schemas block contains config, sqlite, and projectContext keys', () => {
132
+ const result = regenerateProjectInfoJson(tmpRoot);
133
+ const schemas = (result.content as Record<string, unknown>)['schemas'] as Record<
134
+ string,
135
+ unknown
136
+ >;
137
+ expect(schemas).toHaveProperty('config');
138
+ expect(schemas).toHaveProperty('sqlite');
139
+ expect(schemas).toHaveProperty('projectContext');
140
+ });
141
+ });
142
+
143
+ // ── regenerateProjectContextJson ───────────────────────────────────
144
+
145
+ describe('regenerateProjectContextJson', () => {
146
+ it('returns filename="project-context.json"', () => {
147
+ const result = regenerateProjectContextJson(tmpRoot);
148
+ expect(result.filename).toBe('project-context.json');
149
+ });
150
+
151
+ it('returns a non-null object as content', () => {
152
+ const result = regenerateProjectContextJson(tmpRoot);
153
+ expect(result.content).toBeTypeOf('object');
154
+ expect(result.content).not.toBeNull();
155
+ });
156
+
157
+ it('does NOT write project-context.json to disk', () => {
158
+ regenerateProjectContextJson(tmpRoot);
159
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo', 'project-context.json'))).toBe(false);
160
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo'))).toBe(false);
161
+ });
162
+
163
+ it('content has required schema fields from detectProjectType', () => {
164
+ const result = regenerateProjectContextJson(tmpRoot);
165
+ const content = result.content as Record<string, unknown>;
166
+ expect(content).toHaveProperty('schemaVersion');
167
+ expect(content).toHaveProperty('detectedAt');
168
+ expect(content).toHaveProperty('projectTypes');
169
+ expect(content).toHaveProperty('monorepo');
170
+ });
171
+
172
+ it('detectedAt is a valid ISO timestamp', () => {
173
+ const result = regenerateProjectContextJson(tmpRoot);
174
+ const content = result.content as Record<string, unknown>;
175
+ const detectedAt = content['detectedAt'] as string;
176
+ expect(typeof detectedAt).toBe('string');
177
+ const parsed = new Date(detectedAt);
178
+ expect(Number.isNaN(parsed.getTime())).toBe(false);
179
+ });
180
+ });
181
+
182
+ // ── regenerateAllJson ───────────────────────────────────────────────
183
+
184
+ describe('regenerateAllJson', () => {
185
+ it('returns all three files', () => {
186
+ const all = regenerateAllJson(tmpRoot);
187
+ expect(all.config.filename).toBe('config.json');
188
+ expect(all.projectInfo.filename).toBe('project-info.json');
189
+ expect(all.projectContext.filename).toBe('project-context.json');
190
+ });
191
+
192
+ it('does NOT write any files to disk', () => {
193
+ regenerateAllJson(tmpRoot);
194
+ expect(fs.existsSync(path.join(tmpRoot, '.cleo'))).toBe(false);
195
+ });
196
+
197
+ it('all three content values are non-null objects', () => {
198
+ const all = regenerateAllJson(tmpRoot);
199
+ for (const file of [all.config, all.projectInfo, all.projectContext]) {
200
+ expect(file.content).toBeTypeOf('object');
201
+ expect(file.content).not.toBeNull();
202
+ }
203
+ });
204
+ });
205
+
206
+ // ── Cross-projectRoot differentiation ──────────────────────────────
207
+
208
+ describe('cross-projectRoot differentiation', () => {
209
+ it('regenerated project-info differs across different projectRoots', () => {
210
+ const root2 = fs.mkdtempSync(path.join(os.tmpdir(), 'cleo-t352-other-'));
211
+ try {
212
+ const a = regenerateProjectInfoJson(tmpRoot);
213
+ const b = regenerateProjectInfoJson(root2);
214
+ // At minimum the projectHash must differ (different paths)
215
+ expect(JSON.stringify(a.content)).not.toBe(JSON.stringify(b.content));
216
+ } finally {
217
+ fs.rmSync(root2, { recursive: true, force: true });
218
+ }
219
+ });
220
+ });
221
+
222
+ // ── Stability / shape ───────────────────────────────────────────────
223
+
224
+ describe('stability', () => {
225
+ it('config content has the same type shape across two calls', () => {
226
+ const a = regenerateConfigJson(tmpRoot);
227
+ const b = regenerateConfigJson(tmpRoot);
228
+ expect(typeof a.content).toBe(typeof b.content);
229
+ expect(Object.keys(a.content as Record<string, unknown>).sort()).toEqual(
230
+ Object.keys(b.content as Record<string, unknown>).sort(),
231
+ );
232
+ });
233
+ });
234
+ });
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Tests for the T357 conflict report generator.
3
+ *
4
+ * Verifies that {@link buildConflictReport} and {@link writeConflictReport}
5
+ * produce correctly structured markdown output per T311 spec §6.5 and that
6
+ * all edge-cases (empty warnings, missing values, multi-file reports) are
7
+ * handled consistently.
8
+ *
9
+ * @task T357
10
+ * @epic T311
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+
18
+ import { buildConflictReport, writeConflictReport } from '../restore-conflict-report.js';
19
+ import type { JsonRestoreReport } from '../restore-json-merge.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function makeReport(
26
+ filename: 'config.json' | 'project-info.json' | 'project-context.json',
27
+ ): JsonRestoreReport {
28
+ return {
29
+ filename,
30
+ localGenerated: { x: 1 },
31
+ imported: { x: 2 },
32
+ classifications: [
33
+ {
34
+ path: 'projectRoot',
35
+ local: '/local',
36
+ imported: '/source',
37
+ category: 'machine-local',
38
+ resolution: 'A',
39
+ rationale: 'expected to differ between machines',
40
+ },
41
+ {
42
+ path: 'brain.embeddingProvider',
43
+ local: 'local',
44
+ imported: 'openai',
45
+ category: 'user-intent',
46
+ resolution: 'B',
47
+ rationale: 'user intent — preserve from source',
48
+ },
49
+ {
50
+ path: 'somethingNew',
51
+ local: undefined,
52
+ imported: 'value',
53
+ category: 'unknown',
54
+ resolution: 'manual-review',
55
+ rationale: 'unclassified field — needs human review',
56
+ },
57
+ ],
58
+ applied: {},
59
+ conflictCount: 1,
60
+ };
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Suite
65
+ // ---------------------------------------------------------------------------
66
+
67
+ describe('T357 conflict report generator', () => {
68
+ let tmpRoot: string;
69
+
70
+ beforeEach(() => {
71
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cleo-t357-'));
72
+ });
73
+
74
+ afterEach(() => {
75
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
76
+ });
77
+
78
+ it('buildConflictReport returns a non-empty markdown string', () => {
79
+ const md = buildConflictReport({
80
+ reports: [makeReport('config.json')],
81
+ bundlePath: '/tmp/test.cleobundle.tar.gz',
82
+ sourceMachineFingerprint: 'aaaa',
83
+ targetMachineFingerprint: 'bbbb',
84
+ cleoVersion: '2026.4.13',
85
+ });
86
+ expect(md).toContain('# T311 Import Conflict Report');
87
+ expect(md).toContain('config.json');
88
+ expect(md).toContain('projectRoot');
89
+ expect(md).toContain('brain.embeddingProvider');
90
+ expect(md).toContain('somethingNew');
91
+ expect(md).toContain('manual-review');
92
+ });
93
+
94
+ it('groups Resolved and Manual review sections separately', () => {
95
+ const md = buildConflictReport({
96
+ reports: [makeReport('config.json')],
97
+ bundlePath: '/tmp/x',
98
+ sourceMachineFingerprint: 'a',
99
+ targetMachineFingerprint: 'b',
100
+ cleoVersion: '2026.4.13',
101
+ });
102
+ const resolvedIdx = md.indexOf('Resolved');
103
+ const manualIdx = md.indexOf('Manual review');
104
+ expect(resolvedIdx).toBeGreaterThanOrEqual(0);
105
+ expect(manualIdx).toBeGreaterThanOrEqual(0);
106
+ expect(manualIdx).toBeGreaterThan(resolvedIdx);
107
+ });
108
+
109
+ it('includes reauth warnings section when warnings present', () => {
110
+ const md = buildConflictReport({
111
+ reports: [makeReport('config.json')],
112
+ bundlePath: '/tmp/x',
113
+ sourceMachineFingerprint: 'a',
114
+ targetMachineFingerprint: 'b',
115
+ cleoVersion: '2026.4.13',
116
+ reauthWarnings: [
117
+ { agentId: 'agent-1', reason: 'KDF mismatch' },
118
+ { agentId: 'agent-2', reason: 'KDF mismatch' },
119
+ ],
120
+ });
121
+ expect(md).toContain('Agent re-authentication required');
122
+ expect(md).toContain('agent-1');
123
+ expect(md).toContain('agent-2');
124
+ });
125
+
126
+ it('includes schema warnings section when present', () => {
127
+ const md = buildConflictReport({
128
+ reports: [makeReport('config.json')],
129
+ bundlePath: '/tmp/x',
130
+ sourceMachineFingerprint: 'a',
131
+ targetMachineFingerprint: 'b',
132
+ cleoVersion: '2026.4.13',
133
+ schemaWarnings: [
134
+ { db: 'tasks', bundleVersion: '1', localVersion: '2', severity: 'older-bundle' },
135
+ ],
136
+ });
137
+ expect(md).toContain('Schema compatibility warnings');
138
+ expect(md).toContain('tasks');
139
+ expect(md).toContain('older-bundle');
140
+ });
141
+
142
+ it('handles empty reauth and schema warnings gracefully', () => {
143
+ const md = buildConflictReport({
144
+ reports: [makeReport('config.json')],
145
+ bundlePath: '/tmp/x',
146
+ sourceMachineFingerprint: 'a',
147
+ targetMachineFingerprint: 'b',
148
+ cleoVersion: '2026.4.13',
149
+ });
150
+ // Should still produce valid markdown — either skip the empty sections or print "None"
151
+ expect(md.length).toBeGreaterThan(100);
152
+ });
153
+
154
+ it('writeConflictReport writes to .cleo/restore-conflicts.md', () => {
155
+ const md = '# Test Report\n';
156
+ fs.mkdirSync(path.join(tmpRoot, '.cleo'), { recursive: true });
157
+ const written = writeConflictReport(tmpRoot, md);
158
+ expect(written).toBe(path.join(tmpRoot, '.cleo', 'restore-conflicts.md'));
159
+ expect(fs.readFileSync(written, 'utf-8')).toBe(md);
160
+ });
161
+
162
+ it('writeConflictReport creates .cleo/ if missing', () => {
163
+ const md = '# Test\n';
164
+ const written = writeConflictReport(tmpRoot, md);
165
+ expect(fs.existsSync(written)).toBe(true);
166
+ });
167
+
168
+ it('handles missing fields (undefined values) in formatValue', () => {
169
+ const md = buildConflictReport({
170
+ reports: [makeReport('config.json')],
171
+ bundlePath: '/tmp/x',
172
+ sourceMachineFingerprint: 'a',
173
+ targetMachineFingerprint: 'b',
174
+ cleoVersion: '2026.4.13',
175
+ });
176
+ // somethingNew has local=undefined → should render as "(not present)"
177
+ expect(md).toContain('not present');
178
+ });
179
+
180
+ it('multi-file reports include all three filenames', () => {
181
+ const md = buildConflictReport({
182
+ reports: [
183
+ makeReport('config.json'),
184
+ makeReport('project-info.json'),
185
+ makeReport('project-context.json'),
186
+ ],
187
+ bundlePath: '/tmp/x',
188
+ sourceMachineFingerprint: 'a',
189
+ targetMachineFingerprint: 'b',
190
+ cleoVersion: '2026.4.13',
191
+ });
192
+ expect(md).toContain('## config.json');
193
+ expect(md).toContain('## project-info.json');
194
+ expect(md).toContain('## project-context.json');
195
+ });
196
+
197
+ it('includes RESOLVED placeholder in manual-review items', () => {
198
+ const md = buildConflictReport({
199
+ reports: [makeReport('config.json')],
200
+ bundlePath: '/tmp/x',
201
+ sourceMachineFingerprint: 'a',
202
+ targetMachineFingerprint: 'b',
203
+ cleoVersion: '2026.4.13',
204
+ });
205
+ expect(md).toContain('RESOLVED:');
206
+ expect(md).toContain('cleo restore finalize');
207
+ });
208
+
209
+ it('includes source bundle path in header', () => {
210
+ const bundlePath = '/home/user/my-project.cleobundle.tar.gz';
211
+ const md = buildConflictReport({
212
+ reports: [makeReport('config.json')],
213
+ bundlePath,
214
+ sourceMachineFingerprint: 'src-fingerprint',
215
+ targetMachineFingerprint: 'tgt-fingerprint',
216
+ cleoVersion: '2026.4.13',
217
+ });
218
+ expect(md).toContain(bundlePath);
219
+ expect(md).toContain('src-fingerprint');
220
+ expect(md).toContain('tgt-fingerprint');
221
+ expect(md).toContain('2026.4.13');
222
+ });
223
+
224
+ it('shows _None_ for empty reauth warnings', () => {
225
+ const md = buildConflictReport({
226
+ reports: [makeReport('config.json')],
227
+ bundlePath: '/tmp/x',
228
+ sourceMachineFingerprint: 'a',
229
+ targetMachineFingerprint: 'b',
230
+ cleoVersion: '2026.4.13',
231
+ reauthWarnings: [],
232
+ });
233
+ // When no re-auth warnings, the section should contain _None_
234
+ const reauthIdx = md.indexOf('Agent re-authentication required');
235
+ expect(reauthIdx).toBeGreaterThanOrEqual(0);
236
+ const afterReauth = md.slice(reauthIdx);
237
+ expect(afterReauth).toContain('_None_');
238
+ });
239
+
240
+ it('shows _None_ for empty schema warnings', () => {
241
+ const md = buildConflictReport({
242
+ reports: [makeReport('config.json')],
243
+ bundlePath: '/tmp/x',
244
+ sourceMachineFingerprint: 'a',
245
+ targetMachineFingerprint: 'b',
246
+ cleoVersion: '2026.4.13',
247
+ schemaWarnings: [],
248
+ });
249
+ const schemaIdx = md.indexOf('Schema compatibility warnings');
250
+ expect(schemaIdx).toBeGreaterThanOrEqual(0);
251
+ const afterSchema = md.slice(schemaIdx);
252
+ expect(afterSchema).toContain('_None_');
253
+ });
254
+
255
+ it('newer-bundle schema warning contains correct status text', () => {
256
+ const md = buildConflictReport({
257
+ reports: [makeReport('config.json')],
258
+ bundlePath: '/tmp/x',
259
+ sourceMachineFingerprint: 'a',
260
+ targetMachineFingerprint: 'b',
261
+ cleoVersion: '2026.4.13',
262
+ schemaWarnings: [
263
+ { db: 'conduit', bundleVersion: '99', localVersion: '1', severity: 'newer-bundle' },
264
+ ],
265
+ });
266
+ expect(md).toContain('newer-bundle: upgrade cleo for full support');
267
+ });
268
+
269
+ it('writeConflictReport returns absolute path', () => {
270
+ const md = '# X\n';
271
+ const written = writeConflictReport(tmpRoot, md);
272
+ expect(path.isAbsolute(written)).toBe(true);
273
+ });
274
+ });