diff --git a/.changeset/training-agent-property-list-uniform-errors.md b/.changeset/training-agent-property-list-uniform-errors.md new file mode 100644 index 0000000000..0063bfa1a2 --- /dev/null +++ b/.changeset/training-agent-property-list-uniform-errors.md @@ -0,0 +1,6 @@ +--- +--- + +Training agent: `get_property_list`, `update_property_list`, `delete_property_list`, and `validate_property_delivery` now return a uniform error on unresolved `list_id` — `error.code` is `REFERENCE_NOT_FOUND` (was lowercase `not_found`), `error.message` is the generic `"Property list not found"` (was `"No property list with id ''"`), and `error.field` is `list_id`. Closes #2739. + +The probed id is no longer echoed back on the error path, so paired-probe uniform-response invariants (e.g. `@adcp/client` fuzz) now pass against the public test agent. This matters because the training agent is the reference implementors point conformance runs at; an echoing message there invalidates the signal for every downstream seller running the same invariant. diff --git a/server/src/training-agent/property-handlers.ts b/server/src/training-agent/property-handlers.ts index 3fde648464..8739656c01 100644 --- a/server/src/training-agent/property-handlers.ts +++ b/server/src/training-agent/property-handlers.ts @@ -257,7 +257,7 @@ export async function handleGetPropertyList( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } const domains = extractDomains(state.baseProperties); @@ -280,7 +280,7 @@ export async function handleUpdatePropertyList( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } if (req.name) { @@ -324,7 +324,7 @@ export async function handleDeletePropertyList( const existed = session.propertyLists.delete(req.list_id); if (!existed) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } return { list_id: req.list_id, deleted: true }; @@ -343,7 +343,7 @@ export async function handleValidatePropertyDelivery( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } const records = req.records || []; diff --git a/server/tests/unit/training-agent.test.ts b/server/tests/unit/training-agent.test.ts index e8c603d0ba..2f60630b5e 100644 --- a/server/tests/unit/training-agent.test.ts +++ b/server/tests/unit/training-agent.test.ts @@ -6860,6 +6860,50 @@ describe('get_brand_identity handler', () => { }); }); +describe('property-list uniform not-found response (issue #2739)', () => { + beforeEach(async () => { + await clearSessions(); + }); + afterEach(async () => { + await clearSessions(); + stopSessionCleanup(); + }); + + // Paired-probe: two distinct unresolvable list_ids must produce byte-identical + // error bodies, otherwise the probed id is a cross-tenant enumeration oracle. + const PROBE_TOOLS = ['get_property_list', 'update_property_list', 'delete_property_list'] as const; + for (const toolName of PROBE_TOOLS) { + it(`${toolName} returns byte-identical errors for two distinct unresolvable list_ids`, async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const account = { brand: { domain: 'uniform-probe.example' }, operator: 'pinnacle-agency.com' }; + + const probeA = await simulateCallTool(server, toolName, { account, list_id: 'd7aff8ea-136c-498f-b70f-a69582ad3bec' }); + const probeB = await simulateCallTool(server, toolName, { account, list_id: '221acd34-cd2c-4763-ae0a-321c1e85fb2b' }); + + expect(probeA.isError).toBe(probeB.isError); + expect(probeA.result).toEqual(probeB.result); + expect(probeA.result.code).toBe('REFERENCE_NOT_FOUND'); + expect(probeA.result.message).toBe('Property list not found'); + expect(probeA.result.field).toBe('list_id'); + }); + } + + it('validate_property_delivery returns byte-identical errors for two distinct unresolvable list_ids', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const account = { brand: { domain: 'uniform-probe.example' }, operator: 'pinnacle-agency.com' }; + const records = [{ identifier: { type: 'domain', value: 'x.example' }, impressions: 1 }]; + + const probeA = await simulateCallTool(server, 'validate_property_delivery', { account, list_id: 'd7aff8ea-136c-498f-b70f-a69582ad3bec', records }); + const probeB = await simulateCallTool(server, 'validate_property_delivery', { account, list_id: '221acd34-cd2c-4763-ae0a-321c1e85fb2b', records }); + + expect(probeA.isError).toBe(probeB.isError); + expect(probeA.result).toEqual(probeB.result); + expect(probeA.result.code).toBe('REFERENCE_NOT_FOUND'); + expect(probeA.result.message).toBe('Property list not found'); + expect(probeA.result.field).toBe('list_id'); + }); +}); + describe('cross-machine session persistence', () => { beforeEach(async () => { await clearSessions();