Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
710fbeaaeThis file is pinned to the selected release. Download the original to open it in your design tool.
// Settings ▸ Debug. What matters here is the pane's job as a *reader*: the
// newest line first, a way to narrow five hundred rows to the one that failed,
// and two different empty states — nothing logged yet, and a filter that hid
// everything — since a screen that renders those the same is the screen that
// sends somebody looking for a bug in the log stack.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:harness/core/harness_cli_runner.dart';
import 'package:harness/logging/app_log.dart';
import 'package:harness/logging/log_stream.dart';
import 'package:harness/settings/sections/debug_detail_dialog.dart';
import 'package:harness/settings/sections/debug_filter_bar.dart';
import 'package:harness/settings/sections/debug_log_tile.dart';
import 'package:harness/settings/sections/debug_paths_card.dart';
import 'package:harness/settings/sections/debug_section.dart';
import 'package:harness/settings/settings_section.dart';
import 'package:harness/shared/theme/app_theme.dart';
import 'package:harness/shared/widgets/empty_state.dart';
/// The probe every test here passes: the real one reads `~/.harness`, which no
/// widget test may do.
Future<DebugEnvironment> _fakeProbe() async => const DebugEnvironment(
harnessCommand: '/tmp/node /tmp/cli.js',
harnessSource: HarnessCliSource.managed,
logsDirectory: '/tmp/logs',
);
void main() {
Future<void> pumpDebug(WidgetTester tester, LogStream stream) async {
tester.view.physicalSize = const Size(1000 * 2, 760 * 2);
tester.view.devicePixelRatio = 2;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(brightness: Brightness.light),
home: Builder(
builder: (context) {
AppTheme.brightness.value = Brightness.light;
return BrightnessScope(
child: Scaffold(
body: DebugSection(stream: stream, probe: _fakeProbe),
),
);
},
),
),
);
await tester.pumpAndSettle();
}
testWidgets('lists what was logged, newest first', (tester) async {
final stream = LogStream()
..add(AppLogLevel.info, 'app', 'launched')
..add(AppLogLevel.debug, 'ws', '→ agent_create {engine: codex}');
await pumpDebug(tester, stream);
final tiles = tester.widgetList<DebugLogTile>(find.byType(DebugLogTile));
expect(tiles.map((tile) => tile.entry.message), [
'→ agent_create {engine: codex}',
'launched',
]);
expect(find.text('2 entries'), findsOneWidget);
});
testWidgets('the Failed lens keeps the failures and the warnings', (
tester,
) async {
final stream = LogStream()
..add(AppLogLevel.debug, 'ws', '→ agent_create')
..add(AppLogLevel.warn, 'ws', '← agent_create failed')
..add(AppLogLevel.error, 'api', 'GET /api/machines → failed');
await pumpDebug(tester, stream);
await tester.tap(find.text('Failed'));
await tester.pumpAndSettle();
final tiles = tester.widgetList<DebugLogTile>(find.byType(DebugLogTile));
expect(tiles.map((tile) => tile.entry.message), [
'GET /api/machines → failed',
'← agent_create failed',
]);
});
testWidgets('a category lens narrows to that category', (tester) async {
final stream = LogStream()
..add(AppLogLevel.info, 'cli', 'harness auth status --json')
..add(AppLogLevel.debug, 'ws', '→ agents_list');
await pumpDebug(tester, stream);
// The lens, not the `cli` a row prints in its own meta column.
await tester.tap(
find.descendant(
of: find.byType(DebugFilterBar),
matching: find.text('cli'),
),
);
await tester.pumpAndSettle();
expect(find.byType(DebugLogTile), findsOneWidget);
expect(find.textContaining('harness auth status'), findsOneWidget);
});
testWidgets('search narrows on the message, and says when nothing matches', (
tester,
) async {
final stream = LogStream()
..add(AppLogLevel.info, 'app', 'launched')
..add(AppLogLevel.debug, 'ws', '→ agents_list');
await pumpDebug(tester, stream);
await tester.enterText(find.byType(TextField), 'agents');
await tester.pumpAndSettle();
expect(find.byType(DebugLogTile), findsOneWidget);
await tester.enterText(find.byType(TextField), 'nothing like this');
await tester.pumpAndSettle();
expect(find.text('No matches'), findsOneWidget);
});
testWidgets('an empty log says nothing has been logged, not "no matches"', (
tester,
) async {
await pumpDebug(tester, LogStream());
expect(find.text('Nothing logged yet'), findsOneWidget);
expect(find.byType(EmptyState), findsOneWidget);
});
testWidgets('Clear empties the list, and is dead while it is empty', (
tester,
) async {
final stream = LogStream()..add(AppLogLevel.info, 'app', 'launched');
await pumpDebug(tester, stream);
await tester.tap(find.text('Clear'));
await tester.pumpAndSettle();
expect(stream.entries, isEmpty);
expect(find.byType(DebugLogTile), findsNothing);
// Pressing it again is a no-op rather than an error.
await tester.tap(find.text('Clear'));
await tester.pumpAndSettle();
expect(find.text('0 entries'), findsOneWidget);
});
testWidgets('a row opens the transcript the line could not hold', (
tester,
) async {
final stream = LogStream();
final id = stream.add(
AppLogLevel.info,
'cli',
'harness link list',
command: LogCommand(),
);
stream.appendOutput(id, 'one link');
stream.finish(id, exitCode: 0, duration: const Duration(milliseconds: 40));
await pumpDebug(tester, stream);
await tester.tap(find.byType(DebugLogTile));
await tester.pumpAndSettle();
expect(find.byType(Dialog), findsOneWidget);
expect(find.textContaining('one link'), findsOneWidget);
});
group('the copied text', () {
test('carries the command, how it ended, and its output', () {
final stream = LogStream();
final id = stream.add(
AppLogLevel.info,
'cli',
'harness link create',
command: LogCommand(),
);
stream.appendOutput(id, 'joined');
stream.finish(id, exitCode: 1, duration: const Duration(seconds: 2));
final text = debugEntryAsText(stream.entries.single);
expect(text, contains('harness link create'));
expect(text, contains('FAILED exit=1 (2s)'));
expect(text, contains('joined'));
});
});
group('the section itself', () {
test('is listed where the debug surface is on, which a test build is', () {
final sections = [for (final group in settingsGroups) ...group.sections];
expect(sections, contains(SettingsSection.debug));
});
});
}