fix(#531): report the timeout window that was armed, not a rounded-up one

The timer ran timeoutMs while the message printed
(timeoutMs / 1000).ceil(), so every window in (4000, 5000] announced
"timeout after 5 seconds". The owner's 0-hop window was 4074 ms and fired
at 4.07 s while claiming 5, which is what made the behaviour look
arbitrary rather than deterministic: the number shown was never the
number used.

The service now throws a typed RepeaterCommandTimeout carrying the window
that was actually armed, formatted to one decimal. Every existing caller
already stringifies the error, so all of them inherit an honest figure
without being touched; the CLI screen additionally renders it through a
new localized string rather than the generic error wrapper.

Tests pin that 4074 reports 4.1 rather than 5, that the new 28748 ms
budget reports 28.7 rather than 29, and that two windows inside the same
second no longer collapse to the same text, which was the defect's
signature.

Part of epic #473, stacked on #528 and #529.
release/1.5.0-beta.1
Strycher 2 months ago
parent a307c837bc
commit 75d08e73ea

@ -1566,6 +1566,14 @@
}
}
},
"repeater_cliCommandTimeout": "Command timed out after {seconds} seconds",
"@repeater_cliCommandTimeout": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"repeater_cliLateResponse": "Late response to \"{command}\" (arrived {seconds}s after timeout)",
"@repeater_cliLateResponse": {
"placeholders": {

@ -5126,6 +5126,12 @@ abstract class AppLocalizations {
/// **'Error: {error}'**
String repeater_cliCommandError(String error);
/// No description provided for @repeater_cliCommandTimeout.
///
/// In en, this message translates to:
/// **'Command timed out after {seconds} seconds'**
String repeater_cliCommandTimeout(String seconds);
/// No description provided for @repeater_cliLateResponse.
///
/// In en, this message translates to:

@ -2905,6 +2905,11 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Грешка: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2908,6 +2908,11 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Fehler: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2849,6 +2849,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Error: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2899,6 +2899,11 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Error: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2921,6 +2921,11 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Erreur : $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2912,6 +2912,11 @@ class AppLocalizationsHu extends AppLocalizations {
return 'Hiba: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2905,6 +2905,11 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Errore: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2785,6 +2785,11 @@ class AppLocalizationsJa extends AppLocalizations {
return 'エラー:$error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2783,6 +2783,11 @@ class AppLocalizationsKo extends AppLocalizations {
return '오류: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2885,6 +2885,11 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Fout: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2913,6 +2913,11 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Błąd: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2897,6 +2897,11 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Erro: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2903,6 +2903,11 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Ошибка: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2884,6 +2884,11 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Chyba: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2883,6 +2883,11 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Napaka: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2869,6 +2869,11 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Fel: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2901,6 +2901,11 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Помилка: $error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -2737,6 +2737,11 @@ class AppLocalizationsZh extends AppLocalizations {
return '错误:$error';
}
@override
String repeater_cliCommandTimeout(String seconds) {
return 'Command timed out after $seconds seconds';
}
@override
String repeater_cliLateResponse(String command, String seconds) {
return 'Late response to \"$command\" (arrived ${seconds}s after timeout)';

@ -191,10 +191,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
}
} catch (e) {
if (mounted) {
// A timeout knows the exact window that was armed, so it reports that
// rather than a rounded-up guess (#531).
final text = e is RepeaterCommandTimeout
? context.l10n.repeater_cliCommandTimeout(e.secondsText)
: context.l10n.repeater_cliCommandError(e.toString());
setState(() {
_commandHistory.add({
'type': 'response',
'text': context.l10n.repeater_cliCommandError(e.toString()),
'text': text,
'timestamp': DateTime.now().toString(),
});
});

@ -29,6 +29,32 @@ class UnmatchedRepeaterResponse {
bool get isLateReply => command != null;
}
/// Thrown when a CLI command's window closes with no reply.
///
/// Carries the window that was actually armed, so callers can report the real
/// figure. The previous message printed `(timeoutMs / 1000).ceil()`, so a
/// 4074 ms window announced "timeout after 5 seconds": every window in
/// (4000, 5000] reported the same 5, and the number shown was never the number
/// used (#531).
class RepeaterCommandTimeout implements Exception {
final String command;
final int timeoutMs;
const RepeaterCommandTimeout({
required this.command,
required this.timeoutMs,
});
/// The armed window in seconds to one decimal, e.g. `28.7`.
String get secondsText => (timeoutMs / 1000).toStringAsFixed(1);
/// Non-localized fallback for logs and for callers without a
/// [BuildContext]. UI paths should prefer [secondsText] with their own
/// localized string.
@override
String toString() => 'Command timed out after $secondsText seconds';
}
class _ExpiredCommand {
final String command;
final DateTime expiredAt;
@ -136,7 +162,6 @@ class RepeaterCommandService {
pathLength: pathLengthValue,
messageBytes: responseBytes,
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
await _connector.sendFrame(frame);
_commandTimeouts[commandId]?.cancel();
_commandTimeouts[commandId] = Timer(
@ -151,7 +176,7 @@ class RepeaterCommandService {
expiredAt: DateTime.now(),
);
completer.completeError(
'Command timeout after $timeoutSeconds seconds',
RepeaterCommandTimeout(command: command, timeoutMs: timeoutMs),
);
_cleanup(commandId);
}

@ -114,4 +114,35 @@ void main() {
expect(seen.single.command, isNull);
});
group('timeout reporting (#531)', () {
// The old message printed (timeoutMs / 1000).ceil(), so every window in
// (4000, 5000] announced "5 seconds" and the figure shown was never the
// one armed. The owner's 0-hop window was 4074 ms and it claimed 5.
test('reports the armed window to one decimal, not rounded up', () {
const e = RepeaterCommandTimeout(command: 'ver', timeoutMs: 4074);
expect(e.secondsText, '4.1');
expect(e.toString(), 'Command timed out after 4.1 seconds');
});
test('does not round a sub-second remainder up to the next second', () {
// 28748 ms is the new CLI budget on the owner's preset. ceil() would say
// 29; the armed window is 28.7.
const e = RepeaterCommandTimeout(command: 'status', timeoutMs: 28748);
expect(e.secondsText, '28.7');
});
test('distinct windows in the same second are distinguishable', () {
// The defect's signature: 4001 and 4999 both printed "5 seconds".
const a = RepeaterCommandTimeout(command: 'a', timeoutMs: 4001);
const b = RepeaterCommandTimeout(command: 'b', timeoutMs: 4999);
expect(a.secondsText, isNot(b.secondsText));
});
test('carries the command so a caller can name what timed out', () {
const e = RepeaterCommandTimeout(command: 'get tx', timeoutMs: 1234);
expect(e.command, 'get tx');
expect(e.timeoutMs, 1234);
});
});
}

Loading…
Cancel
Save

Powered by TurnKey Linux.