mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 15:08:28 +01:00
The geofence needs HOME_LAT/HOME_LON, but those say where I live, so the committed policy asset is generated with --redact-home and the real values are pushed to app-private storage instead. push_home_location.sh greps config_secrets.sh rather than sourcing it -- that file holds other secrets and this script has no business evaluating them -- and refuses the shipped REDACTED_* placeholder with a message naming the file to fill in, rather than writing whatever the parse happened to land on. Private storage was chosen for its lifetime as much as its permissions: it does not survive an uninstall, so reinstalling forces re-provisioning instead of silently inheriting a stale home. The default umask left the file 666, so the script chmods it 600; the shell user gets Permission denied, and the /data/local/tmp staging copy is removed after the transfer. Every read failure returns null rather than throwing. Null reaches the enforcement layer as location-unknown, which already fails closed into enforcing -- so a corrupt coordinates file cannot become an unlock, and cannot crash the service either. Workout detection replaces a root-only SQLite read of StrongLifts' private database. The manual source is time-boxed rather than a plain toggle, because a workout mode left on by accident is indistinguishable from having no exception at all. The guarded wrapper catches broadly and times out: nothing about a workout probe failing should be able to stop enforcement running. The combined source can name which signal opened the exception, so the UI can explain an unlock rather than presenting it as magic. Only the two sources verified to exist on this device will be wired -- com.kuhy.workout_app exposes nothing readable and org.runnerup is not installed. HideTestReceiver is deleted. It proved setApplicationHidden persistence, which is what it was for, but it applied policy from an exported receiver and that has no place in a build that will later hold device owner. Confirmed absent from the rebuilt and reinstalled APK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
126 lines
3.8 KiB
Dart
126 lines
3.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:focus_owner/workout_signal.dart';
|
|
|
|
/// A source with a fixed answer, for exercising the combinator.
|
|
class _Fixed implements WorkoutSignal {
|
|
const _Fixed(this.name, this.value);
|
|
|
|
@override
|
|
final String name;
|
|
final bool value;
|
|
|
|
@override
|
|
Future<bool> isActive() async => value;
|
|
}
|
|
|
|
void main() {
|
|
group('ManualWorkoutSignal', () {
|
|
late DateTime now;
|
|
late ManualWorkoutSignal signal;
|
|
|
|
setUp(() {
|
|
now = DateTime(2026, 8, 9, 12);
|
|
signal = ManualWorkoutSignal(
|
|
maxDuration: const Duration(minutes: 90),
|
|
clock: () => now,
|
|
);
|
|
});
|
|
|
|
test('is inactive until a workout is declared', () async {
|
|
expect(await signal.isActive(), isFalse);
|
|
expect(signal.remaining, isNull);
|
|
});
|
|
|
|
test('is active once started', () async {
|
|
signal.start();
|
|
expect(await signal.isActive(), isTrue);
|
|
expect(signal.remaining, const Duration(minutes: 90));
|
|
});
|
|
|
|
test('lapses when the window expires', () async {
|
|
// Time-boxed rather than a plain toggle: a "workout mode" left on by
|
|
// accident is indistinguishable from having no exception at all.
|
|
signal.start();
|
|
now = now.add(const Duration(minutes: 89));
|
|
expect(await signal.isActive(), isTrue);
|
|
now = now.add(const Duration(minutes: 2));
|
|
expect(await signal.isActive(), isFalse);
|
|
expect(signal.remaining, isNull);
|
|
});
|
|
|
|
test('stop ends it immediately', () async {
|
|
signal.start();
|
|
signal.stop();
|
|
expect(await signal.isActive(), isFalse);
|
|
});
|
|
|
|
test('restarting extends the window from the new start', () async {
|
|
signal.start();
|
|
now = now.add(const Duration(minutes: 80));
|
|
signal.start();
|
|
expect(signal.remaining, const Duration(minutes: 90));
|
|
});
|
|
});
|
|
|
|
group('GuardedWorkoutSignal', () {
|
|
test('passes through a successful probe', () async {
|
|
const signal = GuardedWorkoutSignal(name: 'hc', probe: _alwaysTrue);
|
|
expect(await signal.isActive(), isTrue);
|
|
});
|
|
|
|
test('a throwing probe reads as no workout', () async {
|
|
// Fail closed: a broken probe must not unlock YouTube.
|
|
const signal = GuardedWorkoutSignal(name: 'hc', probe: _alwaysThrows);
|
|
expect(await signal.isActive(), isFalse);
|
|
});
|
|
|
|
test('a hanging probe times out to no workout', () async {
|
|
const signal = GuardedWorkoutSignal(
|
|
name: 'hc',
|
|
probe: _neverCompletes,
|
|
timeout: Duration(milliseconds: 50),
|
|
);
|
|
expect(await signal.isActive(), isFalse);
|
|
});
|
|
});
|
|
|
|
group('CombinedWorkoutSignal', () {
|
|
test('is inactive when every source is', () async {
|
|
const combined = CombinedWorkoutSignal([
|
|
_Fixed('a', false),
|
|
_Fixed('b', false),
|
|
]);
|
|
expect(await combined.isActive(), isFalse);
|
|
expect(await combined.activeSource(), isNull);
|
|
});
|
|
|
|
test('is active when any source is', () async {
|
|
const combined = CombinedWorkoutSignal([
|
|
_Fixed('a', false),
|
|
_Fixed('b', true),
|
|
]);
|
|
expect(await combined.isActive(), isTrue);
|
|
});
|
|
|
|
test('names the source that opened the exception', () async {
|
|
// So the UI can explain the unlock rather than presenting it as magic.
|
|
const combined = CombinedWorkoutSignal([
|
|
_Fixed('manual', false),
|
|
_Fixed('health_connect', true),
|
|
]);
|
|
expect(await combined.activeSource(), 'health_connect');
|
|
});
|
|
|
|
test('with no sources it is inactive', () async {
|
|
const combined = CombinedWorkoutSignal([]);
|
|
expect(await combined.isActive(), isFalse);
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<bool> _alwaysTrue() async => true;
|
|
Future<bool> _alwaysThrows() async => throw StateError('probe failed');
|
|
Future<bool> _neverCompletes() => Completer<bool>().future;
|