← back to Dw Rotation Activator

test/vision-http.test.js

70 lines

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events');
const { errorDetails, cooldownMs, requestVision } = require('../lib/vision-http');
const { SettlementGate } = require('../lib/settlement-gate');
const gate = () => Object.assign(Object.create(SettlementGate.prototype), {
  lockOk: true, geminiKeys: ['test-key-one', 'test-key-two'], visionRetryAt: 0,
  visionHttpAttempts: 0, visionThrottledSkips: 0, lastVisionError: null,
});
test('upstream quota details and retry delay retained with secrets redacted', () => {
  const details = errorDetails({ status: 429, body: JSON.stringify({ error: {
    status: 'RESOURCE_EXHAUSTED', message: 'quota exhausted key=secret-one https://api.test/?key=secret-two',
    details: [{ '@type': 'type.googleapis.com/google.rpc.RetryInfo', retryDelay: '70s' },
      { '@type': 'type.googleapis.com/google.rpc.QuotaFailure', violations: [{ quotaId: 'RequestsPerDay', quotaValue: '20' }] }],
  } }) }, ['secret-one', 'secret-two']);
  assert.equal(details.retryAfterMs, 70000);
  assert.equal(details.quota[0].value, '20');
  assert.ok(!JSON.stringify(details).includes('secret-'));
  assert.equal(cooldownMs(details), 3600000);
});
test('429 across existing keys opens circuit; later drafts stay HELD without downloads', async () => {
  const g = gate(); let calls = 0, downloads = 0;
  g._visionCall = async () => { calls++; return { ok: false, why: 'vision-http-429', details: { httpStatus: 429 } }; };
  g.fetchImageB64 = async () => { downloads++; return 'bytes'; };
  assert.equal((await g.visionDetect('bytes', 'image/jpeg')).ok, false);
  const result = await g.evaluate({ title: 'Tropical Birds', material: 'paper', imageUrl: 'https://example.test/p.jpg' });
  assert.equal(result.verdict, 'HELD'); assert.equal(result.cost, 0);
  assert.equal(calls, 2); assert.equal(downloads, 0);
  assert.equal(result.reason, 'vision-rate-limit-cooldown');
});
test('cooldown expiration retries service; no fail-open during outage', async () => {
  const g = gate(); g.visionRetryAt = Date.now() - 1;
  g._visionCall = async () => ({ ok: true, partA: true, partB: true });
  const v = await g.visionDetect('bytes', 'image/jpeg');
  assert.equal(g.verdictFromVision(v).verdict, 'BLOCK');
  assert.equal(g.visionRetryAt, 0);
});
test('authentication and malformed response errors never fall through as PASS', async () => {
  for (const why of ['vision-http-401', 'vision-http-403', 'vision-parse', 'incomplete-verdict']) {
    const g = gate(); let calls = 0;
    g._visionCall = async () => { calls++; return { ok: false, why }; };
    assert.equal((await g.visionDetect('x', 'image/jpeg')).ok, false);
    assert.equal(calls, 1);
  }
});
test('HTTP request uses header auth and propagates response details', async () => {
  let options;
  const request = (opts, callback) => {
    options = opts;
    const req = new EventEmitter(); req.setTimeout = () => {};
    req.end = () => { const res = new EventEmitter(); res.statusCode = 429; res.headers = { 'retry-after': '90' };
      callback(res); res.emit('data', '{"error":{"status":"RESOURCE_EXHAUSTED"}}'); res.emit('end'); };
    return req;
  };
  const response = await requestVision({}, 'fake-secret', { request });
  assert.ok(!options.path.includes('fake-secret')); assert.equal(options.headers['x-goog-api-key'], 'fake-secret');
  assert.equal(errorDetails(response).retryAfterMs, 90000);
});
test('timeout resolves as an unavailable service rather than hanging', async () => {
  const request = () => {
    const req = new EventEmitter(); let timeout;
    req.setTimeout = (_, callback) => { timeout = callback; };
    req.destroy = error => req.emit('error', error);
    req.end = () => timeout(); return req;
  };
  const response = await requestVision({}, 'fake-secret', { request });
  assert.equal(response.status, 0); assert.equal(response.err, 'vision-request-timeout');
});