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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,17 @@ Supported options:
- `-o, --outputfile` write output to file instead of stdout
- `-t, --testMode` enable diagnostics mode and generate one row
- `--unsafe-faker-expressions` allow expression-style faker args (disabled by default)
- `--trim-input` trim whitespace from every imported field value before amend processing
- `--trim-input-fields` comma-separated imported field names to trim before amend processing

Amend existing data with a schema:

`anywaydata amend --schema-file schema.txt --data-file input.csv --input-format csv -f json -o amended.json`

Trim imported input values during amend:

`anywaydata amend --schema-file schema.txt --data-file input.csv --input-format csv --trim-input-fields Name,Email -f json`

Pairwise note:

- when `--pairwise` is enabled, `--numberOfLines` is accepted but ignored (the pairwise engine determines row count)
Expand Down Expand Up @@ -370,6 +376,7 @@ Amend imported data endpoint:
`POST http://localhost:3000/v1/generate/amend`

- JSON body fields: `textSpec` (required), `inputData` (required raw text), `inputFormat` (required), `rowCount` (optional, defaults to input row count), `outputFormat` (optional), `responseFormat` (optional), `stream` (optional and ignored)
- amend trim controls: `trimInput` trims all imported field values; `trimInputFieldsCsv` trims only the listed imported field names
- `rowCount` must be `<=` imported row count
- response returns the full resulting dataset after amendment

Expand Down Expand Up @@ -499,6 +506,8 @@ Inputs:
- `outputFormat` (required string e.g. `csv`, `json`, `jsonl`, `xml`, `sql`)
- `options` (optional object)
- `seed` (optional number)
- `trimInput` (optional boolean for `amend_data_from_spec`)
- `trimInputFieldsCsv` (optional string for `amend_data_from_spec`)

Discoverability support:

Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/api-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export function createApiService({
outputFormat = 'csv',
options,
seed,
trimInput,
trimInputFieldsCsv,
unsafeFakerExpressions,
stream,
} = payload;
Expand Down Expand Up @@ -242,6 +244,8 @@ export function createApiService({
outputFormat: concreteOutputFormat,
options: effectiveOptions,
seed: parsedSeed.seed,
trimInput: parseBooleanFlag(trimInput),
trimInputFieldsCsv,
unsafeFakerExpressions: unsafeFakerExpressions || false,
stream,
});
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,16 @@ const openApiDocument = {
outputFormat: { type: 'string', default: 'csv' },
options: { type: 'object' },
seed: { type: 'number' },
trimInput: {
type: 'boolean',
default: false,
description: 'Trim whitespace from every imported input field value before amend processing.',
},
trimInputFieldsCsv: {
type: 'string',
description:
'Comma-separated imported field names whose values should be trimmed before amend processing.',
},
unsafeFakerExpressions: { type: 'boolean', default: false },
responseFormat: { type: 'string', enum: ['rows', 'rendered', 'all', 'raw'], default: 'rows' },
stream: { type: 'boolean', description: 'Accepted for compatibility and ignored for amend mode.' },
Expand Down
17 changes: 17 additions & 0 deletions apps/api/src/tests/generate/v1-generate-amend.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,21 @@ test.describe('POST /v1/generate/amend', () => {
expect(Array.isArray(body.diagnostics)).toBe(false);
expect(typeof body.diagnostics).toBe('object');
});

test('amend trimInputFieldsCsv trims only listed imported columns', async ({ request }) => {
const response = await request.post(apiUrl('/v1/generate/amend'), {
data: {
textSpec: 'Status\nActive',
inputData: '"Name","Role"\n" Alice "," Engineer "',
inputFormat: 'csv',
rowCount: 0,
outputFormat: 'json',
trimInputFieldsCsv: 'Name',
},
});

expect(response.status()).toBe(200);
const body = await response.json();
expect(body.rows).toEqual([['Alice', ' Engineer ', '']]);
});
});
6 changes: 6 additions & 0 deletions apps/api/src/tests/health/health-endpoints.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ test.describe('Health and docs endpoints', () => {
expect(body.openapi).toBe('3.0.3');
expect(body.paths['/v1/generate']).toBeTruthy();
expect(body.paths['/v1/generate/amend']).toBeTruthy();
expect(
body.paths['/v1/generate/amend'].post.requestBody.content['application/json'].schema.properties.trimInput
).toBeTruthy();
expect(
body.paths['/v1/generate/amend'].post.requestBody.content['application/json'].schema.properties.trimInputFieldsCsv
).toBeTruthy();
});

