@cleocode/core 2026.4.12 → 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.
@@ -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
+ });