Zion Boggan
repos/TreeTrace/test/adapters.test.js
zionboggan.com ↗
297 lines · javascript
History for this file →
1
import { test } from 'node:test';
2
import assert from 'node:assert/strict';
3
import { readFileSync } from 'node:fs';
4
import { fileURLToPath } from 'node:url';
5
import { dirname, join } from 'node:path';
6
 
7
import { adaptFrom, autoAdapt, TOOLS } from '../src/adapters/index.js';
8
import { classifyPrompts } from '../src/extract.js';
9
import { buildTree } from '../src/tree.js';
10
import { analyzeTree } from '../src/analyze.js';
11
import { detectChatGPT } from '../src/adapters/chatgpt.js';
12
import { detectCopilot } from '../src/adapters/copilot.js';
13
import { detectGeminiJson } from '../src/adapters/gemini.js';
14
import { detectCursor } from '../src/adapters/cursor.js';
15
import { detectGrok } from '../src/adapters/grok.js';
16
 
17
const DIR = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'adapters');
18
const fx = (name) => join(DIR, name);
19
const read = (name) => readFileSync(fx(name), 'utf8');
20
 
21
function pipeline(sessions) {
22
  const nodes = classifyPrompts(sessions);
23
  return { nodes, tree: buildTree(sessions, nodes) };
24
}
25
 
26
test('codex: parses real rollout JSONL into human prompts', () => {
27
  const sessions = adaptFrom('codex', read('codex-session.jsonl'), fx('codex-session.jsonl'));
28
  assert.equal(sessions.length, 1);
29
  const s = sessions[0];
30
  assert.equal(s.prompts.length, 3);
31
  assert.ok(s.prompts.every((p) => !p.text.startsWith('<environment_context>')));
32
  assert.ok(s.prompts[0].text.includes('/version subcommand'));
33
  assert.equal(s.stats.assistantLines, 3);
34
  assert.ok(s.stats.toolUses >= 1);
35
  assert.equal(s.stats.inputTokens, 5200);
36
  const { tree } = pipeline(sessions);
37
  assert.equal(tree.roots.length, 1);
38
  assert.equal(tree.stats.promptCount, 3);
39
});
40
 
41
test('codex: auto-detected from JSONL shape', () => {
42
  const detected = autoAdapt(read('codex-session.jsonl'), fx('codex-session.jsonl'));
43
  assert.ok(detected);
44
  assert.equal(detected.tool, 'codex');
45
  assert.equal(detected.sessions[0].prompts.length, 3);
46
});
47
 
48
test('chatgpt: parses export mapping, walks user turns, detects model', () => {
49
  const sessions = adaptFrom('chatgpt', read('chatgpt-conversations.json'), fx('chatgpt-conversations.json'));
50
  assert.equal(sessions.length, 1);
51
  const s = sessions[0];
52
  assert.equal(s.prompts.length, 1);
53
  assert.equal(s.title, 'Debounce a search input in React');
54
  assert.ok(s.stats.models.includes('gpt-4o'));
55
  assert.ok(s.stats.assistantLines >= 1);
56
  assert.ok(s.stats.toolUses >= 1);
57
});
58
 
59
test('chatgpt: auto-detected from mapping shape', () => {
60
  const detected = autoAdapt(read('chatgpt-conversations.json'), fx('chatgpt-conversations.json'));
61
  assert.ok(detected);
62
  assert.equal(detected.tool, 'chatgpt');
63
});
64
 
65
test('gemini: parses ChatRecordingService session, tools and tokens', () => {
66
  const sessions = adaptFrom('gemini', read('gemini-session.json'), fx('gemini-session.json'));
67
  const s = sessions[0];
68
  assert.equal(s.prompts.length, 3);
69
  assert.ok(s.prompts[0].text.includes('health-check'));
70
  assert.equal(s.stats.assistantLines, 3);
71
  assert.ok(s.stats.toolUses >= 1);
72
  assert.ok(s.stats.filesTouched.includes('src/server/health.ts'));
73
  assert.ok(s.stats.models.includes('gemini-3-flash-preview'));
74
});
75
 
76
test('gemini: auto-detected from session JSON', () => {
77
  const detected = autoAdapt(read('gemini-session.json'), fx('gemini-session.json'));
78
  assert.ok(detected);
79
  assert.equal(detected.tool, 'gemini');
80
  assert.equal(detected.sessions[0].prompts.length, 3);
81
});
82
 
