[grade=B urn:ump:35dpnggtxsqrxvlqm2d2buqkbnggw3acflbccnkqpqyiiijcxx3q] fix(DC-170): MCP truth batch — 6 dead tools fixed, full-catalog drift gate
Audit P0-5 + deploy_app truth (batch 5 of the 2026-10-27 sweep, AUDIT-2026-10-27.md committed here): ROUTE ADDITIONS: - routes/containers.js GET / — container list (dockerode listContainers); the router had NO list route, so dashcaddy_list_containers 404'd always - routes/dns.js GET /records — record list via active provider with a list-records capability guard (501 + guidance for push-only providers like RFC2136); dashcaddy_list_dns called GET /dns, never existed HANDLER FIXES (src/mcp/mcp-core.js): - get_service resolves from GET /services by id/name (single-service GET never existed) with a known-ids 404 - check_health + diagnose -> /health/service/:id and /health/services (both previously hit nonexistent routes) - container_action remove -> DELETE /containers/:id (was /:id/remove) - system_health/system_metrics via apiCall error envelope (metrics text capped at 64KB) - search_catalog applies category filter client-side (/catalog/search has no category param — constraint was silently dropped) - generate_caddyfile defaults authService=status when auth:true (auth flag alone silently produced UNGATED Caddyfiles) - deploy_app honest re-contract: registers service + generates Caddyfile, does NOT claim image pull/container creation/deployed:true; base domain read from /config (hardcoded owner .sami TLD removed); caddy-code failure now surfaces as caddyfileError + FAILED message - list_fleet description gains Premium hint VERIFIER (audit #40): EXPECTED table 12 -> 34 rows covering EVERY tool's (method,path); NEW Layer-3 handler-derived test drives the real createToolHandler for all 28 tools and strictly resolves every captured call mount-aware (a vacuous first draft of this gate was caught and fixed pre-grade); extractRoutes regex extended to apiRouter.get inline decls; Bearer credential-passthrough test added (DC-162 coverage gap) NEW TESTS: dns-records.routes.test.js (5 route-level cases incl. 501 capability behavior); deploy_app partial-failure + no-.sami tests HOST RELIABILITY (same sweep, evidenced here): jest maxWorkers 50% — default 5-worker runs repeatedly OOM-killed this host's gateway (dmesg 2026-10-29 00:58:40; swap was 4G/4G full). Gateway protected separately via systemd OOMScoreAdjust=-500 drop-in + /swapfile2 (host ops, graded B urn:ump:35dpnggtxsqrxvlqm2d2buqkbnggw3acflbccnkqpqyiiijcxx3q). DC-171 DNS provider-parity ticket (Sami, plug-in-directly) appended to backlog. Full suite: 173/173 suites, 3740/3740 tests. Judge B r1 (urn:ump:jsobkg2t2omzvpinel2fufk7sspoy3vubsrth3e6lat3ygolvffq) blockers all fixed: dns.js 501 arg order (the file aliases error() message-first — the sweep's 'reversed-arg' finding was a false positive for its 7 sites; lesson #14), deploy_app partial-failure honesty, handler-derived verifier.
This commit is contained in:
9 files changed
+529
-40
No files matched your search
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* DC-170/DC-171: GET /api/v1/dns/records — the DNS record-listing route added
|
||||
* so the MCP dashcaddy_list_dns tool (and any client) can list records via the
|
||||
* ACTIVE provider, with honest capability behavior:
|
||||
* - provider supports list-records -> { zone, count, records }
|
||||
* - provider without list-records (e.g. RFC2136 push-only) -> 501 + guidance
|
||||
* - no active provider resolvable -> 501
|
||||
* Also pins the zone passthrough and the siteConfig.tld default.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
function buildApp(dnsDep, siteConfig) {
|
||||
const dnsRoutes = require('../../routes/dns');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/dns', dnsRoutes({
|
||||
dns: dnsDep,
|
||||
siteConfig: siteConfig || { tld: '.example.net' },
|
||||
asyncHandler: (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next),
|
||||
log: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
||||
safeErrorMessage: (e) => (e && e.message) || 'error',
|
||||
fetchT: async () => { throw new Error('not used here'); },
|
||||
credentialManager: { retrieve: async () => null },
|
||||
dnsPropagationChecker: {},
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
const GOOD_PROVIDER = {
|
||||
supportsCapability: (cap) => cap === 'list-records',
|
||||
listRecords: jest.fn(async ({ zone }) => ({ records: [
|
||||
{ name: `host.${zone}`, type: 'A', value: '10.0.0.5' },
|
||||
{ name: `nas.${zone}`, type: 'A', value: '10.0.0.6' },
|
||||
] })),
|
||||
};
|
||||
|
||||
describe('GET /dns/records (DC-170 route + DC-171 capability behavior)', () => {
|
||||
test('lists records via the active provider when list-records is supported', async () => {
|
||||
const dns = { getActiveProvider: () => GOOD_PROVIDER };
|
||||
const res = await request(buildApp(dns)).get('/dns/records');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.zone).toBe('example.net'); // from siteConfig.tld
|
||||
expect(res.body.count).toBe(2);
|
||||
expect(res.body.records[0]).toMatchObject({ type: 'A', value: '10.0.0.5' });
|
||||
expect(GOOD_PROVIDER.listRecords).toHaveBeenCalledWith({ zone: 'example.net' });
|
||||
});
|
||||
|
||||
test('zone query param overrides the siteConfig.tld default', async () => {
|
||||
const dns = { getActiveProvider: () => GOOD_PROVIDER };
|
||||
const res = await request(buildApp(dns)).get('/dns/records?zone=custom.zone');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.zone).toBe('custom.zone');
|
||||
expect(GOOD_PROVIDER.listRecords).toHaveBeenCalledWith({ zone: 'custom.zone' });
|
||||
});
|
||||
|
||||
test('provider WITHOUT list-records capability answers 501 with guidance', async () => {
|
||||
// RFC2136-style: push-only protocol, cannot enumerate records
|
||||
const dns = { getActiveProvider: () => ({ supportsCapability: () => false }) };
|
||||
const res = await request(buildApp(dns)).get('/dns/records');
|
||||
expect(res.status).toBe(501);
|
||||
expect(String(res.body.error || res.body.message)).toMatch(/not supported/i);
|
||||
});
|
||||
|
||||
test('missing/unresolvable active provider answers 501, does not crash', async () => {
|
||||
const dns = {}; // no getActiveProvider at all
|
||||
const res = await request(buildApp(dns)).get('/dns/records');
|
||||
expect(res.status).toBe(501);
|
||||
});
|
||||
|
||||
test('provider listRecords failure surfaces as 500 without leaking internals', async () => {
|
||||
const dns = { getActiveProvider: () => ({
|
||||
supportsCapability: () => true,
|
||||
listRecords: async () => { throw new Error('upstream token expired with secret abc'); },
|
||||
}) };
|
||||
const res = await request(buildApp(dns)).get('/dns/records');
|
||||
expect([500, 502]).toContain(res.status);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user