conduit/test/shared/widgets/model_list_tile_test.dart
cogwheel 4e73c9a0ea
Some checks are pending
L10n / l10n (push) Waiting to run
Fix ten reported issues and vendor Hermes Agent as an API reference (#725)
* Fix ten reported issues and vendor Hermes Agent as an API reference

Closes #693, #695, #699, #700, #701, #703, #708, #712, #716, #723.

- Hermes (#693, #699): load transcripts over REST even when session.resume
  fails, page at 200 messages and shrink on oversized responses, and show
  distinct errors for too-large, not-found, and RPC failures. Add the
  hermes-src submodule (NousResearch/hermes-agent v2026.9.11) as the RPC and
  REST contract reference.
- Server version banner (#695): dismissible, remembered per server and version.
- Model selector (#700): overflow fade and selection tint use the card surface.
- OpenRouter (#701): skip Batch API-only ":batch" model variants.
- Persisted chat content (#703): store plain output text instead of rendered
  <details> markup when structured output exists, matching the web client.
- Direct connections (#708): probes report their real failure instead of a
  generic message, the Open WebUI logout fence no longer blocks Direct setup,
  and 401/403 under the api-key header mode hints at Bearer auth.
- Reasoning effort (#712): per-server, per-model user picks override the
  model's configured default and are sent explicitly in the request.
- Voice calls (#716): stop the record plugin from clearing the Android
  communication device and verify the loudspeaker route before reporting it.
- Voice mode (#723): finalize speech on the responseDone marker.
- Add the five new strings to every locale.

* Address review: keep incomplete-clear block, pass effort on sends, keep all dismissals

- Direct profiles stay hidden and unusable after an incomplete full app-data
  clear, in memory and across restarts, until the logout fence is cleared by a
  completed cleanup or a new authenticated session. Ordinary failed sign-outs
  still do not block Direct setup.
- The primary Open WebUI send path now passes the resolved per-model reasoning
  effort like regeneration and queued completions already did.
- Server version warning dismissals are stored as a bounded set so dismissing
  one server no longer forgets another.

* Address review: release rejected speaker device, speak settled text on responseDone

- When Android accepts the built-in speaker but the read-back disagrees,
  clear that selection before the legacy setSpeakerphoneOn fallback, which is
  ignored while a communication device stays selected.
- Voice mode takes the final text from the message once responseDone is set;
  the streaming content provider can lag behind the flushed message.

* Address review: await the incomplete-clear restart marker before returning

The marker is written through the app-data-clear barrier with a checked write
and awaited by the clear coordinator, so a process death right after an
incomplete clear cannot reload the surviving Direct profiles on restart.

* Address review: arm the restart fence before the wipe and propagate marker failures

The incomplete-clear marker is now armed before any data is wiped, disarmed
only when the clear completes or yields ownership, re-asserted on an
incomplete outcome, and a failed checked write aborts the clear instead of
being swallowed. The in-memory block is unchanged.

* Do not raise an error banner for response.incomplete

Open WebUI forwards the provider's `response.incomplete` event over the
socket but treats it as a no-op: the output is kept and the turn still ends
with a normal `done`. Raising an error on that event flashed "The response
stopped before it was complete" under the answer until `done` cleared it.
`response.failed` still surfaces the provider error.
2026-09-15 00:48:03 +05:30

109 lines
3.4 KiB
Dart

import 'package:checks/checks.dart';
import 'package:conduit/core/models/model.dart';
import 'package:conduit/l10n/app_localizations.dart';
import 'package:conduit/l10n/conduit_localizations.dart';
import 'package:conduit/shared/theme/app_theme.dart';
import 'package:conduit/shared/theme/tweakcn_themes.dart';
import 'package:conduit/shared/theme/theme_extensions.dart';
import 'package:conduit/shared/widgets/conduit_components.dart';
import 'package:conduit/shared/widgets/horizontal_overflow_fade.dart';
import 'package:conduit/shared/widgets/model_list_tile.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
const _reasoningModel = Model(
id: 'reasoning-model',
name: 'Reasoning model',
supportedParameters: ['reasoning'],
);
Future<ConduitThemeExtension> _pumpTile(
WidgetTester tester, {
required bool isSelected,
}) async {
late ConduitThemeExtension theme;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(TweakcnThemes.conduit),
localizationsDelegates: conduitLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Builder(
builder: (context) {
theme = context.conduitTheme;
return ConduitCard(
child: ModelListTile(
model: _reasoningModel,
isSelected: isSelected,
onTap: () {},
),
);
},
),
),
),
);
return theme;
}
List<Color> _fadeColors(WidgetTester tester) {
final fade = find.byType(HorizontalOverflowFade);
final gradient = tester
.widgetList<DecoratedBox>(
find.descendant(of: fade, matching: find.byType(DecoratedBox)),
)
.map((box) => box.decoration)
.whereType<BoxDecoration>()
.map((decoration) => decoration.gradient)
.whereType<LinearGradient>()
.single;
return gradient.colors;
}
void main() {
testWidgets('unselected overflow fade derives from the card background', (
tester,
) async {
final theme = await _pumpTile(tester, isSelected: false);
final card = theme.cardBackground;
check(card).not((it) => it.equals(theme.surfaceBackground));
final colors = _fadeColors(tester);
check(colors)
.deepEquals([card.withValues(alpha: 0), card.withValues(alpha: 0.9)]);
});
testWidgets('selected overflow fade matches the highlighted row surface', (
tester,
) async {
final theme = await _pumpTile(tester, isSelected: true);
final highlighted = Color.alphaBlend(
theme.buttonPrimary.withValues(alpha: 0.1),
theme.cardBackground,
);
final surfaceHighlighted = Color.alphaBlend(
theme.buttonPrimary.withValues(alpha: 0.1),
theme.surfaceBackground,
);
check(highlighted).not((it) => it.equals(surfaceHighlighted));
final colors = _fadeColors(tester);
check(colors).deepEquals([
highlighted.withValues(alpha: 0),
highlighted.withValues(alpha: 0.9),
]);
// The highlight itself paints against the card surface.
final tile = find.byType(ModelListTile);
final containers = tester.widgetList<Container>(
find.descendant(of: tile, matching: find.byType(Container)),
);
final rowBackgrounds = containers
.map((container) => container.decoration)
.whereType<BoxDecoration>()
.map((decoration) => decoration.color);
check(rowBackgrounds).contains(highlighted);
});
}