fix(shell): daemon-start and health-poll auth handling; policy panel freshness
Some checks failed
Security / Security check (push) Failing after 2s

Review follow-ups on the auth-status work:

- Both daemon-start paths classified an auth-rejected hub as "daemon
  dead" via healthy() and showed a start-failure dialog while the
  shell banner above correctly blamed the token. They now share
  daemonAnswers(): only an unreachable probe counts as down.
- An auth-rejected poll now re-reads ~/.chain/hub-auth-token and
  reconnects when the file changed, so a token fixed outside Studio
  (CLI, editor) heals the connection without a restart — previously
  the client kept the stale in-memory token forever and the banner's
  own advice could not work.
- An endpoint switch resets the failure streak, so a stale in-flight
  probe can no longer let the unreachable banner blame the new
  endpoint for the old one's misses.
- The auth-policy panel re-queries when the hub token is saved or
  cleared in the panel above (reloadTick), instead of keeping a
  stale admin-denied hint; it also renders the hub's new
  reload_required flag as a pending-reload warning (DE+EN).
- today-pipeline.md still documented ~/.fai/today after the rename;
  the FAB theme comment now states the both-themes intent.

flutter analyze clean; 71 tests green including four new ones.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-15 05:50:24 +02:00
parent 2a1cbc82c3
commit 7cc8bab9b9
13 changed files with 242 additions and 16 deletions

View file

@ -156,4 +156,54 @@ void main() {
expect(find.textContaining('admin-Recht'), findsOneWidget);
expect(find.text('Erneut versuchen'), findsOneWidget);
});
testWidgets('reload-required policy shows the pending-reload hint', (
tester,
) async {
await tester.pumpWidget(
_host(
HubAuthPolicyPanel(
loader: () async => const HubAuthPolicy(
validator: 'static',
anonymousAllowed: false,
tokens: [],
reloadRequired: true,
),
),
),
);
await tester.pumpAndSettle();
expect(
find.textContaining('erzwingt noch den alten Stand'),
findsOneWidget,
);
});
testWidgets('a bumped reloadTick re-queries the policy', (tester) async {
var loads = 0;
Widget hostWithTick(int tick) => _host(
HubAuthPolicyPanel(
loader: () async {
loads++;
return _staticPolicy();
},
reloadTick: tick,
),
);
await tester.pumpWidget(hostWithTick(0));
await tester.pumpAndSettle();
expect(loads, 1);
// Same tick: no reload on unrelated rebuilds.
await tester.pumpWidget(hostWithTick(0));
await tester.pumpAndSettle();
expect(loads, 1);
// Bumped tick (token saved next door): the panel re-queries.
await tester.pumpWidget(hostWithTick(1));
await tester.pumpAndSettle();
expect(loads, 2);
});
}

View file

@ -33,6 +33,7 @@ Future<void> _pumpShell(WidgetTester tester) async {
void main() {
tearDown(() {
StudioShellState.debugProbeOverride = null;
StudioShellState.debugReloadTokenOverride = null;
});
testWidgets('sustained auth rejection points at the token, not the wire',
@ -60,4 +61,40 @@ void main() {
expect(find.textContaining("Can't reach"), findsOneWidget);
expect(find.textContaining('rejected the sign-in'), findsNothing);
});
testWidgets('auth-rejected poll re-reads the token file and re-probes',
(tester) async {
// First probe(s) rejected; after the token file "changes" the
// immediate re-probe serves the operator fixed the token via
// CLI/editor and Studio healed without waiting a poll cycle.
var probes = 0;
var reloads = 0;
StudioShellState.debugProbeOverride = () async {
probes++;
return reloads > 0 ? HubProbeResult.serving : HubProbeResult.authRejected;
};
StudioShellState.debugReloadTokenOverride = () async {
reloads++;
return true; // token file content changed reconnected
};
await _pumpShell(tester);
expect(reloads, greaterThanOrEqualTo(1));
// The self-heal re-probe ran within the same tick (probes >
// reloads means at least one immediate retry happened) and the
// banner never surfaces.
expect(probes, greaterThan(reloads));
expect(find.textContaining('rejected the sign-in'), findsNothing);
expect(find.textContaining("Can't reach"), findsNothing);
});
testWidgets('daemonAnswers treats auth-rejected as alive', (tester) async {
StudioShellState.debugProbeOverride =
() async => HubProbeResult.authRejected;
expect(await StudioShellState.daemonAnswers(), isTrue);
StudioShellState.debugProbeOverride =
() async => HubProbeResult.unreachable;
expect(await StudioShellState.daemonAnswers(), isFalse);
});
}