test('GET /v1/docs serves Swagger UI html', async ({ request }) => {
Expand Down
11 changes: 11 additions & 0 deletions apps/cli/src/cli-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export function parseCliOptions(argvInput = process.argv) {
.option('schema-file', { type: 'string', describe: 'Schema file path for amend command' })
.option('data-file', { type: 'string', describe: 'Input data file path for amend command' })
.option('input-format', { type: 'string', describe: 'Input data format for amend command (e.g. csv, json)' })
.option('trim-input', {
type: 'boolean',
default: false,
describe: 'Trim whitespace from every imported input field value before amend processing',
})
.option('trim-input-fields', {
type: 'string',
describe: 'Comma-separated imported field names whose values should be trimmed before amend processing',
})
.option('n', {
alias: 'numberOfLines',
type: 'number',
Expand Down Expand Up @@ -93,5 +102,7 @@ export function parseCliOptions(argvInput = process.argv) {
bom: parsed.bom === true,
unsafeFakerExpressions: parsed['unsafe-faker-expressions'] === true,
pairwise: parsed.pairwise === true,
trimInput: command === 'amend' && parsed['trim-input'] === true,
trimInputFieldsCsv: command === 'amend' ? parsed['trim-input-fields'] || '' : '',
};
}
2 changes: 2 additions & 0 deletions apps/cli/src/run-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export async function runCliCommand({ options, platform }) {
rowCount: options.rowCount,
outputFormat,
options: formatterOptions,
trimInput: options.trimInput === true,
trimInputFieldsCsv: options.trimInputFieldsCsv || '',
unsafeFakerExpressions: options.unsafeFakerExpressions,
stream: options.shouldStream,
});
Expand Down
5 changes: 5 additions & 0 deletions apps/cli/src/tests/cli-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ test('amend command options are parsed', () => {
'data.csv',
'--input-format',
'csv',
'--trim-input',
'--trim-input-fields',
'Name,Role',
'-n',
'3',
'-f',
Expand All @@ -63,6 +66,8 @@ test('amend command options are parsed', () => {
expect(opts.inputFormat).toBe('csv');
expect(opts.rowCount).toBe(3);
expect(opts.shouldStream).toBe(false);
expect(opts.trimInput).toBe(true);
expect(opts.trimInputFieldsCsv).toBe('Name,Role');
});

test('export encoding flags are parsed', () => {
Expand Down
27 changes: 27 additions & 0 deletions apps/cli/src/tests/integration.cli-params.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,33 @@ test('amend command applies schema to existing data', async () => {
expect(JSON.parse(result.stdout.trim())).toEqual([{ Name: 'Bob' }, { Name: 'Eve' }]);
});

test('amend command trim flags affect imported input values before amend processing', async () => {
const schemaPath = tempFile('trim-schema');
const dataPath = tempFile('trim-data');
await fs.writeFile(schemaPath, 'Status\nActive', 'utf8');
await fs.writeFile(dataPath, '"Name","Role"\n" Alice "," Engineer "', 'utf8');

const result = runCli([
'amend',
'--schema-file',
schemaPath,
'--data-file',
dataPath,
'--input-format',
'csv',
'--trim-input-fields',
'Name',
'-n',
'0',
'-f',
'json',
'--show-progress',
'false',
]);
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout.trim())).toEqual([{ Name: 'Alice', Role: ' Engineer ', Status: '' }]);
});

test('amend command fixture flow: CSV input to DSV output on stdout', async () => {
const schemaPath = path.join(amendFixturesDir, 'schema.txt');
const dataPath = path.join(amendFixturesDir, 'input.csv');
Expand Down
9 changes: 9 additions & 0 deletions apps/mcp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,15 @@ function handleRequest(request) {
outputFormat: { type: 'string', enum: SUPPORTED_FORMATS },
options: GENERATE_OPTIONS_SCHEMA,
seed: { type: 'number' },
trimInput: {
type: 'boolean',
description: 'Trim whitespace from every imported input field value before amend processing.',
},
trimInputFieldsCsv: {
type: 'string',
description:
'Comma-separated imported field names whose values should be trimmed before amend processing.',
},
stream: {
type: 'boolean',
description: 'Accepted for compatibility and ignored for amend operation (always buffered).',
Expand Down
29 changes: 29 additions & 0 deletions apps/mcp/src/mcp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,35 @@ test('MCP server handles amend_data_from_spec tool call', () => {
expect((payload.diagnostics?.warnings || []).join(' ')).toContain('stream is ignored');
});

test('MCP amend tool schema includes import trim options', () => {
const response = requestServer({ jsonrpc: '2.0', id: 1500, method: 'tools/list' });
const amendTool = response?.result?.tools?.find((tool) => tool.name === 'amend_data_from_spec');
expect(amendTool?.inputSchema?.properties?.trimInput?.type).toBe('boolean');
expect(amendTool?.inputSchema?.properties?.trimInputFieldsCsv?.type).toBe('string');
});

test('MCP amend tool applies trimInputFieldsCsv to imported values', () => {
const response = requestServer({
jsonrpc: '2.0',
id: 1501,
method: 'tools/call',
params: {
name: 'amend_data_from_spec',
arguments: {
textSpec: 'Status\nActive',
inputData: '"Name","Role"\n" Alice "," Engineer "',
inputFormat: 'csv',
rowCount: 0,
outputFormat: 'json',
trimInputFieldsCsv: 'Name',
},
},
});
const payload = JSON.parse(response?.result?.content?.[0]?.text || '{}');
expect(payload.ok).toBe(true);
expect(payload.rows).toEqual([['Alice', ' Engineer ', '']]);
});

test('MCP amend parity matches core', () => {
const coreResult = amendFromTextSpecAndData({
textSpec: 'Name\nBob',
Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/stories/import-export-toolbar.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ function renderImportExportToolbarStory(args) {
exportStatusMessage: args.exportStatusMessage,
exportStatusLoading: args.exportStatusLoading,
errorStatusMessage: args.errorStatusMessage,
trimInput: args.trimInput,
trimInputFieldsEnabled: args.trimInputFieldsEnabled,
trimInputFieldsCsv: args.trimInputFieldsCsv,
},
callbacks: {
onDownload: () => {
Expand All @@ -47,6 +50,10 @@ function renderImportExportToolbarStory(args) {
args.onImportFromClipboard?.();
result.textContent = 'action:import-from-clipboard';
},
onImportTrimSettingsChange: (settings) => {
args.onImportTrimSettingsChange?.(settings);
result.textContent = `trim:${JSON.stringify(settings)}`;
},
},
});

Expand Down Expand Up @@ -77,6 +84,7 @@ const meta = {
React.createElement(Controls),
React.createElement(Canvas, { of: Default }),
React.createElement(Canvas, { of: FileImportSurface }),
React.createElement(Canvas, { of: ImportTrimSettings }),
React.createElement(Canvas, { of: BusyAndStatus })
),
description: {
Expand All @@ -98,9 +106,13 @@ const meta = {
exportStatusMessage: '',
exportStatusLoading: false,
errorStatusMessage: '',
trimInput: false,
trimInputFieldsEnabled: false,
trimInputFieldsCsv: '',
onDownload: fn(),
onFileSelected: fn(),
onImportFromClipboard: fn(),
onImportTrimSettingsChange: fn(),
},
argTypes: {
fileExtension: {
Expand Down Expand Up @@ -143,6 +155,18 @@ const meta = {
control: 'text',
description: 'Persistent error/status message shown at the end of the toolbar.',
},
trimInput: {
control: 'boolean',
description: 'When true, imported input values are trimmed before import/amend processing.',
},
trimInputFieldsEnabled: {
control: 'boolean',
description: 'Enables field-list trimming in the import settings disclosure.',
},
trimInputFieldsCsv: {
control: 'text',
description: 'Comma-separated imported field names trimmed when selected-fields mode is enabled.',
},
onDownload: {
description: 'Storybook action fired when Download is clicked.',
table: { category: 'Events' },
Expand All @@ -155,6 +179,10 @@ const meta = {
description: 'Storybook action fired when From Clipboard is clicked.',
table: { category: 'Events' },
},
onImportTrimSettingsChange: {
description: 'Storybook action fired when the import trim settings change.',
table: { category: 'Events' },
},
},
render: renderImportExportToolbarStory,
};
Expand Down Expand Up @@ -236,3 +264,28 @@ export const BusyAndStatus = {
await expect(canvas.getByText('Generating export text...')).toBeVisible();
},
};

export const ImportTrimSettings = {
args: {
trimInput: true,
trimInputFieldsEnabled: true,
trimInputFieldsCsv: 'Name, Email',
},
render: renderImportExportToolbarStory,
parameters: {
docs: {
description: {
story:
'Documents the import-only trim settings behind the import settings disclosure. Toggle the radio groups and edit the CSV textbox to confirm the emitted settings payload shown in the story log.',
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByLabelText('Import settings'));
const textInput = canvas.getByRole('textbox', { name: /trim input fields csv/i });
await userEvent.clear(textInput);
await userEvent.type(textInput, 'Name, Role');
await expect(canvas.getByText(/"trimInputFieldsCsv":"Name, Role"/)).toBeVisible();
},
};
Comment on lines 264 to +291

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Story play action doesn't cover the documented radio-group modes

The ImportTrimSettings story description says "Toggle the radio groups and edit the CSV textbox to confirm the emitted settings payload", but the play function only exercises the CSV text input — the radio group interactions are left for reviewers to do manually. Per the AGENTS.md Storybook quality rules, when docs describe behavior modes, each mode should have its own primary story with a play interaction that clearly demonstrates it. As-is, the trim-all (trimInput: on) mode and the per-field (trimInputFieldsEnabled: selected-fields) mode are described in prose but never automated. A reviewer reading the story docs won't have a verified example for either radio path.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Loading
Loading