Files
clide/test/builtin/claude/session_index_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

157 lines
5.6 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:clide/builtin/claude/src/session_index.dart';
import 'package:test/test.dart';
String userLine(String text) => jsonEncode({
'type': 'user',
'message': {'role': 'user', 'content': text},
});
String userBlocksLine(String text) => jsonEncode({
'type': 'user',
'message': {
'role': 'user',
'content': [
{'type': 'text', 'text': text},
],
},
});
String toolResultLine() => jsonEncode({
'type': 'user',
'message': {
'role': 'user',
'content': [
{'type': 'tool_result', 'content': 'output'},
],
},
});
String assistantLine(String text) => jsonEncode({
'type': 'assistant',
'message': {
'role': 'assistant',
'content': [
{'type': 'text', 'text': text},
],
},
});
void main() {
group('userText', () {
test('extracts string and text-block content', () {
expect(userText(jsonDecode(userLine('hello')) as Map<String, Object?>), 'hello');
expect(userText(jsonDecode(userBlocksLine('blocky')) as Map<String, Object?>), 'blocky');
});
test('ignores tool-result-only user records and non-user records', () {
expect(userText(jsonDecode(toolResultLine()) as Map<String, Object?>), isNull);
expect(userText(jsonDecode(assistantLine('hi')) as Map<String, Object?>), isNull);
});
});
group('listSessions', () {
late Directory dir;
setUp(() async => dir = await Directory.systemTemp.createTemp('session_index_test'));
tearDown(() async {
if (await dir.exists()) await dir.delete(recursive: true);
});
Future<void> writeSession(String id, List<String> lines) async {
await File('${dir.path}/$id.jsonl').writeAsString('${lines.join('\n')}\n');
}
test('empty / missing dir yields no sessions', () async {
expect(await listSessions(dir), isEmpty);
expect(await listSessions(Directory('${dir.path}/nope')), isEmpty);
});
test('summarises each session with first … last bookends', () async {
await writeSession('aaaa', [userLine('start the swallow'), assistantLine('ok'), toolResultLine(), userLine('now the peacock')]);
final sessions = await listSessions(dir);
expect(sessions, hasLength(1));
expect(sessions.single.id, 'aaaa');
expect(sessions.single.firstUser, 'start the swallow');
expect(sessions.single.lastUser, 'now the peacock');
expect(sessions.single.label, 'start the swallow … now the peacock');
});
test('a single-prompt session labels without an ellipsis', () async {
await writeSession('bbbb', [userLine('only one'), assistantLine('reply')]);
expect((await listSessions(dir)).single.label, 'only one');
});
test('orders most-recently-modified first', () async {
await writeSession('old', [userLine('older')]);
await Future<void>.delayed(const Duration(milliseconds: 50));
await writeSession('new', [userLine('newer')]);
final sessions = await listSessions(dir);
expect(sessions.map((s) => s.id), ['new', 'old']);
// Defensive: timestamps are non-increasing regardless of FS granularity.
for (var i = 1; i < sessions.length; i++) {
expect(sessions[i - 1].modified.isBefore(sessions[i].modified), isFalse);
}
});
test('reads the last user prompt from the tail of a large transcript', () async {
final lines = [
userLine('the very first prompt'),
for (var i = 0; i < 50; i++) assistantLine('filler line number $i to push past the window'),
userLine('the very last prompt'),
];
await writeSession('big', lines);
// Tiny window forces the head/tail split path.
final sessions = await listSessions(dir, window: 256);
expect(sessions.single.firstUser, 'the very first prompt');
expect(sessions.single.lastUser, 'the very last prompt');
});
test('sizeBytes folds the transcript and its subagents dir (T-148)', () async {
final main = '${userLine('hi')}\n';
await File('${dir.path}/sz.jsonl').writeAsString(main);
final subDir = Directory('${dir.path}/sz/subagents')..createSync(recursive: true);
const subBody = 'agent transcript bytes';
await File('${subDir.path}/agent-1.jsonl').writeAsString(subBody);
final s = (await listSessions(dir)).single;
expect(s.sizeBytes, main.length + subBody.length);
});
});
group('deleteSession', () {
late Directory dir;
setUp(() async => dir = await Directory.systemTemp.createTemp('session_del_test'));
tearDown(() async {
if (await dir.exists()) await dir.delete(recursive: true);
});
test('removes the transcript and its subagents dir', () async {
await File('${dir.path}/del.jsonl').writeAsString('x');
Directory('${dir.path}/del/subagents').createSync(recursive: true);
await File('${dir.path}/del/subagents/a.jsonl').writeAsString('y');
await deleteSession(dir, 'del');
expect(File('${dir.path}/del.jsonl').existsSync(), isFalse);
expect(Directory('${dir.path}/del').existsSync(), isFalse);
});
test('rejects ids that could escape the dir', () async {
expect(() => deleteSession(dir, '../evil'), throwsArgumentError);
expect(() => deleteSession(dir, 'a/b'), throwsArgumentError);
expect(() => deleteSession(dir, ''), throwsArgumentError);
});
});
group('formatBytes', () {
test('scales B / KB / MB / GB', () {
expect(formatBytes(0), '0 B');
expect(formatBytes(512), '512 B');
expect(formatBytes(2048), '2 KB');
expect(formatBytes((1.5 * 1024 * 1024).round()), '1.5 MB');
expect(formatBytes((2 * 1024 * 1024 * 1024).round()), '2.0 GB');
});
});
}