// ref-diff — the view-vs-disk comparison that makes ref replication observable.
//
// Grounded in a real 2026-08-08 failure: `refs/heads/release` was deleted on the
// origin for 5 repos and believed to have propagated fleet-wide. It propagated
// for 2. For the other 3 the deletion never entered the view (gipPublishRef
// returns silently when the tracker is not writable, or when the view never had
// the ref) while `ref-update-observed` still answered ok:true. Nothing could show
// the view's refs, so the no-op was indistinguishable from success.
import test from 'brittle'
import {
classifyRefs, refDisagreements, IN_SYNC, DIVERGENT, VIEW_ONLY, DISK_ONLY
} from '../lib/services/ref-diff.js'
const A = 'a'.repeat(40)
const B = 'b'.repeat(40)
test('agreeing refs are in-sync', (t) => {
const rows = classifyRefs(new Map([['refs/heads/main', A]]), new Map([['refs/heads/main', A]]))
t.is(rows.length, 1)
t.is(rows[0].state, IN_SYNC)
t.alike(refDisagreements(rows), [], 'nothing for an operator to look at')
})
test('same ref at different oids is divergent', (t) => {
// The view wins: RefReconciler rolls disk BACK to the view's oid. That is how a
// push accepted locally but non-FF in the linearized order gets undone.
const rows = classifyRefs(new Map([['refs/heads/main', A]]), new Map([['refs/heads/main', B]]))
t.is(rows[0].state, DIVERGENT)
t.is(rows[0].view, A)
t.is(rows[0].disk, B)
})
test('viewOnly means deleting from disk alone will NOT stick', (t) => {
// RefReconciler materializes view refs onto disk, so a local `update-ref -d`
// gets undone on the next reconcile.
const rows = classifyRefs(new Map([['refs/heads/release', A]]), new Map())
t.is(rows[0].state, VIEW_ONLY)
t.is(rows[0].disk, null)
t.is(refDisagreements(rows).length, 1)
})
test('diskOnly is the resurrection hazard — the exact 2026-08-08 state', (t) => {
// stortron after the origin-side delete silently failed to publish: disk still
// has the ref, the view does not. ref-publish-reconciler reads that as "disk
// ahead of view" and re-announces, undoing the deletion fleet-wide.
const rows = classifyRefs(new Map(), new Map([['refs/heads/release', A]]))
t.is(rows[0].state, DISK_ONLY)
t.is(rows[0].view, null)
t.is(rows[0].disk, A)
})
test('diskOnly cannot distinguish "never propagated" from "deleted elsewhere"', (t) => {
// Both an out-of-band ref and a ref deleted on another node look identical from
// here, because deletion removes the key rather than leaving a tombstone. This
// pins the ambiguity as a known property, so a future tombstone can be judged
// against it rather than added speculatively.
const outOfBand = classifyRefs(new Map(), new Map([['refs/heads/internal', A]]))
const deletedElsewhere = classifyRefs(new Map(), new Map([['refs/heads/release', A]]))
t.is(outOfBand[0].state, deletedElsewhere[0].state,
'indistinguishable by state alone — only intent separates them')
})
test('a full repo picture sorts and covers every combination', (t) => {
const rows = classifyRefs(
new Map([['refs/heads/main', A], ['refs/heads/dev', A], ['refs/heads/gone-locally', A]]),
new Map([['refs/heads/main', A], ['refs/heads/dev', B], ['refs/heads/release', A]])
)
t.alike(rows.map(r => r.ref), [
'refs/heads/dev', 'refs/heads/gone-locally', 'refs/heads/main', 'refs/heads/release'
], 'sorted by ref name')
t.alike(rows.map(r => r.state), [DIVERGENT, VIEW_ONLY, IN_SYNC, DISK_ONLY])
t.is(refDisagreements(rows).length, 3, 'only the in-sync one is filtered out')
})
test('empty and missing inputs are not errors', (t) => {
t.alike(classifyRefs(new Map(), new Map()), [])
t.alike(classifyRefs(null, null), [], 'null tolerated — a repo with no tracker yet')
t.alike(classifyRefs([['refs/heads/main', A]], [['refs/heads/main', A]])[0].state, IN_SYNC,
'accepts plain entry arrays as well as Maps')
})