83
test('copilot: parses requests[] into prompts, counts tool invocations', () => {
84
  const sessions = adaptFrom('copilot', read('copilot-chatsession.json'), fx('copilot-chatsession.json'));
85
  const s = sessions[0];
86
  assert.equal(s.prompts.length, 5);
87
  assert.ok(s.prompts[0].text.toLowerCase().includes('html'));
88
  assert.equal(s.stats.assistantLines, 5);
89
});
90
 
91
test('copilot: auto-detected from requesterUsername/requests', () => {
92
  const detected = autoAdapt(read('copilot-chatsession.json'), fx('copilot-chatsession.json'));
93
  assert.ok(detected);
94
  assert.equal(detected.tool, 'copilot');
95
});
96
 
97
test('cursor: parses exported session messages, model and files', () => {
98
  const sessions = adaptFrom('cursor', read('cursor-export.json'), fx('cursor-export.json'));
99
  const s = sessions[0];
100
  assert.equal(s.prompts.length, 3);
101
  assert.equal(s.title, 'Add pagination to the users table');
102
  assert.ok(s.stats.models.includes('claude-3.5-sonnet'));
103
  assert.ok(s.stats.toolUses >= 1);
104
  assert.ok(s.stats.filesTouched.some((f) => f.endsWith('Users.tsx')));
105
});
106
 
107
test('cursor: auto-detected from exported session shape', () => {
108
  const detected = autoAdapt(read('cursor-export.json'), fx('cursor-export.json'));
109
  assert.ok(detected);
110
  assert.equal(detected.tool, 'cursor');
111
});
112
 
113
test('grok: parses conversation[] role/content into prompts', () => {
114
  const sessions = adaptFrom('grok', read('grok-session.json'), fx('grok-session.json'));
115
  const s = sessions[0];
116
  assert.equal(s.prompts.length, 3);
117
  assert.ok(s.prompts[0].text.includes('Fibonacci'));
118
  assert.ok(s.stats.models.includes('grok-4'));
119
  assert.equal(s.stats.assistantLines, 2);
120
});
121
 
122
test('grok: auto-detected from conversation messages', () => {
123
  const detected = autoAdapt(read('grok-session.json'), fx('grok-session.json'));
124
  assert.ok(detected);
125
  assert.equal(detected.tool, 'grok');
126
});
127
 
128
test('adapter output flows through the full classify/tree pipeline', () => {
129
  for (const name of ['codex-session.jsonl', 'gemini-session.json', 'cursor-export.json', 'grok-session.json']) {
130
    const text = read(name);
131
    const detected = autoAdapt(text, fx(name));
132
    assert.ok(detected, `no detection for ${name}`);
133
    const { tree } = pipeline(detected.sessions);
134
    assert.ok(tree.nodes.length >= 1, `no nodes for ${name}`);
135
    assert.equal(tree.nodes[0].kind, 'root', `first node not root for ${name}`);
136
    assert.ok(tree.roots.length >= 1);
137
  }
138
});
139
 
140
test('detectors: exactly one JSON detector fires per fixture (no cursor/grok overlap)', () => {
141
  const jsonDetectors = {
142
    chatgpt: detectChatGPT,
143
    copilot: detectCopilot,
144
    gemini: detectGeminiJson,
145
    cursor: detectCursor,
146
    grok: detectGrok,
147
  };
148
  const expected = {
149
    'chatgpt-conversations.json': 'chatgpt',
150
    'copilot-chatsession.json': 'copilot',
151
    'gemini-session.json': 'gemini',
152
    'cursor-export.json': 'cursor',
153
    'grok-session.json': 'grok',
154
  };
155
  for (const [name, want] of Object.entries(expected)) {
156
    const parsed = JSON.parse(read(name));
157
    const fired = Object.entries(jsonDetectors).filter(([, fn]) => fn(parsed)).map(([k]) => k);
158
    assert.deepEqual(fired, [want], `${name} should fire exactly [${want}], got [${fired.join(', ')}]`);
159
  }
160
});
161
 
162
test('detectGrok requires a grok-specific signal, not just role/content messages', () => {
163
  const generic = { messages: [{ role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }] };
164
  assert.equal(detectGrok(generic), false, 'generic role/content dump must not be claimed by grok');
165
  assert.equal(detectGrok({ model: 'grok-4', messages: generic.messages }), true, 'a grok model marker should be detected');
166
});
167
 
