Skip to content
Merged
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
13 changes: 11 additions & 2 deletions lib/src/control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -958,8 +958,17 @@ CmdResponse? parseCommandResponse(Uint8List inner,
// resume. Records older than it are unreachable via normal sync: a read
// cursor at/above range_oldest while records are still expected means
// they were trimmed or skipped, not merely pending.
//
// Status-gated like the clock read above: a failure reply leaves the body
// unpopulated, so a stale revision byte and stale-but-plausible range,
// backlog and read-cursor values from a prior successful read would
// otherwise be reported as current. A stale cursor is the worst of these:
// it makes the band's drain position look stalled or rewound. This opcode
// is shared by both profiles (see commands.dart), so gen4 is deliberately
// left ungated for the clock read's unconfirmed-status-byte reason.
final statusOk = !profile.isGen5 || status == 1;
final revOk = payload.length > 2 && payload[2] == 1;
if (revOk && payload.length >= 63) {
if (statusOk && revOk && payload.length >= 63) {
final oldest = u32(payload, 35);
final newest = u32(payload, 59);
if (_plausibleUnix(oldest) && _plausibleUnix(newest) && oldest <= newest) {
Expand All @@ -977,7 +986,7 @@ CmdResponse? parseCommandResponse(Uint8List inner,
// strap's own view of its ring buffer, which is what separates a stalled
// offload from an idle one. Degrades safely: implausible values emit
// nothing at all.
if (revOk && payload.length >= 35) {
if (statusOk && revOk && payload.length >= 35) {
final writePage = u32(payload, 11);
final capacity = u32(payload, 23); // TotalPages
if (capacity > 0 && writePage <= capacity) {
Expand Down
64 changes: 64 additions & 0 deletions test/control_plane_offsets_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,70 @@ void main() {
dataRangeBody(writePage: 200000, totalPages: 131072)))!;
expect(r.decoded.containsKey('pages_behind'), isFalse);
});

// Every field a successful reply emits, cursor included. The body used
// below decodes to all of them, so a missing key means the gate dropped it.
const rangeKeys = [
'range_oldest',
'range_newest',
'trim_ts',
'current_read_ts',
'pages_behind',
];
const expectedPages = {
'written': 30,
'used': 4567,
'capacity': 131072,
'trim_page': 40,
'wrap_count': 3,
'free_records': 89012,
'raw_old_page': 10,
'read_page': 20,
};

test('gen5: a non-success outer status emits no range, backlog or cursor',
() {
// A failure reply leaves the body as the buffer held it after the last
// good read, so a plausible body with revision 1 is not evidence of a
// current one. A stale read cursor would look like the band's drain
// position went backwards or stalled.
for (final status in [0, 2, 3]) {
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: status),
profile: BandProfile.gen5)!;
for (final k in rangeKeys) {
expect(r.decoded.containsKey(k), isFalse,
reason: 'status=$status key=$k');
}
}
});

test('gen5: a success outer status decodes every field', () {
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: 1),
profile: BandProfile.gen5)!;
expect(r.decoded['range_oldest'], 1780000000);
expect(r.decoded['range_newest'], 1786000000);
expect(r.decoded['trim_ts'], 1781000000);
expect(r.decoded['current_read_ts'], 1782000000);
expect(r.decoded['pages_behind'], expectedPages);
});

test('gen4 stays ungated on outer status', () {
// Same policy as GET_CLOCK: the gen4 status byte is unconfirmed, so a
// gate there could silently stop every range read instead.
for (final status in [0, 1, 2, 3]) {
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: status))!;
expect(r.decoded['range_oldest'], 1780000000, reason: 'status=$status');
expect(r.decoded['range_newest'], 1786000000, reason: 'status=$status');
expect(r.decoded['trim_ts'], 1781000000, reason: 'status=$status');
expect(r.decoded['current_read_ts'], 1782000000,
reason: 'status=$status');
expect(r.decoded['pages_behind'], expectedPages,
reason: 'status=$status');
}
});
});

// The alarm/haptics status byte: the SET and RUN
Expand Down
Loading