Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/training-agent-property-list-uniform-errors.md
Original file line number Diff line number Diff line change
@@ -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 '<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.
8 changes: 4 additions & 4 deletions server/src/training-agent/property-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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 };
Expand All @@ -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 || [];
Expand Down
44 changes: 44 additions & 0 deletions server/tests/unit/training-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading