fix(core): a learned RIP route expires at its time and is announced at once

Java's RIPTable.check marks an expired route only at the router's next
beacon, up to some 35 s late. The engine schedules each router's first
expiry, marks the route with 16 hops at that time and sends its table
at once, as after any change (better than Java where Java only samples,
decided 01.10.2026). ripJava's expire story is compared up to the
expiry and then checks that R2 marks the route at its time and R1 a
cable later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 14:14:07 +00:00
co-authored by Claude Opus 5.5
parent 739deafbf4
commit 0a34f3a4c4
5 changed files with 112 additions and 28 deletions
+21 -5
View File
@@ -2047,18 +2047,34 @@ describe('RIP', () => {
expect(beacons(withSocket, routerAId)).toEqual(beacons(plain, routerAId));
});
it('marks a learned route unreachable at the first beacon after it expired, and keeps it (Java: RIPTable.check)', () => {
it('marks a learned route unreachable when it expires, keeps it, and tells its neighbours at once (Java: at its next beacon)', () => {
const engine = ripTopology();
engine.handle({ type: 'advance', toMilliseconds: 2_000 });
engine.setGlobalPacketLossEnabled(true);
const expiry = route(engine, routerAId, '172.16.0.0')!.expiresAtMilliseconds!;
engine.handle({ type: 'advance', toMilliseconds: expiry + 1 });
engine.handle({ type: 'advance', toMilliseconds: expiry - 1 });
expect(route(engine, routerAId, '172.16.0.0')?.metric).toBe(1);
const [beacon] = beacons(engine, routerAId) as [number];
engine.handle({ type: 'advance', toMilliseconds: beacon - 1 });
expect(route(engine, routerAId, '172.16.0.0')?.metric).toBe(1);
engine.handle({ type: 'advance', toMilliseconds: beacon });
expect(beacon).toBeGreaterThan(expiry);
engine.handle({ type: 'advance', toMilliseconds: expiry });
expect(route(engine, routerAId, '172.16.0.0')).toMatchObject({ metric: 16, expiresAtMilliseconds: expiry });
expect(sentTables(engine, routerAId).at(-1)).toBe(expiry);
// The next beacon comes a pause after the message, not at the old time.
expect(beacons(engine, routerAId)[0]).toBeGreaterThan(beacon);
});
it('expires each learned route at its own time, also after another expired first', () => {
const engine = ripTopology();
engine.handle({ type: 'advance', toMilliseconds: 2_000 });
const first = fromB(engine, '10.0.0.2\n10.0.0.2\n16\n75000\n198.51.100.0 255.255.255.0 1');
engine.handle({ type: 'advance', toMilliseconds: first + 10_000 });
const second = fromB(engine, '10.0.0.2\n10.0.0.2\n16\n75000\n203.0.113.0 255.255.255.0 1');
engine.setGlobalPacketLossEnabled(true);
engine.handle({ type: 'advance', toMilliseconds: second + 75_000 - 1 });
expect(route(engine, routerAId, '198.51.100.0')?.metric).toBe(16);
expect(route(engine, routerAId, '203.0.113.0')?.metric).toBe(2);
engine.handle({ type: 'advance', toMilliseconds: second + 75_000 });
expect(route(engine, routerAId, '203.0.113.0')?.metric).toBe(16);
});
it('routes with its table alone: a manual route does not count', () => {