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
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,16 @@ export function computeProviderTrackRecords(
stances.set(signal.provider, signal.vote === "fail");
}

// Dedupe to one signal per (provider, targetKey) pair, latest-vote-wins — matching stancesByTarget's
// last-write semantics above. loadLiveProviderTrackRecords reads raw audit rows with no dedup, so a provider
// re-reviewing the same PR (e.g. after a new push) would otherwise inflate its signals/decided/shared/consensus
// counters proportionally to its revote count while its stance reflects only the last vote (#8876).
const dedupedByProviderTarget = new Map<string, ProviderReviewSignal>();
for (const signal of signals) dedupedByProviderTarget.set(`${signal.provider} ${signal.targetKey}`, signal);

const perRepo = new Map<string, Map<string, MutableStats>>(); // provider → repo → stats
const overall = new Map<string, MutableStats>();
for (const signal of signals) {
for (const signal of dedupedByProviderTarget.values()) {
let repos = perRepo.get(signal.provider);
if (repos === undefined) {
repos = new Map();
Expand Down
20 changes: 20 additions & 0 deletions packages/loopover-engine/test/provider-track-record.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ test("null discipline: no fail votes -> null precision; no shared targets -> nul
assert.equal(overall.splitRate, null);
assert.equal(overall.agreementRate, 1);
});

test("computeProviderTrackRecords counts a provider's repeated votes on one target once, latest-vote-wins (#8876)", () => {
const cases = [labeled("acme/widgets#1", "confirmed"), labeled("acme/widgets#2", "confirmed")];
const signals = [
signal("provider-a", "acme/widgets#1", "pass"),
signal("provider-a", "acme/widgets#1", "fail"),
signal("provider-b", "acme/widgets#1", "fail"),
signal("provider-a", "acme/widgets#2", "fail"),
];
const records = computeProviderTrackRecords(signals, cases);
const aOverall = records.find((r) => r.provider === "provider-a" && r.repoFullName === null)!;
// Without dedup this would be signals:3 (the stale pass on #1 counted); deduped it is 2 distinct targets,
// #1 resolving to the latest fail so precision stays 1.
assert.equal(aOverall.signals, 2);
assert.equal(aOverall.decided, 2);
assert.equal(aOverall.precision, 1);
assert.equal(aOverall.consensusRate, 1);
const bOverall = records.find((r) => r.provider === "provider-b" && r.repoFullName === null)!;
assert.equal(bOverall.signals, 1);
});
23 changes: 23 additions & 0 deletions test/unit/provider-track-record-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,27 @@ describe("computeProviderTrackRecords (#8228)", () => {
it("returns an empty list for empty inputs", () => {
expect(computeProviderTrackRecords([], [])).toEqual([]);
});

it("counts a provider's repeated votes on one target exactly once, latest-vote-wins (#8876)", () => {
const cases = [labeled("acme/widgets#1", "confirmed"), labeled("acme/widgets#2", "confirmed")];
const signals = [
// provider-a re-reviews #1: an earlier pass then a later fail. Only the latest (fail) should count, once.
signal("provider-a", "acme/widgets#1", "pass"),
signal("provider-a", "acme/widgets#1", "fail"),
// provider-b reviews #1 once (fail) so #1 is a shared target with a consensus stance.
signal("provider-b", "acme/widgets#1", "fail"),
// provider-a also reviews #2 once, to prove distinct (provider, target) pairs still each count.
signal("provider-a", "acme/widgets#2", "fail"),
];
const records = computeProviderTrackRecords(signals, cases);

const aOverall = records.find((r) => r.provider === "provider-a" && r.repoFullName === null)!;
// Without dedup this would be signals:3/decided:3 (the pass+fail on #1 both counted). Deduped: 2 distinct
// targets, and #1 resolves to the latest fail vote (so precision stays 1, not diluted by the stale pass).
expect(aOverall).toMatchObject({ signals: 2, decided: 2, confirmed: 2, precision: 1, agreementRate: 1 });
const bOverall = records.find((r) => r.provider === "provider-b" && r.repoFullName === null)!;
// #1 is shared by a (latest fail) and b (fail): a genuine consensus, counted once for each provider.
expect(bOverall).toMatchObject({ signals: 1, decided: 1, consensusRate: 1 });
expect(aOverall.consensusRate).toBe(1);
});
});