168
test('copilot and cursor adapters handle null/primitive JSON without throwing', () => {
169
  for (const bad of ['null', '42', 'true', '"hi"']) {
170
    assert.doesNotThrow(() => {
171
      const c = adaptFrom('copilot', bad, fx('bad.json'));
172
      assert.equal(c[0].prompts.length, 0);
173
    }, `copilot threw on ${bad}`);
174
    assert.doesNotThrow(() => {
175
      const c = adaptFrom('cursor', bad, fx('bad.json'));
176
      assert.equal(c[0].prompts.length, 0);
177
    }, `cursor threw on ${bad}`);
178
  }
179
});
180
 
181
test('adaptFrom rejects an unknown tool name', () => {
182
  assert.throws(() => adaptFrom('notatool', '{}', 'x.json'), /unknown/);
183
  assert.ok(TOOLS.includes('codex') && TOOLS.includes('cursor'));
184
});
185
 
186
test('codex import emits actions that drive a verified security signal and model attribution', () => {
187
  const jsonl = [
188
    { type: 'session_meta', timestamp: '2026-06-12T10:00:00Z', payload: { id: 'cdx1', originator: 'codex_cli_rs', cwd: '/repo', cli_version: '0.139.0' } },
189
    { type: 'turn_context', timestamp: '2026-06-12T10:00:01Z', payload: { model: 'gpt-5.5', cwd: '/repo' } },
190
    { type: 'response_item', timestamp: '2026-06-12T10:00:02Z', payload: { type: 'message', role: 'user', content: [{ type: 'text', text: 'Add rate limiting to the checkout endpoint' }] } },
191
    { type: 'response_item', timestamp: '2026-06-12T10:00:03Z', payload: { type: 'reasoning', summary: [] } },
192
    { type: 'response_item', timestamp: '2026-06-12T10:00:05Z', payload: { type: 'function_call', name: 'apply_patch', arguments: JSON.stringify({ path: 'src/auth/session.ts' }), call_id: 'c1' } },
193
    { type: 'response_item', timestamp: '2026-06-12T10:00:06Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'Edited session.ts' }] } },
194
  ].map((r) => JSON.stringify(r)).join('\n');
195
 
196
  const sessions = adaptFrom('codex', jsonl, fx('codex-synth.jsonl'));
197
  const s = sessions[0];
198
  assert.equal(s.prompts[0].actions.length, 1);
199
  assert.equal(s.prompts[0].actions[0].file, 'src/auth/session.ts');
200
  assert.equal(s.prompts[0].actions[0].model, 'gpt-5.5');
201
  assert.equal(s.prompts[0].thinking, 1);
202
 
203
  const { tree } = pipeline(sessions);
204
  const analysis = analyzeTree(tree);
205
  const sec = analysis.failures.find((f) => f.type === 'security_or_privacy_risk' && f.tier === 'verified');
206
  assert.ok(sec, 'a codex import should now produce a verified security signal');
207
  assert.equal(sec.model, 'gpt-5.5');
208
  assert.deepEqual(analysis.summary.models, ['gpt-5.5']);
209
  assert.ok(analysis.summary.thinkingBlocks >= 1);
210
});
211
 
212
test('gemini import emits actions for a verified security signal', () => {
213
  const obj = {
214
    sessionId: 'g1',
215
    messages: [
216
      { type: 'user', content: [{ text: 'Add rate limiting to checkout' }], timestamp: '2026-06-12T10:00:00Z' },
217
      { type: 'gemini', model: 'gemini-3-flash', timestamp: '2026-06-12T10:00:05Z', toolCalls: [{ name: 'edit_file', args: { file_path: 'src/auth/middleware.ts' } }], thoughts: [{ subject: 'a', description: 'b' }] },
218
    ],
219
  };
220
  const sessions = adaptFrom('gemini', JSON.stringify(obj), fx('gemini-synth.json'));
221
  const analysis = analyzeTree(pipeline(sessions).tree);
222
  const sec = analysis.failures.find((f) => f.type === 'security_or_privacy_risk' && f.tier === 'verified');
223
  assert.ok(sec, 'gemini import should produce a verified security signal');
224
  assert.equal(sec.model, 'gemini-3-flash');
225
  assert.ok(analysis.summary.thinkingBlocks >= 1);
226
});
227
 
228
test('copilot import emits actions from toolSpecificData for a verified security signal', () => {
229
  const obj = {
230
    version: 3,
231
    requests: [
232
      {
233
        requestId: 'r1',
234
        message: { text: 'Add rate limiting to checkout' },
235
        result: { metadata: { modelId: 'gpt-4o-copilot' } },
236
        response: [{ kind: 'toolInvocationSerialized', toolId: 'copilot_editFile', toolSpecificData: { uri: { path: 'src/auth/session.ts' } } }],
237
      },
238
    ],
239
  };
240
  const sessions = adaptFrom('copilot', JSON.stringify(obj), fx('copilot-synth.json'));
241
  const analysis = analyzeTree(pipeline(sessions).tree);
242
  const sec = analysis.failures.find((f) => f.type === 'security_or_privacy_risk' && f.tier === 'verified');
243
  assert.ok(sec, 'copilot import should produce a verified security signal');
244
  assert.equal(sec.model, 'gpt-4o-copilot');
245
});
246
 
247
test('cursor import emits actions from exported tool calls for a verified security signal', () => {
248
  const obj = {
249
    id: 'cur1',
250
    title: 'session',
251
    workspaceId: 'w1',
252
    messages: [
253
      { role: 'user', content: 'Add rate limiting to checkout', timestamp: '2026-06-12T10:00:00Z' },
254
      { role: 'assistant', model: 'claude-sonnet-4-6', toolCalls: [{ name: 'edit_file', filePath: 'src/auth/session.ts' }] },
255
    ],
256
  };
257
  const sessions = adaptFrom('cursor', JSON.stringify(obj), fx('cursor-synth.json'));
258
  const analysis = analyzeTree(pipeline(sessions).tree);
259
  const sec = analysis.failures.find((f) => f.type === 'security_or_privacy_risk' && f.tier === 'verified');
260
  assert.ok(sec, 'cursor import should produce a verified security signal');
261
  assert.equal(sec.model, 'claude-sonnet-4-6');
262
});
263
 
264
test('adapters capture an assistant refusal as model_refusal', () => {
265
  const gem = JSON.stringify({ sessionId: 'g1', messages: [
266
    { type: 'user', content: '[disallowed ask]' },
267
    { type: 'gemini', content: [{ text: "I'm sorry, I cannot help with that." }], model: 'gemini-3' },
268
  ] });
269
  assert.equal(adaptFrom('gemini', gem, 'g.json')[0].stats.rejectionsByKind.model_refusal, 1, 'gemini');
270
 
271
  const cdx = [
272
    JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: '[disallowed ask]' } }),
273
    JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'I cannot help with that request.' }] } }),
