pipe/test/fixtures/stuck_loading/lib/notifier_gaps.dart
trvon 3c91db2946
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
fix: act on the external audit - 5 defects, 5 vacuous tests, a real fixture exclusion bug
An independent Gemini 3.8 Flash audit of this repo found real problems. Each was
reproduced before being fixed.

Confirmed defects:
- tool/adjudicate.py crashed with KeyError once R001 started grouping variants
  under 'variants' instead of 'variant'. Fixing that exposed a second bug of my
  own: it then divided verified VARIANTS by FINDINGS and reported 216% precision.
  Counts are now coherent (findings vs variant-level checks) and a clean repo
  reports 0/0 rather than a fake 100%.
- GuideCommand called _readPolicy but never registered --policy, so
   exited 64 despite the README documenting it.
- loading-flag-not-reset treated a constructor-invoked method as inheriting the
  loading flag without checking flag.defaultsTrue, so a default-FALSE flag was
  reported as in flight.
- Finding.fingerprint embedded the line number for rules whose only identity was
  , so moving code broke baseline entries.  is now a
  discriminating key; only loading-not-reset, which can emit two findings from one
  method, carries an explicit catchLine.
- suggest grouped unrelated provider-read-in-build findings by file; the group key,
  label and target list now use valueType/call.

Recall defect found while checking the auditor's own claim: UI variant mentions
were collected project-wide by unqualified name, so ExportSuccess - which appears
in FOUR unions on nuwayze - was cross-credited. Mentions are now scoped per union
to files that also name the state type or its owner, which surfaced
ArtistStoreManagementState[ExportSuccess] as a real finding.

Five negative controls could not fail (an event that is never handled can never be
reported as unhandled; a union with no render sites can never be reported as
rendered-not-emitted). Added fixture cases that make each one bite: HandledTap
(handled AND dispatched), PatternState render sites, GuardedCubit (guarded loading
path), QuietCubit._warmUp (constructor-invoked await on a default-false flag).

Root cause of a recurring mystery: this repo EXCLUDED test/fixtures/** from
analysis, which made any analyzer running in the repo context parse the fixture
without resolving its imports and report phantom undefined-class errors. I had
dismissed these as stale cache several times; they were my own exclusion. The
exclude is gone and the fixture governs itself. A new test asserts the fixture has
zero analyzer errors, not merely that it resolves, so IR distortion cannot hide.

84 tests (was 81).
2026-09-20 20:45:58 -06:00

157 lines
3.6 KiB
Dart

import 'bloc.dart';
import 'notifier.dart';
/// BAD: `bumpWithoutNotifying` mutates state and never notifies.
class StaleNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void bumpWithoutNotifying() {
_count = _count + 1;
}
/// GOOD: notifies directly.
void bump() {
_count = _count + 1;
notifyListeners();
}
/// GOOD: mutates, but a notifying method calls it, so the caller announces
/// the change.
void setQuiet(int next) {
_count = next;
}
void setAndAnnounce(int next) {
setQuiet(next);
notifyListeners();
}
}
/// BAD: consumed by a Consumer but has no notifying path at all, so that
/// Consumer can never rebuild.
class InertNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void setValue(int next) {
_value = next;
}
}
/// GOOD: consumed and it notifies.
class LiveNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void setValue(int next) {
_value = next;
notifyListeners();
}
}
/// Consumed by the UI, but `unusedFlag` is read nowhere: dead state the user can
/// never see, while `value` is displayed.
class PartiallyReadNotifier extends ChangeNotifier {
int _value = 0;
bool _unusedFlag = false;
int get value => _value;
bool get unusedFlag => _unusedFlag;
void setValue(int next) {
_value = next;
notifyListeners();
}
}
/// A union whose variants are matched ONLY through typed variable patterns
/// (`case PatternA value:`), which an object-pattern-only matcher misses.
abstract class PatternState {
const PatternState();
}
class PatternA extends PatternState {
const PatternA();
}
class PatternB extends PatternState {
const PatternB();
}
class PatternEvent {}
class PatternBloc extends Bloc<PatternEvent, PatternState> {
PatternBloc() : super(const PatternA());
void go() => emit(const PatternB());
}
/// A loading variant emitted on a path whose catch DOES resolve it. The negative
/// control for `loading-not-reset`: reporting every loading-emit-then-await would
/// flag this cubit, and it must stay silent.
abstract class GuardedState {
const GuardedState();
}
class GuardedLoading extends GuardedState {
const GuardedLoading();
}
class GuardedReady extends GuardedState {
const GuardedReady();
}
class GuardedCubit extends Bloc<PatternEvent, GuardedState> {
GuardedCubit() : super(const GuardedReady());
Future<void> load() async {
emit(const GuardedLoading());
try {
await Future<void>.delayed(Duration.zero);
emit(const GuardedReady());
} catch (error) {
emit(const GuardedReady());
}
}
}
/// A flag that defaults to FALSE, awaited from the constructor. Nothing is in
/// flight, so `loading-flag-not-reset` must stay silent - the negative control
/// for that rule, and the regression guard for the missing `defaultsTrue` check.
class QuietState {
const QuietState({this.loading = false, this.saved = false});
final bool loading;
final bool saved;
QuietState copyWith({bool? loading, bool? saved}) => QuietState(
loading: loading ?? this.loading,
saved: saved ?? this.saved,
);
}
class QuietCubit extends Bloc<PatternEvent, QuietState> {
QuietCubit() : super(const QuietState()) {
_warmUp();
}
Future<void> _warmUp() async {
await Future<void>.delayed(Duration.zero);
}
/// Sets the flag in flight and resolves it on every path.
Future<void> save() async {
emit(state.copyWith(loading: true));
try {
await Future<void>.delayed(Duration.zero);
emit(state.copyWith(loading: false, saved: true));
} catch (error) {
emit(state.copyWith(loading: false));
}
}
}