Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import 'package:omi/utils/l10n_extensions.dart';
import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart';
import 'package:omi/pages/conversation_detail/widgets/summarized_apps_sheet.dart';
import 'package:omi/pages/conversation_detail/widgets/template_creation_outcome.dart';
import 'package:omi/providers/app_provider.dart';
import 'package:omi/utils/alerts/app_snackbar.dart';
import 'package:omi/utils/logger.dart';
Expand Down Expand Up @@ -179,7 +180,14 @@
if (mounted) {
// Close the create template bottom sheet
Navigator.pop(context);
if (success) {
// Polarity comes from the tested classifier so a failed install
// can never be reported as success (#10074).
final outcome = success ? TemplateCreationOutcome.installed : TemplateCreationOutcome.installFailed;
if (templateCreationOutcomeIsError(outcome)) {
// The provider already showed the failure dialog; tell the user
// what state they are actually in.
AppSnackbar.showSnackbarError(context.l10n.failedToInstallApp(createdApp.name));
} else {
AppSnackbar.showSnackbarSuccess(context.l10n.appCreatedAndInstalled);

// Show the summarized apps sheet so user can use the new app
Expand All @@ -189,10 +197,6 @@
backgroundColor: Colors.transparent,
builder: (context) => const SummarizedAppsBottomSheet(),
);
} else {
// The provider already showed the failure dialog; tell the user
// what state they are actually in.
AppSnackbar.showSnackbarError(context.l10n.failedToInstallApp(createdApp.name));
}
}
} else if (mounted) {
Expand Down Expand Up @@ -222,7 +226,7 @@
}

@override
Widget build(BuildContext context) {

Check warning on line 229 in app/lib/pages/conversation_detail/widgets/create_template_bottom_sheet.dart

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

Widget build(BuildContext context) is 250 lines; consider extracting focused helpers over 150 lines.
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Container(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// Terminal outcomes of the quick-template creation flow, classified by whether
/// the user should see a success or an error message.
///
/// #10074/#10100: a template that was created on the server but whose install
/// did not stick must be surfaced as an error — never reported as a successful
/// install. The sheet derives its snackbar polarity from
/// [templateCreationOutcomeIsError] so that regression cannot be silently
/// reintroduced (it is the one place this decision lives, and it is tested).
enum TemplateCreationOutcome {
/// The create/submit call itself failed; nothing was created.
submitFailed,

/// App created and installed — full success.
installed,

/// App created but the install did not stick — must read as a failure.
installFailed,

/// App created; no install was attempted (its details could not be
/// fetched) — a partial success, not a failure.
createdWithoutInstall,
}

/// Whether [outcome] must be shown to the user as an error rather than success.
bool templateCreationOutcomeIsError(TemplateCreationOutcome outcome) {
switch (outcome) {
case TemplateCreationOutcome.submitFailed:
case TemplateCreationOutcome.installFailed:
return true;
case TemplateCreationOutcome.installed:
case TemplateCreationOutcome.createdWithoutInstall:
return false;
}
}
37 changes: 37 additions & 0 deletions app/test/unit/template_creation_outcome_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter_test/flutter_test.dart';

import 'package:omi/pages/conversation_detail/widgets/template_creation_outcome.dart';

/// #10074/#10100 regression pin: the quick-template sheet derives its
/// success/error snackbar polarity from templateCreationOutcomeIsError, so a
/// created-but-not-installed template can never again be reported as a
/// successful install. #10100 fixed the behavior but shipped without this pin
/// (the decision was inline in the widget, untestable without a full widget
/// harness); this closes that gap by making the decision a pure function.
void main() {
test('a created-but-failed install is an error, never a success (#10074)', () {
expect(templateCreationOutcomeIsError(TemplateCreationOutcome.installFailed), isTrue);
});

test('a full install is a success', () {
expect(templateCreationOutcomeIsError(TemplateCreationOutcome.installed), isFalse);
});

test('created-without-install is a partial success, not an error', () {
expect(templateCreationOutcomeIsError(TemplateCreationOutcome.createdWithoutInstall), isFalse);
});

test('a failed submit is an error', () {
expect(templateCreationOutcomeIsError(TemplateCreationOutcome.submitFailed), isTrue);
});

test('every outcome is classified (exhaustive, no unhandled case)', () {
for (final outcome in TemplateCreationOutcome.values) {
// Must not throw; the switch is exhaustive by construction.
templateCreationOutcomeIsError(outcome);
}
// Exactly the two failure outcomes read as errors.
final errors = TemplateCreationOutcome.values.where(templateCreationOutcomeIsError).toSet();
expect(errors, {TemplateCreationOutcome.submitFailed, TemplateCreationOutcome.installFailed});
});
}
Loading