274
  ].join('\n');
275
  assert.equal(adaptFrom('codex', cdx, 'c.jsonl')[0].stats.rejectionsByKind.model_refusal, 1, 'codex');
276
 
277
  const cg = JSON.stringify([{ title: 't', mapping: {
278
    a: { id: 'a', message: { author: { role: 'user' }, content: { content_type: 'text', parts: ['[disallowed ask]'] }, create_time: 1 } },
279
    b: { id: 'b', message: { author: { role: 'assistant' }, content: { content_type: 'text', parts: ["I'm sorry, I can't help with that."] }, create_time: 2 } },
280
  } }]);
281
  assert.equal(adaptFrom('chatgpt', cg, 'x.json')[0].stats.rejectionsByKind.model_refusal, 1, 'chatgpt');
282
 
283
  const cur = JSON.stringify({ messages: [
284
    { role: 'user', content: '[disallowed ask]' },
285
    { role: 'assistant', content: 'I cannot help with that.', model: 'claude-3.5' },
286
  ], workspaceId: 'w' });
287
  assert.equal(adaptFrom('cursor', cur, 'cur.json')[0].stats.rejectionsByKind.model_refusal, 1, 'cursor');
288
});
289
 
290
test('a benign assistant turn produces no false refusal', () => {
291
  const gem = JSON.stringify({ sessionId: 'g2', messages: [
292
    { type: 'user', content: 'help me write a function' },
293
    { type: 'gemini', content: [{ text: 'Sure, here is a function that does that.' }], model: 'gemini-3' },
294
  ] });
295
  const s = adaptFrom('gemini', gem, 'g2.json')[0];
296
  assert.equal(s.stats.rejections, 0, 'no false positive on a helpful answer');
297
});