Ctrl K
ComunidadGuías…

test/connect-unary-timeout.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// GAP-1 (RATE-RETRY audit, 2026-10-15): the GetUserJwt unary mint had NO timeout.
//
// The mint runs BEFORE the chat request is built (streamChat awaits it), and
// mintUserJwt coalesces concurrent mints per (token, host). So one upstream that
// accepts the TCP connection and then never answers hung the chat request forever
// AND amplified that hang to every other request sharing the token — with no timer
// running anywhere: the stream's own absolute deadline starts after the mint, and
// a cold cache entry has nothing to serve.
//
// These tests pin the bound and the three properties it must have:
//   1. the mint resolves null at the deadline — the same resolve-null contract as
//      every other failure in postConnectUnary — instead of hanging;
//   2. it does not fire early: a mint that degrades to null immediately is a mint
//      that silently disables the opt-in feature;
//   3. the deadline destroys the socket and releases the in-flight slot, so the
//      next request mints again instead of joining a promise that already gave up.
//
// The hanging transport is injected through the same __setRequestImpl seam the
// body-bound tests use, so no socket is opened (test/setup-env.mjs also installs a
// fail-closed network guard). The module is imported as a namespace on purpose:
// pre-fix there is no USER_JWT_MINT_TIMEOUT_MS export, and a named import would
// fail at load time — turning the behavioural red (the mint hangs) into an import
// error that says nothing about the bug.
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';

const connect = await import('../src/devin-connect.js');
const { mintUserJwt, invalidateUserJwtCache, __setRequestImpl } = connect;
const { writeStringField } = await import('../src/proto.js');
const { wrapEnvelope } = await import('../src/connect.js');

const TOKEN = 'devin-session-token$gap1';
const JWT = 'header.payload.signature';
// Generous next to the deadlines under test (150-250ms) so a loaded machine
// cannot turn "the bound works" into a flaky watchdog hit; still far below the
// 15s default, so a missing deadline cannot pass by luck.
const WATCHDOG_MS = 3000;
const WATCHDOG = Symbol('watchdog: the mint never settled');

/** Await `p` with a watchdog that is cleared as soon as either side settles. */
async function withWatchdog(p, ms = WATCHDOG_MS) {
  let timer;
  const watchdog = new Promise((resolve) => { timer = setTimeout(() => resolve(WATCHDOG), ms); });
  try {
    return await Promise.race([p, watchdog]);
  } finally {
    clearTimeout(timer);
  }
}

/**
 * A transport that accepts the request and then never answers: no response
 * callback, no error, no close. `destroy()` is recorded because the bound is only
 * a bound if it also stops the connection from sitting there — resolving null
 * while leaving the request open leaks one connection per mint.
 */
function hangingTransport() {
  const state = { calls: 0, destroyed: 0 };
  state.impl = () => {
    state.calls += 1;
    const req = new EventEmitter();
    req.write = () => {};
    req.end = () => {};
    req.destroy = () => { state.destroyed += 1; };
    return req;
  };
  return state;
}

/**
 * A transport that answers with a valid credential after `delayMs`. The delay is
 * what makes the "0 must not mean instant" case deterministic: an instant timer
 * fires first and the mint returns null, while the documented fallback (15s)
 * leaves the response time to arrive.
 */
function answeringTransport(delayMs = 5) {
  return (_opts, cb) => {
    const listeners = {};
    const res = {
      statusCode: 200,
      on(ev, fn) { listeners[ev] = fn; return res; },
    };
    setTimeout(() => {
      cb(res);
      listeners.data?.(wrapEnvelope(writeStringField(1, JWT), { compress: false }));
      listeners.end?.();
    }, delayMs);
    const req = new EventEmitter();
    req.write = () => {};
    req.end = () => {};
    req.destroy = () => {};
    return req;
  };
}

beforeEach(() => { invalidateUserJwtCache(); });
afterEach(() => { __setRequestImpl(null); invalidateUserJwtCache(); });

describe('the GetUserJwt mint is bounded (GAP-1)', () => {
  it('resolves null at the deadline instead of hanging, and destroys the request', async () => {
    const transport = hangingTransport();
    __setRequestImpl(transport.impl);
    const timeoutMs = 250;
    const started = Date.now();
    const result = await withWatchdog(mintUserJwt(TOKEN, { timeoutMs }));
    const elapsed = Date.now() - started;

    assert.notEqual(result, WATCHDOG, `the mint must settle within ${timeoutMs}ms, not hang`);
    assert.equal(result, null, 'a timed-out mint degrades to null, exactly like a non-200');
    assert.ok(
      elapsed >= timeoutMs / 2,
      `settled after ${elapsed}ms — the deadline must be a deadline, not an early bail`,
    );
    assert.equal(transport.calls, 1);
    assert.equal(transport.destroyed, 1, 'the deadline must destroy the request it gave up on');
  });

  it('releases the in-flight slot, so the next mint opens a NEW request', async () => {
    const transport = hangingTransport();
    __setRequestImpl(transport.impl);

    const first = await withWatchdog(mintUserJwt(TOKEN, { timeoutMs: 150 }));
    assert.equal(first, null, 'the first mint must degrade to null, not hang');
    const second = await withWatchdog(mintUserJwt(TOKEN, { timeoutMs: 150 }));
    assert.equal(second, null, 'the second mint must settle too, not join a promise that gave up');
    assert.equal(
      transport.calls, 2,
      'a timed-out mint must leave _userJwtInflight, or every later request re-awaits the hang',
    );
    assert.equal(transport.destroyed, 2);
  });

  it('bounds every waiter of a coalesced burst at the ONE shared deadline', async () => {
    const transport = hangingTransport();
    __setRequestImpl(transport.impl);
    const results = await withWatchdog(Promise.all([
      mintUserJwt(TOKEN, { timeoutMs: 250 }),
      mintUserJwt(TOKEN, { timeoutMs: 250 }),
      mintUserJwt(TOKEN, { timeoutMs: 250 }),
    ]));

    assert.notEqual(results, WATCHDOG, 'every waiter of the coalesced mint must settle');
    assert.deepEqual(results, [null, null, null]);
    assert.equal(transport.calls, 1, 'the burst still mints exactly once');
    assert.equal(transport.destroyed, 1, 'one request, one deadline — not one timer per waiter');
  });

  it('defaults to 15s, and a 0 override falls back to it instead of arming an instant timer', async () => {
    assert.equal(
      connect.USER_JWT_MINT_TIMEOUT_MS, 15_000,
      'the documented default: 15s, the ceiling for an RPC that gates a user request',
    );

    __setRequestImpl(answeringTransport(5));
    assert.equal(
      await withWatchdog(mintUserJwt(TOKEN, { timeoutMs: 0 })),
      JWT,
      'a 0 override must not fire on the next turn of the loop and kill a healthy mint',
    );
  });
});