OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / release-pull-failure-reason.test.js

100 lines · javascript ref:
// "no holder" is three different failures wearing one label.
//
// ReleasePuller reports every falsy fetchFile() as:
//
//     no holder for <path> (<hash12>)
//
// but hyper-overlay's fetchFile returns null for at least three unrelated
// reasons, and throws away a diagnostic it has already computed:
//
//   1. NO PEERS CONNECTED. fetchFile waits opts.peerWaitMs (3s) for a peer and
//      returns null before ever creating a scheduler. Nothing about holders was
//      established -- the question was never asked.
//   2. PEERS CONNECTED, NONE SERVED. A scheduler ran, no chunk ever arrived.
//      This is the only case the words "no holder" actually describe.
//   3. TRANSFER STARTED THEN DIED. Bytes arrived, then the idle timer fired
//      (DEFAULT_IDLE_TIMEOUT 30s of no progress). A partial transfer, not an
//      absent holder.
//
// The scheduler already reports all of this through onEnd:
//
//     { ok, reason, receivedBytes, totalBytes, chunksRemaining, totalChunks,
//       peers, elapsedMs }
//
// and the puller passes no onEnd, so it is discarded. That is why 2026-08-08,
// 2026-08-25 and 2026-08-29 all read identically in the log while being, for
// all anyone could tell, different bugs -- and why two of my own diagnoses
// (null contentHash, then size-dependence) were wrong: the message asserted a
// cause nobody had measured.
//
// This does not fix the fetch. It makes the log state what actually happened.

import test from 'brittle'

import { describeFetchFailure } from '../lib/services/release-puller.js'

const f = { path: 'app/versions/v1/big.apk', hash: 'ab'.repeat(32) }

test('no scheduler ever ran: says the peer wait expired, NOT that a holder is missing', (t) => {
  const msg = describeFetchFailure(f, null)
  t.ok(msg.includes('app/versions/v1/big.apk'), 'names the artifact')
  t.ok(msg.includes('abababab'), 'and its hash')
  t.ok(/no peer/i.test(msg), 'attributes it to peers, not holders')
  t.absent(/no holder/i.test(msg), 'must not claim a holder question that was never asked')
})

test('peers connected but nothing served: this IS a genuine no-holder', (t) => {
  const msg = describeFetchFailure(f, {
    ok: false,
    reason: 'multi-source fetch stalled (no progress for 30000ms)',
    receivedBytes: 0,
    totalBytes: 168326045,
    chunksRemaining: 4100,
    totalChunks: 4100,
    peers: 2,
    elapsedMs: 30001
  })
  t.ok(/no holder/i.test(msg), 'the one case the phrase fits')
  t.ok(msg.includes('2 peer'), 'reports how many peers were connected')
  t.ok(msg.includes('0 of 4100'), 'and that no chunk arrived')
})

test('partial transfer that stalled: reported as a stall with progress, not no-holder', (t) => {
  const msg = describeFetchFailure(f, {
    ok: false,
    reason: 'multi-source fetch stalled (no progress for 30000ms)',
    receivedBytes: 50000000,
    totalBytes: 168326045,
    chunksRemaining: 2900,
    totalChunks: 4100,
    peers: 1,
    elapsedMs: 92000
  })
  t.absent(/no holder/i.test(msg), 'a holder plainly existed -- it sent 50 MB')
  t.ok(/stall/i.test(msg), 'named as a stall')
  t.ok(msg.includes('50000000') || msg.includes('50.0 MB') || msg.includes('47.7 MiB'),
    'reports bytes received so the partial is visible')
  t.ok(msg.includes('1200 of 4100') || msg.includes('2900'), 'and chunk progress')
})

test('the underlying reason string is carried through verbatim', (t) => {
  const msg = describeFetchFailure(f, {
    ok: false,
    reason: 'EHOSTUNREACH something specific',
    receivedBytes: 0,
    totalBytes: 10,
    chunksRemaining: 1,
    totalChunks: 1,
    peers: 3,
    elapsedMs: 12
  })
  t.ok(msg.includes('EHOSTUNREACH something specific'),
    'never swallow the overlay reason -- it is the only first-hand account')
})

test('a malformed diagnostic degrades instead of throwing', (t) => {
  t.ok(typeof describeFetchFailure(f, {}) === 'string', 'empty object')
  t.ok(typeof describeFetchFailure(f, { peers: 'x' }) === 'string', 'nonsense field')
  t.ok(describeFetchFailure({}, null).length > 0, 'missing artifact fields')
})