Tracks the local participant's LiveKit ConnectionQuality (time spent
poor/lost) and Reconnecting events for the life of the in-call view and
sends one io.lotus.call_summary { durationMs, reconnects, poorMs, verdict }
to the host on the first SFU disconnect or on teardown. Nothing is stored
or sent anywhere else. CallQualityTracker is pure and unit-tested.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
94 lines
2.5 KiB
TypeScript
94 lines
2.5 KiB
TypeScript
/*
|
|
Copyright 2026 Lotus Guild
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { ConnectionQuality } from "livekit-client";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { CallQualityTracker } from "./lotusCallSummary";
|
|
|
|
const clock = (): { now: () => number; tick: (ms: number) => void } => {
|
|
let t = 1_000_000;
|
|
return {
|
|
now: () => t,
|
|
tick: (ms) => {
|
|
t += ms;
|
|
},
|
|
};
|
|
};
|
|
|
|
describe("CallQualityTracker", () => {
|
|
it("reports unknown when nothing was sampled", () => {
|
|
const c = clock();
|
|
const tr = new CallQualityTracker(c.now);
|
|
tr.start();
|
|
c.tick(60_000);
|
|
expect(tr.summary()).toEqual({
|
|
durationMs: 60_000,
|
|
reconnects: 0,
|
|
poorMs: 0,
|
|
verdict: "unknown",
|
|
});
|
|
});
|
|
|
|
it("is good when quality stayed fine", () => {
|
|
const c = clock();
|
|
const tr = new CallQualityTracker(c.now);
|
|
tr.start();
|
|
tr.setQuality(ConnectionQuality.Excellent);
|
|
c.tick(30 * 60_000);
|
|
expect(tr.summary().verdict).toBe("good");
|
|
});
|
|
|
|
it("accumulates poor time across episodes, including an open one", () => {
|
|
const c = clock();
|
|
const tr = new CallQualityTracker(c.now);
|
|
tr.start();
|
|
tr.setQuality(ConnectionQuality.Good);
|
|
c.tick(60_000);
|
|
tr.setQuality(ConnectionQuality.Poor);
|
|
c.tick(10_000);
|
|
tr.setQuality(ConnectionQuality.Good);
|
|
c.tick(60_000);
|
|
tr.setQuality(ConnectionQuality.Lost);
|
|
c.tick(5_000);
|
|
const s = tr.summary();
|
|
expect(s.poorMs).toBe(15_000);
|
|
expect(s.durationMs).toBe(135_000);
|
|
expect(s.verdict).toBe("fair");
|
|
});
|
|
|
|
it("is poor with many reconnects or mostly-poor quality", () => {
|
|
const c = clock();
|
|
const tr = new CallQualityTracker(c.now);
|
|
tr.start();
|
|
tr.setQuality(ConnectionQuality.Good);
|
|
for (let i = 0; i < 4; i += 1) tr.reconnect();
|
|
c.tick(60_000);
|
|
expect(tr.summary()).toMatchObject({ reconnects: 4, verdict: "poor" });
|
|
|
|
const tr2 = new CallQualityTracker(c.now);
|
|
tr2.start();
|
|
tr2.setQuality(ConnectionQuality.Poor);
|
|
c.tick(60_000);
|
|
expect(tr2.summary().verdict).toBe("poor");
|
|
});
|
|
|
|
it("ignores unknown samples and only starts once", () => {
|
|
const c = clock();
|
|
const tr = new CallQualityTracker(c.now);
|
|
tr.start();
|
|
c.tick(1_000);
|
|
tr.start();
|
|
tr.setQuality(ConnectionQuality.Unknown);
|
|
c.tick(1_000);
|
|
expect(tr.summary()).toMatchObject({
|
|
durationMs: 2_000,
|
|
verdict: "unknown",
|
|
});
|
|
});
|
|
});
|