From 65a447e27ebc849341085d45a22ba47b6bb7a06e Mon Sep 17 00:00:00 2001 From: DashCaddy Polish Loop Date: Mon, 14 Sep 2026 04:19:55 -0700 Subject: [PATCH] DC-130 rev3: log.error on unexpected bridge status in /status + test asserting the log call (13 tests green) --- .../__tests__/routes/deploys.routes.test.js | 27 ++++++++++++++++--- dashcaddy-api/routes/deploys.js | 7 ++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/dashcaddy-api/__tests__/routes/deploys.routes.test.js b/dashcaddy-api/__tests__/routes/deploys.routes.test.js index 4f7a74e..f408c34 100644 --- a/dashcaddy-api/__tests__/routes/deploys.routes.test.js +++ b/dashcaddy-api/__tests__/routes/deploys.routes.test.js @@ -19,6 +19,10 @@ const FIXTURE_ROWS = { }; function buildApp(fetchT, env = {}) { + return buildAppWithLogCapture(fetchT, env, { info: jest.fn(), warn: jest.fn(), error: jest.fn() }); +} + +function buildAppWithLogCapture(fetchT, env = {}, log) { process.env.SHIPDECK_BRIDGE_URL = env.url !== undefined ? env.url : 'http://172.17.0.1:8977'; process.env.SHIPDECK_BRIDGE_TOKEN_FILE = env.tokenFile !== undefined ? env.tokenFile : ''; jest.resetModules(); @@ -27,7 +31,7 @@ function buildApp(fetchT, env = {}) { asyncHandler: (fn) => async (req, res, next) => { try { await fn(req, res, next); } catch (e) { next(e); } }, - log: { info: jest.fn(), warn: jest.fn(), error: jest.fn() }, + log, auditLogger: { log: jest.fn(async () => {}) }, fetchT, }); @@ -41,11 +45,20 @@ function buildApp(fetchT, env = {}) { return app; } +// expose log on the fetcher wrapper for log-capture assertions +function jsonFetcherWithLog(responses) { + const calls = []; + const log = { info: jest.fn(), warn: jest.fn(), error: jest.fn() }; + return { calls, log, fetchT: null }; +} + function jsonFetcher(responses) { // responses: map of "METHOD path" -> {status, body} const calls = []; - return { + const log = { info: jest.fn(), warn: jest.fn(), error: jest.fn() }; + const wrapper = { calls, + log, fetchT: jest.fn(async (url, opts) => { const key = `${(opts && opts.method) || 'GET'} ${url.replace(/^https?:\/\/[^/]+/, '')}`; calls.push({ key, opts }); @@ -56,6 +69,7 @@ function jsonFetcher(responses) { }; }), }; + return wrapper; } describe('routes/deploys — feature gate', () => { @@ -155,16 +169,21 @@ describe('routes/deploys — proxied endpoints', () => { expect(body.error).toMatch(/bridge/i); }); - test('GET /status maps unexpected bridge statuses to 502', async () => { + test('GET /status maps unexpected bridge statuses to 502 with error logging', async () => { const f = jsonFetcher({ 'GET /api/status?service=helloworld': { status: 404, body: { ok: false, error: 'not found' } }, }); - const app = buildApp(f.fetchT); + const app = buildAppWithLogCapture(f.fetchT, {}, f.log); const server = app.listen(0); const port = server.address().port; const r = await fetch(`http://127.0.0.1:${port}/api/v1/deploys/status?service=helloworld`); server.close(); expect(r.status).toBe(502); + expect(f.log.error).toHaveBeenCalledWith( + 'deploys', + 'status probe: unexpected bridge response', + expect.objectContaining({ service: 'helloworld', status: 404 }), + ); }); test('GET /status maps bridge connection failure to 502', async () => { diff --git a/dashcaddy-api/routes/deploys.js b/dashcaddy-api/routes/deploys.js index 7d004d1..36935de 100644 --- a/dashcaddy-api/routes/deploys.js +++ b/dashcaddy-api/routes/deploys.js @@ -136,7 +136,12 @@ module.exports = function ({ asyncHandler, log, auditLogger, fetchT }) { } if (status !== 200) { // malformed upstream route or other unexpected bridge failure - return errorResponse(res, 502, body && body.error ? body.error : 'unexpected bridge response'); + log.error('deploys', 'status probe: unexpected bridge response', { + service, + status, + bridgeError: body && body.error ? String(body.error).slice(0, 200) : 'none', + }); + return errorResponse(res, 502, 'shipdeck bridge protocol error (unexpected response, status ' + status + ')'); } return ok(res, { ok: body.ok === true, output: body.output || '' }); }));