chain-studio/test/friendly_error_test.dart
flemming-it 68d23ab7dd
Some checks are pending
Security / Security check (push) Waiting to run
refactor: rename app fai_studio -> chain_studio
Studio is the Ch∆In product's GUI, not a F∆I-vendor app. Rename the
Flutter package, all package: imports, and the build identity across
platforms: linux/windows CMake BINARY_NAME + project, Windows Runner.rc
fields, macOS PRODUCT_NAME / bundle id (ai.flemming.chain.chainStudio) /
.app + scheme BuildableName. Update the client-SDK + flow-editor deps to
their renamed chain_* packages (path + git URL). Company/copyright fields
now read Flemming.AI. flutter analyze: clean.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-06-16 17:48:11 +02:00

155 lines
5 KiB
Dart

// Unit tests for the `friendlyError` mapper. These check the
// duck-typed shape contract — `GrpcError` from package:grpc is
// expected to expose `.code` (int), `.message` (String?), and
// `.codeName` (String) — without taking grpc as a Studio
// dependency. The fakes here mimic that shape.
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chain_studio/data/friendly_error.dart';
import 'package:chain_studio/l10n/app_localizations.dart';
class _FakeGrpcError {
final int code;
final String? message;
final String codeName;
_FakeGrpcError(this.code, this.codeName, [this.message]);
}
Future<AppLocalizations> _loadL10n(Locale locale) async {
return await AppLocalizations.delegate.load(locale);
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
setUpAll(() async {
// Force loading the gen-l10n delegate before tests so each
// call to `_loadL10n` is fast.
await _loadL10n(const Locale('en'));
});
test('UNAVAILABLE maps to hub-not-reachable copy', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(_FakeGrpcError(14, 'UNAVAILABLE', 'connection refused'), l);
expect(r.headline, l.errUnavailable);
expect(r.hint, l.errUnavailableHint);
expect(r.detail, 'connection refused');
});
test('FAILED_PRECONDITION carries module detail in body', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(9, 'FAILED_PRECONDITION',
'install error: no store entry for system.approval'),
l,
);
expect(r.headline, l.errFailedPrecondition);
expect(r.hint, l.errFailedPreconditionHint);
expect(r.detail, contains('system.approval'));
});
test('NOT_FOUND has a recovery hint', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(_FakeGrpcError(5, 'NOT_FOUND', 'no such module'), l);
expect(r.headline, l.errNotFound);
expect(r.hint, isNotNull);
});
test('Unknown code falls back to codeName: message', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(_FakeGrpcError(99, 'CUSTOM_CODE', 'something'), l);
expect(r.headline, contains('CUSTOM_CODE'));
expect(r.headline, contains('something'));
expect(r.hint, isNull);
});
test('Non-gRPC error renders toString as headline', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(FormatException('bad url'), l);
expect(r.headline, contains('bad url'));
});
test('Locale switches the headline language', () async {
final lDe = await _loadL10n(const Locale('de'));
final r =
friendlyError(_FakeGrpcError(14, 'UNAVAILABLE', 'connection refused'), lDe);
expect(r.headline, lDe.errUnavailable);
final lEn = await _loadL10n(const Locale('en'));
expect(lDe.errUnavailable, isNot(equals(lEn.errUnavailable)));
});
// --- Hub-pattern-specific detection ---
// The mapper inspects the message DETAIL for hub patterns and
// returns a more specific FriendlyError than the gRPC code's
// default. These guards lock the matchers — a regression here
// would silently fall back to the generic gRPC-code copy.
test("approval rejection maps to specific headline", () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(
7,
'PERMISSION_DENIED',
"step 'ap1' rejected by alice: needs more detail",
),
l,
);
expect(r.headline, l.errApprovalRejected);
expect(r.hint, l.errApprovalRejectedHint);
});
test('approval timeout maps to specific headline', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(
4,
'DEADLINE_EXCEEDED',
"step 'ap1' timed out waiting for approval after 600s",
),
l,
);
expect(r.headline, l.errApprovalTimedOut);
});
test('output-too-large maps to specific headline', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(
8,
'RESOURCE_EXHAUSTED',
"step 'extract' produced 52428800 bytes of output, "
"exceeding the 10 MB cap",
),
l,
);
expect(r.headline, l.errOutputTooLarge);
});
test('host-service-not-declared maps to specific headline', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(
14,
'UNAVAILABLE',
"step 'summarise' requires service 'ollama' which is not declared "
"in operator config",
),
l,
);
expect(r.headline, l.errServiceUnavailableForStep);
});
test('missing value maps to specific headline', () async {
final l = await _loadL10n(const Locale('en'));
final r = friendlyError(
_FakeGrpcError(
3,
'INVALID_ARGUMENT',
r"step 'classify' references missing value '$inputs.text'",
),
l,
);
expect(r.headline, l.errMissingValue);
});
}