File size: 4,970 Bytes
d522ffa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { writeMigrationErrorsLog } from '../src/migration-errors-log.js';
import { migrationErrorsLogFile } from '../src/paths.js';

let tgt: string;
let src: string;
beforeEach(async () => {
  tgt = await mkdtemp(join(tmpdir(), 'errlog-tgt-'));
  src = await mkdtemp(join(tmpdir(), 'errlog-src-'));
});
afterEach(async () => {
  await rm(tgt, { recursive: true, force: true });
  await rm(src, { recursive: true, force: true });
});

describe('writeMigrationErrorsLog', () => {
  it('appends a "no failures" marker block on a successful run', async () => {
    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-19T00:00:00Z',
      failures: [],
    });
    const log = await readFile(migrationErrorsLogFile(tgt), 'utf-8');
    expect(log).toContain('===== migration run @ 2026-05-19T00:00:00Z =====');
    expect(log).toContain('no failures.');
  });

  it('writes a diagnostic block per failure with a context.jsonl role histogram', async () => {
    const sessionDir = join(src, 'ses-1');
    await mkdir(sessionDir, { recursive: true });
    await writeFile(
      join(sessionDir, 'context.jsonl'),
      '{"role":"_system_prompt","content":"x"}\n{"role":"user","content":"hi"}\n',
    );

    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-19T00:00:00Z',
      failures: [{ sourcePath: sessionDir, reason: 'write failed: ENOSPC' }],
    });

    const log = await readFile(migrationErrorsLogFile(tgt), 'utf-8');
    expect(log).toContain('===== migration run @ 2026-05-19T00:00:00Z =====');
    expect(log).toContain('1 session(s) failed to migrate.');
    expect(log).toContain(sessionDir);
    expect(log).toContain('write failed: ENOSPC');
    expect(log).toContain('context.jsonl: 2 lines');
    expect(log).toContain('_system_prompt=1');
    expect(log).toContain('user=1');
  });

  it('notes an unreadable context.jsonl rather than throwing', async () => {
    const sessionDir = join(src, 'gone');
    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-19T00:00:00Z',
      failures: [{ sourcePath: sessionDir, reason: 'cannot read context.jsonl' }],
    });
    const log = await readFile(migrationErrorsLogFile(tgt), 'utf-8');
    expect(log).toContain('context.jsonl: unreadable');
  });

  it('appends a second block when a later run also has failures', async () => {
    // Cross-run history: a user who retries the migration and hits failures
    // each time must end up with one log that shows every attempt — the team
    // can analyze the full retry sequence from a single file.
    const ses1 = join(src, 'ses-1');
    const ses2 = join(src, 'ses-2');
    await mkdir(ses1, { recursive: true });
    await mkdir(ses2, { recursive: true });
    await writeFile(join(ses1, 'context.jsonl'), '{"role":"user","content":"a"}\n');
    await writeFile(join(ses2, 'context.jsonl'), '{"role":"user","content":"b"}\n');

    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-19T00:00:00Z',
      failures: [{ sourcePath: ses1, reason: 'first-run reason' }],
    });
    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-20T00:00:00Z',
      failures: [{ sourcePath: ses2, reason: 'second-run reason' }],
    });

    const log = await readFile(migrationErrorsLogFile(tgt), 'utf-8');
    // Both run headers survive (append-only, no overwrite).
    const headerMatches = log.match(/===== migration run @ /g) ?? [];
    expect(headerMatches).toHaveLength(2);
    expect(log).toContain('2026-05-19T00:00:00Z');
    expect(log).toContain('2026-05-20T00:00:00Z');
    // Both runs' failure data survive.
    expect(log).toContain('first-run reason');
    expect(log).toContain('second-run reason');
    expect(log).toContain(ses1);
    expect(log).toContain(ses2);
  });

  it('appends after an earlier failed run when the later run has no failures', async () => {
    // The earlier failure record must NOT be deleted by a successful retry —
    // we want the timeline visible even after the user recovers.
    const ses1 = join(src, 'ses-1');
    await mkdir(ses1, { recursive: true });
    await writeFile(join(ses1, 'context.jsonl'), '{"role":"user","content":"a"}\n');

    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-19T00:00:00Z',
      failures: [{ sourcePath: ses1, reason: 'first-run reason' }],
    });
    await writeMigrationErrorsLog(tgt, {
      startedAt: '2026-05-20T00:00:00Z',
      failures: [],
    });

    const log = await readFile(migrationErrorsLogFile(tgt), 'utf-8');
    expect(log).toContain('===== migration run @ 2026-05-19T00:00:00Z =====');
    expect(log).toContain('first-run reason');
    expect(log).toContain('===== migration run @ 2026-05-20T00:00:00Z =====');
    expect(log).toContain('no failures.');
  });
});