Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
/**
|
|
* Regression test for BUG-MCP-001: update_table_row contamination.
|
|
*
|
|
* MCP/agent callers occasionally pass `data` as a JSON-encoded string
|
|
* instead of a parsed object. Without normalization, downstream
|
|
* `JSON.stringify(data)` writes a quoted string into JSONB
|
|
* (jsonb_typeof = 'string'). coerceDataObject is the entry guard.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { coerceDataObject } from '../services/agent-tools/coerceDataInput.js';
|
|
|
|
describe('coerceDataObject (BUG-MCP-001)', () => {
|
|
it('passes through plain objects unchanged', () => {
|
|
const input = { content_en: 'x', comment: 'y' };
|
|
expect(coerceDataObject(input)).toBe(input);
|
|
});
|
|
|
|
it('parses a JSON-encoded string into an object', () => {
|
|
const json = '{"content_en":"x","comment":"y","order":540}';
|
|
expect(coerceDataObject(json)).toEqual({
|
|
content_en: 'x',
|
|
comment: 'y',
|
|
order: 540,
|
|
});
|
|
});
|
|
|
|
it('rejects strings that are not valid JSON', () => {
|
|
expect(() => coerceDataObject('not json')).toThrow(/must be a JSON object/);
|
|
});
|
|
|
|
it('rejects strings that decode to a primitive', () => {
|
|
expect(() => coerceDataObject('"just a string"')).toThrow(
|
|
/must decode to a JSON object/
|
|
);
|
|
expect(() => coerceDataObject('42')).toThrow(/must decode to a JSON object/);
|
|
expect(() => coerceDataObject('true')).toThrow(/must decode to a JSON object/);
|
|
expect(() => coerceDataObject('null')).toThrow(/must decode to a JSON object; got null/);
|
|
});
|
|
|
|
it('rejects strings that decode to an array', () => {
|
|
expect(() => coerceDataObject('[1,2,3]')).toThrow(
|
|
/must decode to a JSON object/
|
|
);
|
|
});
|
|
|
|
it('rejects arrays passed directly', () => {
|
|
expect(() => coerceDataObject([1, 2, 3])).toThrow(/got array/);
|
|
});
|
|
|
|
it('rejects primitives passed directly', () => {
|
|
expect(() => coerceDataObject(42)).toThrow(/got number/);
|
|
expect(() => coerceDataObject(true)).toThrow(/got boolean/);
|
|
});
|
|
|
|
it('returns null for null/undefined (caller decides whether required)', () => {
|
|
expect(coerceDataObject(null)).toBeNull();
|
|
expect(coerceDataObject(undefined)).toBeNull();
|
|
});
|
|
|
|
it('uses the fieldName label in error messages', () => {
|
|
expect(() => coerceDataObject(42, 'updates[].data')).toThrow(
|
|
/updates\[\]\.data must/
|
|
);
|
|
});
|
|
|
|
// Direct repro of the BUG-MCP-001 corruption path:
|
|
// before the fix, `JSON.stringify(stringInput)` would yield a quoted
|
|
// double-encoded string like `"{\"a\":1}"`, which Postgres stored as
|
|
// jsonb_typeof = 'string'. After coercion the payload round-trips cleanly.
|
|
it('round-trips through JSON.stringify without double-encoding', () => {
|
|
const stringInput = '{"a":1,"b":"hello"}';
|
|
const coerced = coerceDataObject(stringInput);
|
|
const stored = JSON.stringify(coerced);
|
|
expect(stored).toBe('{"a":1,"b":"hello"}');
|
|
expect(stored.startsWith('"')).toBe(false);
|
|
});
|
|
});
|