@auxiora/sessions 1.0.0

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.
package/src/manager.ts ADDED
@@ -0,0 +1,343 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import * as crypto from 'node:crypto';
4
+ import { getSessionsDir } from '@auxiora/core';
5
+ import { audit } from '@auxiora/audit';
6
+ import { SessionDatabase } from './db.js';
7
+ import type { Session, SessionConfig, Message, MessageRole, SessionMetadata, Chat, ListChatsOptions } from './types.js';
8
+
9
+ function generateMessageId(): string {
10
+ const timestamp = Date.now().toString(36);
11
+ const random = crypto.randomBytes(4).toString('hex');
12
+ return `${timestamp}-${random}`;
13
+ }
14
+
15
+ export class SessionManager {
16
+ private sessions: Map<string, Session> = new Map();
17
+ private config: SessionConfig;
18
+ private db: SessionDatabase;
19
+ private sessionsDir: string;
20
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
21
+
22
+ constructor(config: SessionConfig) {
23
+ this.config = config;
24
+ this.sessionsDir = config.sessionsDir ?? getSessionsDir();
25
+ const dbPath = config.dbPath ?? path.join(this.sessionsDir, 'sessions.db');
26
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
27
+ this.db = new SessionDatabase(dbPath);
28
+
29
+ // Cleanup expired sessions every 5 minutes (non-webchat channels only)
30
+ this.cleanupInterval = setInterval(() => this.cleanupExpired(), 5 * 60 * 1000);
31
+ }
32
+
33
+ async initialize(): Promise<void> {
34
+ // Migrate legacy JSON session files to SQLite
35
+ let files: string[];
36
+ try {
37
+ files = fs.readdirSync(this.sessionsDir).filter(f => f.endsWith('.json'));
38
+ } catch {
39
+ return; // Directory doesn't exist or not readable
40
+ }
41
+
42
+ if (files.length === 0) return;
43
+
44
+ const migratedDir = path.join(this.sessionsDir, 'migrated');
45
+ fs.mkdirSync(migratedDir, { recursive: true });
46
+
47
+ for (const file of files) {
48
+ const filePath = path.join(this.sessionsDir, file);
49
+ try {
50
+ const raw = fs.readFileSync(filePath, 'utf-8');
51
+ const data = JSON.parse(raw) as {
52
+ id: string;
53
+ messages: Array<{ id: string; role: string; content: string; timestamp: number; tokens?: { input?: number; output?: number } }>;
54
+ metadata: { channelType: string; senderId?: string; createdAt: number; lastActiveAt: number };
55
+ };
56
+
57
+ const title = data.metadata.channelType === 'webchat' ? 'New Chat' : `${data.metadata.channelType} session`;
58
+ this.db.insertChatWithId(
59
+ data.id,
60
+ title,
61
+ data.metadata.channelType,
62
+ data.metadata.createdAt,
63
+ data.metadata.lastActiveAt,
64
+ data.metadata.senderId,
65
+ );
66
+
67
+ for (const msg of data.messages) {
68
+ this.db.addMessage(
69
+ data.id,
70
+ msg.id,
71
+ msg.role,
72
+ msg.content,
73
+ msg.timestamp,
74
+ msg.tokens?.input,
75
+ msg.tokens?.output,
76
+ );
77
+ }
78
+
79
+ // Move original to migrated/
80
+ fs.renameSync(filePath, path.join(migratedDir, file));
81
+ } catch {
82
+ // Skip corrupt files — leave them in place
83
+ }
84
+ }
85
+ }
86
+
87
+ async create(metadata: Partial<SessionMetadata> & { channelType: string }): Promise<Session> {
88
+ const title = metadata.channelType === 'webchat' ? 'New Chat' : `${metadata.channelType} session`;
89
+ const chat = this.db.createChat(title, metadata.channelType, metadata.senderId);
90
+
91
+ const now = Date.now();
92
+ const session: Session = {
93
+ id: chat.id,
94
+ messages: [],
95
+ metadata: {
96
+ channelType: metadata.channelType,
97
+ senderId: metadata.senderId,
98
+ clientId: metadata.clientId,
99
+ createdAt: now,
100
+ lastActiveAt: now,
101
+ },
102
+ };
103
+
104
+ this.sessions.set(chat.id, session);
105
+ await audit('session.created', { sessionId: chat.id, channelType: metadata.channelType });
106
+ return session;
107
+ }
108
+
109
+ async get(id: string): Promise<Session | null> {
110
+ // Check in-memory cache
111
+ if (this.sessions.has(id)) {
112
+ return this.sessions.get(id)!;
113
+ }
114
+
115
+ // Check DB
116
+ const chat = this.db.getChat(id);
117
+ if (!chat) return null;
118
+
119
+ const messages = this.db.getMessages(id);
120
+ const session: Session = {
121
+ id: chat.id,
122
+ messages,
123
+ metadata: {
124
+ channelType: chat.channel,
125
+ senderId: (chat.metadata?.senderId as string | undefined),
126
+ clientId: (chat.metadata?.clientId as string | undefined),
127
+ createdAt: chat.createdAt,
128
+ lastActiveAt: chat.updatedAt,
129
+ },
130
+ };
131
+
132
+ this.sessions.set(id, session);
133
+ return session;
134
+ }
135
+
136
+ async getOrCreate(
137
+ key: string,
138
+ metadata: Partial<SessionMetadata> & { channelType: string },
139
+ ): Promise<Session> {
140
+ // Check in-memory cache first
141
+ for (const session of this.sessions.values()) {
142
+ if (
143
+ session.metadata.senderId === metadata.senderId &&
144
+ session.metadata.channelType === metadata.channelType
145
+ ) {
146
+ session.metadata.lastActiveAt = Date.now();
147
+ return session;
148
+ }
149
+ }
150
+
151
+ // Check DB
152
+ if (metadata.senderId) {
153
+ const chat = this.db.getOrCreateSessionChat(metadata.senderId, metadata.channelType);
154
+ const existing = this.sessions.get(chat.id);
155
+ if (existing) {
156
+ existing.metadata.lastActiveAt = Date.now();
157
+ return existing;
158
+ }
159
+
160
+ const messages = this.db.getMessages(chat.id);
161
+ const session: Session = {
162
+ id: chat.id,
163
+ messages,
164
+ metadata: {
165
+ channelType: metadata.channelType,
166
+ senderId: metadata.senderId,
167
+ clientId: metadata.clientId,
168
+ createdAt: chat.createdAt,
169
+ lastActiveAt: Date.now(),
170
+ },
171
+ };
172
+ this.sessions.set(chat.id, session);
173
+ return session;
174
+ }
175
+
176
+ // No senderId — create new
177
+ return this.create(metadata);
178
+ }
179
+
180
+ async addMessage(
181
+ sessionId: string,
182
+ role: MessageRole,
183
+ content: string,
184
+ tokens?: { input?: number; output?: number },
185
+ ): Promise<Message> {
186
+ const session = await this.get(sessionId);
187
+ if (!session) {
188
+ throw new Error(`Session not found: ${sessionId}`);
189
+ }
190
+
191
+ const message: Message = {
192
+ id: generateMessageId(),
193
+ role,
194
+ content,
195
+ timestamp: Date.now(),
196
+ tokens,
197
+ };
198
+
199
+ session.messages.push(message);
200
+ session.metadata.lastActiveAt = Date.now();
201
+
202
+ // Persist to DB
203
+ this.db.addMessage(sessionId, message.id, role, content, message.timestamp, tokens?.input, tokens?.output);
204
+
205
+ return message;
206
+ }
207
+
208
+ getMessages(sessionId: string): Message[] {
209
+ const session = this.sessions.get(sessionId);
210
+ if (session) return session.messages;
211
+
212
+ // Fall back to DB
213
+ return this.db.getMessages(sessionId);
214
+ }
215
+
216
+ getContextMessages(sessionId: string, maxTokens?: number): Message[] {
217
+ const limit = maxTokens || this.config.maxContextTokens;
218
+
219
+ // Try in-memory first
220
+ const session = this.sessions.get(sessionId);
221
+ if (session) {
222
+ const messages: Message[] = [];
223
+ let tokenCount = 0;
224
+ for (let i = session.messages.length - 1; i >= 0; i--) {
225
+ const msg = session.messages[i];
226
+ const msgTokens = Math.ceil(msg.content.length / 4);
227
+ if (tokenCount + msgTokens > limit) break;
228
+ messages.unshift(msg);
229
+ tokenCount += msgTokens;
230
+ }
231
+ return messages;
232
+ }
233
+
234
+ // Fall back to DB
235
+ return this.db.getContextMessages(sessionId, limit);
236
+ }
237
+
238
+ async setSystemPrompt(sessionId: string, prompt: string): Promise<void> {
239
+ const session = await this.get(sessionId);
240
+ if (!session) {
241
+ throw new Error(`Session not found: ${sessionId}`);
242
+ }
243
+ session.systemPrompt = prompt;
244
+ }
245
+
246
+ async save(_sessionId: string): Promise<void> {
247
+ // No-op — writes are immediate with SQLite
248
+ }
249
+
250
+ async delete(sessionId: string): Promise<boolean> {
251
+ const existed = this.sessions.delete(sessionId);
252
+ this.db.deleteChat(sessionId);
253
+
254
+ if (existed) {
255
+ await audit('session.destroyed', { sessionId });
256
+ }
257
+ return existed;
258
+ }
259
+
260
+ async clear(sessionId: string): Promise<void> {
261
+ const session = await this.get(sessionId);
262
+ if (!session) return;
263
+
264
+ session.messages = [];
265
+ session.metadata.lastActiveAt = Date.now();
266
+ this.db.clearMessages(sessionId);
267
+ }
268
+
269
+ async compact(sessionId: string, summary: string): Promise<void> {
270
+ const session = await this.get(sessionId);
271
+ if (!session || !this.config.compactionEnabled) return;
272
+
273
+ const originalCount = session.messages.length;
274
+ this.db.clearMessages(sessionId);
275
+
276
+ const summaryMessage: Message = {
277
+ id: generateMessageId(),
278
+ role: 'system',
279
+ content: `[Previous conversation summary]\n${summary}`,
280
+ timestamp: Date.now(),
281
+ };
282
+
283
+ session.messages = [summaryMessage];
284
+ session.metadata.lastActiveAt = Date.now();
285
+ this.db.addMessage(sessionId, summaryMessage.id, 'system', summaryMessage.content, summaryMessage.timestamp);
286
+
287
+ await audit('session.compacted', { sessionId, originalCount });
288
+ }
289
+
290
+ private cleanupExpired(): void {
291
+ const now = Date.now();
292
+ const ttlMs = this.config.ttlMinutes * 60 * 1000;
293
+
294
+ for (const [id, session] of this.sessions) {
295
+ // Only expire non-webchat sessions from memory
296
+ if (session.metadata.channelType === 'webchat') continue;
297
+ if (now - session.metadata.lastActiveAt > ttlMs) {
298
+ this.sessions.delete(id);
299
+ }
300
+ }
301
+ }
302
+
303
+ getActiveSessions(): Session[] {
304
+ return Array.from(this.sessions.values());
305
+ }
306
+
307
+ // ── Chat management ──
308
+
309
+ createChat(title?: string): Chat {
310
+ return this.db.createChat(title ?? 'New Chat', 'webchat');
311
+ }
312
+
313
+ listChats(options?: ListChatsOptions): Chat[] {
314
+ return this.db.listChats(options);
315
+ }
316
+
317
+ renameChat(chatId: string, title: string): void {
318
+ this.db.renameChat(chatId, title);
319
+ }
320
+
321
+ archiveChat(chatId: string): void {
322
+ this.db.archiveChat(chatId);
323
+ this.sessions.delete(chatId);
324
+ }
325
+
326
+ deleteChat(chatId: string): void {
327
+ this.db.deleteChat(chatId);
328
+ this.sessions.delete(chatId);
329
+ }
330
+
331
+ getChatMessages(chatId: string): Message[] {
332
+ return this.db.getMessages(chatId);
333
+ }
334
+
335
+ destroy(): void {
336
+ if (this.cleanupInterval) {
337
+ clearInterval(this.cleanupInterval);
338
+ this.cleanupInterval = null;
339
+ }
340
+ this.sessions.clear();
341
+ this.db.close();
342
+ }
343
+ }
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ export type MessageRole = 'user' | 'assistant' | 'system';
2
+
3
+ export interface Message {
4
+ id: string;
5
+ role: MessageRole;
6
+ content: string;
7
+ timestamp: number;
8
+ tokens?: {
9
+ input?: number;
10
+ output?: number;
11
+ };
12
+ }
13
+
14
+ export interface SessionMetadata {
15
+ channelType: string;
16
+ senderId?: string;
17
+ clientId?: string;
18
+ createdAt: number;
19
+ lastActiveAt: number;
20
+ activeMode?: string;
21
+ modeAutoDetected?: boolean;
22
+ escalationLevel?: string;
23
+ suspendedMode?: string;
24
+ }
25
+
26
+ export interface Session {
27
+ id: string;
28
+ messages: Message[];
29
+ metadata: SessionMetadata;
30
+ systemPrompt?: string;
31
+ }
32
+
33
+ export interface SessionConfig {
34
+ maxContextTokens: number;
35
+ ttlMinutes: number;
36
+ autoSave: boolean;
37
+ compactionEnabled: boolean;
38
+ dbPath?: string;
39
+ sessionsDir?: string;
40
+ }
41
+
42
+ export interface Chat {
43
+ id: string;
44
+ title: string;
45
+ channel: string;
46
+ createdAt: number;
47
+ updatedAt: number;
48
+ archived: boolean;
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+
52
+ export interface ListChatsOptions {
53
+ archived?: boolean;
54
+ limit?: number;
55
+ offset?: number;
56
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { SessionDatabase } from '../src/db.js';
6
+
7
+ const testDir = path.join(os.tmpdir(), 'auxiora-db-test-' + Date.now());
8
+ const dbPath = path.join(testDir, 'test.db');
9
+
10
+ describe('SessionDatabase', () => {
11
+ let db: SessionDatabase;
12
+
13
+ beforeEach(() => {
14
+ fs.mkdirSync(testDir, { recursive: true });
15
+ db = new SessionDatabase(dbPath);
16
+ });
17
+
18
+ afterEach(() => {
19
+ db.close();
20
+ fs.rmSync(testDir, { recursive: true, force: true });
21
+ });
22
+
23
+ describe('chats', () => {
24
+ it('should create a chat', () => {
25
+ const chat = db.createChat('Test Chat', 'webchat');
26
+ expect(chat.id).toBeDefined();
27
+ expect(chat.title).toBe('Test Chat');
28
+ expect(chat.channel).toBe('webchat');
29
+ expect(chat.archived).toBe(false);
30
+ });
31
+
32
+ it('should list chats ordered by updatedAt descending', () => {
33
+ const chat1 = db.createChat('First', 'webchat');
34
+ const chat2 = db.createChat('Second', 'webchat');
35
+ db.addMessage(chat1.id, 'msg-1', 'user', 'hello', Date.now() + 1000);
36
+
37
+ const chats = db.listChats();
38
+ expect(chats).toHaveLength(2);
39
+ expect(chats[0].id).toBe(chat1.id);
40
+ });
41
+
42
+ it('should filter out archived chats by default', () => {
43
+ db.createChat('Active', 'webchat');
44
+ const archived = db.createChat('Archived', 'webchat');
45
+ db.archiveChat(archived.id);
46
+
47
+ const chats = db.listChats();
48
+ expect(chats).toHaveLength(1);
49
+ expect(chats[0].title).toBe('Active');
50
+ });
51
+
52
+ it('should include archived chats when requested', () => {
53
+ db.createChat('Active', 'webchat');
54
+ const archived = db.createChat('Archived', 'webchat');
55
+ db.archiveChat(archived.id);
56
+
57
+ const chats = db.listChats({ archived: true });
58
+ expect(chats).toHaveLength(2);
59
+ });
60
+
61
+ it('should rename a chat', () => {
62
+ const chat = db.createChat('Old Name', 'webchat');
63
+ db.renameChat(chat.id, 'New Name');
64
+ const updated = db.getChat(chat.id);
65
+ expect(updated?.title).toBe('New Name');
66
+ });
67
+
68
+ it('should delete a chat and its messages', () => {
69
+ const chat = db.createChat('To Delete', 'webchat');
70
+ db.addMessage(chat.id, 'msg-1', 'user', 'hello', Date.now());
71
+ db.deleteChat(chat.id);
72
+ expect(db.getChat(chat.id)).toBeUndefined();
73
+ expect(db.getMessages(chat.id)).toEqual([]);
74
+ });
75
+
76
+ it('should support pagination', () => {
77
+ for (let i = 0; i < 5; i++) {
78
+ db.createChat(`Chat ${i}`, 'webchat');
79
+ }
80
+ const page1 = db.listChats({ limit: 2 });
81
+ expect(page1).toHaveLength(2);
82
+ const page2 = db.listChats({ limit: 2, offset: 2 });
83
+ expect(page2).toHaveLength(2);
84
+ const page3 = db.listChats({ limit: 2, offset: 4 });
85
+ expect(page3).toHaveLength(1);
86
+ });
87
+ });
88
+
89
+ describe('messages', () => {
90
+ it('should add and retrieve messages', () => {
91
+ const chat = db.createChat('Test', 'webchat');
92
+ db.addMessage(chat.id, 'msg-1', 'user', 'Hello', 1000);
93
+ db.addMessage(chat.id, 'msg-2', 'assistant', 'Hi there', 2000);
94
+
95
+ const messages = db.getMessages(chat.id);
96
+ expect(messages).toHaveLength(2);
97
+ expect(messages[0].role).toBe('user');
98
+ expect(messages[0].content).toBe('Hello');
99
+ expect(messages[1].role).toBe('assistant');
100
+ });
101
+
102
+ it('should store token counts', () => {
103
+ const chat = db.createChat('Test', 'webchat');
104
+ db.addMessage(chat.id, 'msg-1', 'assistant', 'response', 1000, 100, 50);
105
+ const messages = db.getMessages(chat.id);
106
+ expect(messages[0].tokens).toEqual({ input: 100, output: 50 });
107
+ });
108
+
109
+ it('should update chat updatedAt when adding messages', () => {
110
+ const chat = db.createChat('Test', 'webchat');
111
+ const originalUpdatedAt = chat.updatedAt;
112
+ db.addMessage(chat.id, 'msg-1', 'user', 'Hello', originalUpdatedAt + 5000);
113
+ const updated = db.getChat(chat.id);
114
+ expect(updated!.updatedAt).toBeGreaterThan(originalUpdatedAt);
115
+ });
116
+
117
+ it('should get context messages within token budget', () => {
118
+ const chat = db.createChat('Test', 'webchat');
119
+ for (let i = 0; i < 20; i++) {
120
+ db.addMessage(chat.id, `msg-${i}`, 'user', 'x'.repeat(100), i * 1000);
121
+ }
122
+ const context = db.getContextMessages(chat.id, 100);
123
+ expect(context.length).toBeLessThan(20);
124
+ expect(context.length).toBeGreaterThan(0);
125
+ expect(context[context.length - 1].id).toBe('msg-19');
126
+ });
127
+ });
128
+
129
+ describe('session compatibility', () => {
130
+ it('should get or create a session-style chat by sender+channel', () => {
131
+ const chat1 = db.getOrCreateSessionChat('user123', 'telegram');
132
+ expect(chat1.id).toBeDefined();
133
+ const chat2 = db.getOrCreateSessionChat('user123', 'telegram');
134
+ expect(chat2.id).toBe(chat1.id);
135
+ const chat3 = db.getOrCreateSessionChat('user456', 'telegram');
136
+ expect(chat3.id).not.toBe(chat1.id);
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { SessionManager } from '../src/manager.js';
6
+
7
+ const testDir = path.join(os.tmpdir(), 'auxiora-migration-test-' + Date.now());
8
+
9
+ describe('JSON-to-SQLite migration', () => {
10
+ let manager: SessionManager;
11
+
12
+ beforeEach(() => {
13
+ fs.mkdirSync(testDir, { recursive: true });
14
+ });
15
+
16
+ afterEach(() => {
17
+ manager?.destroy();
18
+ fs.rmSync(testDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it('should migrate JSON session files to SQLite', async () => {
22
+ // Create a fake JSON session file
23
+ const sessionId = 'test-session-123';
24
+ const sessionData = {
25
+ id: sessionId,
26
+ messages: [
27
+ { id: 'msg-1', role: 'user', content: 'Hello', timestamp: 1000 },
28
+ { id: 'msg-2', role: 'assistant', content: 'Hi there!', timestamp: 2000 },
29
+ ],
30
+ metadata: {
31
+ channelType: 'telegram',
32
+ senderId: 'user456',
33
+ createdAt: 1000,
34
+ lastActiveAt: 2000,
35
+ },
36
+ };
37
+
38
+ fs.writeFileSync(
39
+ path.join(testDir, `${sessionId}.json`),
40
+ JSON.stringify(sessionData),
41
+ );
42
+
43
+ manager = new SessionManager({
44
+ maxContextTokens: 10000,
45
+ ttlMinutes: 60,
46
+ autoSave: true,
47
+ compactionEnabled: true,
48
+ dbPath: path.join(testDir, 'sessions.db'),
49
+ sessionsDir: testDir,
50
+ });
51
+
52
+ await manager.initialize();
53
+
54
+ // Verify session is now in SQLite
55
+ const session = await manager.get(sessionId);
56
+ expect(session).not.toBeNull();
57
+ expect(session!.messages).toHaveLength(2);
58
+ expect(session!.messages[0].content).toBe('Hello');
59
+ expect(session!.messages[1].content).toBe('Hi there!');
60
+ expect(session!.metadata.channelType).toBe('telegram');
61
+
62
+ // Verify JSON file moved to migrated/
63
+ expect(fs.existsSync(path.join(testDir, `${sessionId}.json`))).toBe(false);
64
+ expect(fs.existsSync(path.join(testDir, 'migrated', `${sessionId}.json`))).toBe(true);
65
+ });
66
+
67
+ it('should be a no-op when no JSON files exist', async () => {
68
+ manager = new SessionManager({
69
+ maxContextTokens: 10000,
70
+ ttlMinutes: 60,
71
+ autoSave: true,
72
+ compactionEnabled: true,
73
+ dbPath: path.join(testDir, 'sessions.db'),
74
+ sessionsDir: testDir,
75
+ });
76
+
77
+ await manager.initialize();
78
+
79
+ // Should not throw and no migrated/ dir created
80
+ expect(fs.existsSync(path.join(testDir, 'migrated'))).toBe(false);
81
+ });
82
+
83
+ it('should skip corrupt JSON files without crashing', async () => {
84
+ // Write a corrupt file and a valid file
85
+ fs.writeFileSync(path.join(testDir, 'corrupt.json'), '{invalid json!!!');
86
+ fs.writeFileSync(
87
+ path.join(testDir, 'valid-session.json'),
88
+ JSON.stringify({
89
+ id: 'valid-session',
90
+ messages: [{ id: 'msg-1', role: 'user', content: 'Works', timestamp: 1000 }],
91
+ metadata: { channelType: 'webchat', createdAt: 1000, lastActiveAt: 1000 },
92
+ }),
93
+ );
94
+
95
+ manager = new SessionManager({
96
+ maxContextTokens: 10000,
97
+ ttlMinutes: 60,
98
+ autoSave: true,
99
+ compactionEnabled: true,
100
+ dbPath: path.join(testDir, 'sessions.db'),
101
+ sessionsDir: testDir,
102
+ });
103
+
104
+ await manager.initialize();
105
+
106
+ // Valid session should be migrated
107
+ const session = await manager.get('valid-session');
108
+ expect(session).not.toBeNull();
109
+ expect(session!.messages).toHaveLength(1);
110
+
111
+ // Corrupt file should remain (not moved to migrated/)
112
+ expect(fs.existsSync(path.join(testDir, 'corrupt.json'))).toBe(true);
113
+ });
114
+ });