Compare commits

...

2 Commits

Author SHA1 Message Date
Bryan Thompson
2713db6d10 feat: add MCP tool annotations to all 20 tools (#512)
* feat: add MCP tool annotations to all 20 tools

Add MCP tool annotations per specification to help AI assistants
understand tool behavior and capabilities.

Documentation tools (7):
- tools_documentation, search_nodes, get_node, validate_node,
  get_template, search_templates, validate_workflow
- All marked readOnlyHint=true (local database queries)

Management tools (13):
- n8n_create_workflow, n8n_get_workflow, n8n_update_full_workflow,
  n8n_update_partial_workflow, n8n_delete_workflow, n8n_list_workflows,
  n8n_validate_workflow, n8n_autofix_workflow, n8n_test_workflow,
  n8n_executions, n8n_health_check, n8n_workflow_versions,
  n8n_deploy_template
- All marked openWorldHint=true (n8n API access)
- Destructive operations (delete_workflow, executions delete,
  workflow_versions delete/truncate) marked destructiveHint=true

Annotations follow MCP spec:
https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add idempotentHint to all read-only tools

Adds idempotentHint: true annotation to all read-only tools that produce
the same output when called multiple times:
- 7 documentation tools (tools.ts)
- 4 management tools (tools-n8n-manager.ts): n8n_get_workflow,
  n8n_list_workflows, n8n_validate_workflow, n8n_health_check

Also adds trailing newline to tools-n8n-manager.ts.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add idempotentHint to update operations, bump to 2.31.5

Adds idempotentHint: true to update operations that produce the same
result when called repeatedly with the same arguments:
- n8n_update_full_workflow
- n8n_update_partial_workflow
- n8n_autofix_workflow

Also bumps version to 2.31.5 and updates CHANGELOG.md with complete
documentation of all MCP tool annotations added in this PR.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: triepod-ai <noreply@github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local>
2026-01-02 15:48:47 +01:00
Romuald Członkowski
f10772a9d2 fix: preserve workflow data during serialization (Issue #517) (#519)
Fixed a critical bug where workflow mutation data was corrupted during
serialization to Supabase. The recursive toSnakeCase() function was
converting nested workflow data, mangling:
- Connection keys (node names like "Webhook" → "_webhook")
- Node field names (typeVersion → type_version)

Solution: Replace recursive conversion with selective mutationToSupabaseFormat()
that only converts top-level field names to snake_case while preserving
nested workflow data exactly as-is.

Impact:
- 98.9% of workflow mutations had corrupted data
- Deployability rate improved from ~21% to ~68%

Changes:
- src/telemetry/batch-processor.ts: New selective converter
- tests/unit/telemetry/batch-processor.test.ts: 3 new regression tests

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 10:44:13 +01:00
7 changed files with 499 additions and 37 deletions

View File

@@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.31.5] - 2026-01-02
### Added
**MCP Tool Annotations (PR #512)**
Added MCP tool annotations to all 20 tools following the [MCP specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations). These annotations help AI assistants understand tool behavior and capabilities.
**Annotations added:**
- `title`: Human-readable name for each tool
- `readOnlyHint`: True for tools that don't modify state (11 tools)
- `destructiveHint`: True for delete operations (3 tools)
- `idempotentHint`: True for operations that produce same result when called repeatedly (14 tools)
- `openWorldHint`: True for tools accessing external n8n API (13 tools)
**Documentation tools** (7): All marked `readOnlyHint=true`, `idempotentHint=true`
- `tools_documentation`, `search_nodes`, `get_node`, `validate_node`, `get_template`, `search_templates`, `validate_workflow`
**Management tools** (13): All marked `openWorldHint=true`
- Read-only: `n8n_get_workflow`, `n8n_list_workflows`, `n8n_validate_workflow`, `n8n_health_check`
- Idempotent updates: `n8n_update_full_workflow`, `n8n_update_partial_workflow`, `n8n_autofix_workflow`
- Destructive: `n8n_delete_workflow`, `n8n_executions` (delete action), `n8n_workflow_versions` (delete/truncate)
## [2.31.4] - 2026-01-02
### Fixed
**Workflow Data Mangled During Serialization: snake_case Conversion (Issue #517)**
Fixed a critical bug where workflow mutation data was corrupted during serialization to Supabase, making 98.9% of collected workflow data invalid for n8n API operations.
**Problem:**
The `toSnakeCase()` function in `batch-processor.ts` was applied **recursively** to the entire mutation object, including nested workflow data that should be preserved exactly as-is:
- **Connection keys mangled**: Node names like `"Webhook"` became `"_webhook"`, `"AI Agent"` became `"_a_i _agent"`
- **Node field names mangled**: n8n camelCase fields like `typeVersion`, `webhookId`, `onError` became `type_version`, `webhook_id`, `on_error`
**Root Cause:**
```javascript
// Old code - recursive conversion corrupted nested data
result[snakeKey] = toSnakeCase(obj[key]); // WRONG
```
**Solution:**
Replaced recursive `toSnakeCase()` with selective `mutationToSupabaseFormat()` that:
- Converts **only** top-level field names to snake_case (for Supabase columns)
- Preserves all nested data (workflow JSON, operations, validations) **exactly as-is**
```javascript
// New code - preserves nested workflow structure
for (const [key, value] of Object.entries(mutation)) {
result[keyToSnakeCase(key)] = value; // Value preserved as-is
}
```
**Impact:**
- Workflow mutation data now maintains n8n API compatibility
- Deployability rate improved from ~21% to ~68%
- Added 3 regression tests to prevent future occurrences
## [2.31.3] - 2025-12-26
### Fixed

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.31.3",
"version": "2.31.5",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -66,7 +66,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['name', 'nodes', 'connections']
}
},
annotations: {
title: 'Create Workflow',
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
},
{
name: 'n8n_get_workflow',
@@ -86,7 +92,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['id']
}
},
annotations: {
title: 'Get Workflow',
readOnlyHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_update_full_workflow',
@@ -120,7 +132,14 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['id']
}
},
annotations: {
title: 'Update Full Workflow',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_update_partial_workflow',
@@ -151,7 +170,14 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['id', 'operations']
}
},
annotations: {
title: 'Update Partial Workflow',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_delete_workflow',
@@ -165,7 +191,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['id']
}
},
annotations: {
title: 'Delete Workflow',
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_list_workflows',
@@ -194,12 +226,18 @@ export const n8nManagementTools: ToolDefinition[] = [
type: 'string',
description: 'Filter by project ID (enterprise feature)'
},
excludePinnedData: {
type: 'boolean',
description: 'Exclude pinned data from response (default: true)'
excludePinnedData: {
type: 'boolean',
description: 'Exclude pinned data from response (default: true)'
}
}
}
},
annotations: {
title: 'List Workflows',
readOnlyHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_validate_workflow',
@@ -227,16 +265,22 @@ export const n8nManagementTools: ToolDefinition[] = [
type: 'boolean',
description: 'Validate n8n expressions (default: true)'
},
profile: {
type: 'string',
profile: {
type: 'string',
enum: ['minimal', 'runtime', 'ai-friendly', 'strict'],
description: 'Validation profile to use (default: runtime)'
description: 'Validation profile to use (default: runtime)'
}
}
}
},
required: ['id']
}
},
annotations: {
title: 'Validate Workflow',
readOnlyHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_autofix_workflow',
@@ -271,7 +315,14 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['id']
}
},
annotations: {
title: 'Autofix Workflow',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
// Execution Management Tools
@@ -328,7 +379,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['workflowId']
}
},
annotations: {
title: 'Test Workflow',
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
},
{
name: 'n8n_executions',
@@ -410,7 +467,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['action']
}
},
annotations: {
title: 'Manage Executions',
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
},
// System Tools
@@ -431,7 +494,13 @@ export const n8nManagementTools: ToolDefinition[] = [
description: 'Include extra details in diagnostic mode (default: false)'
}
}
}
},
annotations: {
title: 'Health Check',
readOnlyHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'n8n_workflow_versions',
@@ -485,7 +554,13 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['mode']
}
},
annotations: {
title: 'Workflow Versions',
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
},
// Template Deployment Tool
@@ -520,6 +595,12 @@ export const n8nManagementTools: ToolDefinition[] = [
}
},
required: ['templateId']
}
},
annotations: {
title: 'Deploy Template',
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
}
];
];

View File

@@ -25,6 +25,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
},
},
annotations: {
title: 'Tools Documentation',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'search_nodes',
@@ -55,6 +60,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
required: ['query'],
},
annotations: {
title: 'Search Nodes',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'get_node',
@@ -108,6 +118,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
required: ['nodeType'],
},
annotations: {
title: 'Get Node Info',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'validate_node',
@@ -188,6 +203,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
required: ['nodeType', 'displayName', 'valid']
},
annotations: {
title: 'Validate Node Config',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'get_template',
@@ -208,6 +228,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
required: ['templateId'],
},
annotations: {
title: 'Get Template',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'search_templates',
@@ -303,6 +328,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
},
},
annotations: {
title: 'Search Templates',
readOnlyHint: true,
idempotentHint: true,
},
},
{
name: 'validate_workflow',
@@ -388,6 +418,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
},
required: ['valid', 'summary']
},
annotations: {
title: 'Validate Workflow',
readOnlyHint: true,
idempotentHint: true,
},
},
];

View File

@@ -9,23 +9,34 @@ import { TelemetryError, TelemetryErrorType, TelemetryCircuitBreaker } from './t
import { logger } from '../utils/logger';
/**
* Convert camelCase object keys to snake_case
* Needed because Supabase PostgREST doesn't auto-convert
* Convert camelCase key to snake_case
*/
function toSnakeCase(obj: any): any {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(toSnakeCase);
if (typeof obj !== 'object') return obj;
function keyToSnakeCase(key: string): string {
return key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// Convert camelCase to snake_case
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
// Recursively convert nested objects
result[snakeKey] = toSnakeCase(obj[key]);
}
/**
* Convert WorkflowMutationRecord to Supabase-compatible format.
*
* IMPORTANT: Only converts top-level field names to snake_case.
* Nested workflow data (workflowBefore, workflowAfter, operations, etc.)
* is preserved EXACTLY as-is to maintain n8n API compatibility.
*
* The Supabase workflow_mutations table stores workflow_before and
* workflow_after as JSONB columns, which preserve the original structure.
* Only the top-level columns (user_id, session_id, etc.) require snake_case.
*
* Issue #517: Previously this used recursive conversion which mangled:
* - Connection keys (node names like "Webhook" → "_webhook")
* - Node field names (typeVersion → type_version)
*/
function mutationToSupabaseFormat(mutation: WorkflowMutationRecord): Record<string, any> {
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(mutation)) {
result[keyToSnakeCase(key)] = value;
}
return result;
}
@@ -266,7 +277,7 @@ export class TelemetryBatchProcessor {
for (const batch of batches) {
const result = await this.executeWithRetry(async () => {
// Convert camelCase to snake_case for Supabase
const snakeCaseBatch = batch.map(mutation => toSnakeCase(mutation));
const snakeCaseBatch = batch.map(mutation => mutationToSupabaseFormat(mutation));
const { error } = await this.supabase!
.from('workflow_mutations')

View File

@@ -10,6 +10,23 @@ export interface MCPServerConfig {
authToken?: string;
}
/**
* MCP Tool annotations to help AI assistants understand tool behavior.
* Per MCP spec: https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations
*/
export interface ToolAnnotations {
/** Human-readable title for the tool */
title?: string;
/** If true, the tool does not modify its environment */
readOnlyHint?: boolean;
/** If true, the tool may perform destructive updates to its environment */
destructiveHint?: boolean;
/** If true, calling the tool repeatedly with the same arguments has no additional effect */
idempotentHint?: boolean;
/** If true, the tool may interact with external entities (APIs, services) */
openWorldHint?: boolean;
}
export interface ToolDefinition {
name: string;
description: string;
@@ -25,6 +42,8 @@ export interface ToolDefinition {
required?: string[];
additionalProperties?: boolean | Record<string, any>;
};
/** Tool behavior hints for AI assistants */
annotations?: ToolAnnotations;
}
export interface ResourceDefinition {

View File

@@ -1,7 +1,9 @@
import { describe, it, expect, beforeEach, vi, afterEach, beforeAll, afterAll, type MockInstance } from 'vitest';
import { TelemetryBatchProcessor } from '../../../src/telemetry/batch-processor';
import { TelemetryEvent, WorkflowTelemetry, TELEMETRY_CONFIG } from '../../../src/telemetry/telemetry-types';
import { TelemetryEvent, WorkflowTelemetry, WorkflowMutationRecord, TELEMETRY_CONFIG } from '../../../src/telemetry/telemetry-types';
import { TelemetryError, TelemetryErrorType } from '../../../src/telemetry/telemetry-error';
import { IntentClassification, MutationToolName } from '../../../src/telemetry/mutation-types';
import { AddNodeOperation } from '../../../src/types/workflow-diff';
import type { SupabaseClient } from '@supabase/supabase-js';
// Mock logger to avoid console output in tests
@@ -679,4 +681,258 @@ describe('TelemetryBatchProcessor', () => {
expect(mockProcessExit).toHaveBeenCalledWith(0);
});
});
describe('Issue #517: workflow data preservation', () => {
// This test verifies that workflow mutation data is NOT recursively converted to snake_case
// Previously, the toSnakeCase function was applied recursively which caused:
// - Connection keys like "Webhook" to become "_webhook"
// - Node fields like "typeVersion" to become "type_version"
it('should preserve connection keys exactly as-is (node names)', async () => {
const mutation: WorkflowMutationRecord = {
userId: 'user1',
sessionId: 'session1',
workflowBefore: {
nodes: [],
connections: {}
},
workflowAfter: {
nodes: [
{ id: '1', name: 'Webhook', type: 'n8n-nodes-base.webhook', typeVersion: 1, position: [0, 0], parameters: {} }
],
// Connection keys are NODE NAMES - must be preserved exactly
connections: {
'Webhook': { main: [[{ node: 'AI Agent', type: 'main', index: 0 }]] },
'AI Agent': { main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]] },
'HTTP Request': { main: [[{ node: 'Send Email', type: 'main', index: 0 }]] }
}
},
workflowHashBefore: 'hash1',
workflowHashAfter: 'hash2',
userIntent: 'Test',
intentClassification: IntentClassification.ADD_FUNCTIONALITY,
toolName: MutationToolName.UPDATE_PARTIAL,
operations: [],
operationCount: 0,
operationTypes: [],
validationImproved: null,
errorsResolved: 0,
errorsIntroduced: 0,
nodesAdded: 1,
nodesRemoved: 0,
nodesModified: 0,
connectionsAdded: 3,
connectionsRemoved: 0,
propertiesChanged: 0,
mutationSuccess: true,
durationMs: 100
};
let capturedData: any = null;
vi.mocked(mockSupabase.from).mockImplementation((table) => ({
insert: vi.fn().mockImplementation((data) => {
if (table === 'workflow_mutations') {
capturedData = data;
}
return Promise.resolve(createMockSupabaseResponse());
}),
url: { href: '' },
headers: {},
select: vi.fn(),
upsert: vi.fn(),
update: vi.fn(),
delete: vi.fn()
} as any));
await batchProcessor.flush(undefined, undefined, [mutation]);
expect(capturedData).toBeDefined();
expect(capturedData).toHaveLength(1);
const savedMutation = capturedData[0];
// Top-level keys should be snake_case for Supabase
expect(savedMutation).toHaveProperty('user_id');
expect(savedMutation).toHaveProperty('session_id');
expect(savedMutation).toHaveProperty('workflow_after');
// Connection keys should be preserved EXACTLY (not "_webhook", "_a_i _agent", etc.)
const connections = savedMutation.workflow_after.connections;
expect(connections).toHaveProperty('Webhook'); // NOT "_webhook"
expect(connections).toHaveProperty('AI Agent'); // NOT "_a_i _agent"
expect(connections).toHaveProperty('HTTP Request'); // NOT "_h_t_t_p _request"
});
it('should preserve node field names in camelCase', async () => {
const mutation: WorkflowMutationRecord = {
userId: 'user1',
sessionId: 'session1',
workflowBefore: { nodes: [], connections: {} },
workflowAfter: {
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
// These fields MUST remain in camelCase for n8n API compatibility
typeVersion: 2,
webhookId: 'abc123',
onError: 'continueOnFail',
alwaysOutputData: true,
continueOnFail: false,
retryOnFail: true,
maxTries: 3,
notesInFlow: true,
waitBetweenTries: 1000,
executeOnce: false,
position: [100, 200],
parameters: {}
}
],
connections: {}
},
workflowHashBefore: 'hash1',
workflowHashAfter: 'hash2',
userIntent: 'Test',
intentClassification: IntentClassification.ADD_FUNCTIONALITY,
toolName: MutationToolName.UPDATE_PARTIAL,
operations: [],
operationCount: 0,
operationTypes: [],
validationImproved: null,
errorsResolved: 0,
errorsIntroduced: 0,
nodesAdded: 1,
nodesRemoved: 0,
nodesModified: 0,
connectionsAdded: 0,
connectionsRemoved: 0,
propertiesChanged: 0,
mutationSuccess: true,
durationMs: 100
};
let capturedData: any = null;
vi.mocked(mockSupabase.from).mockImplementation((table) => ({
insert: vi.fn().mockImplementation((data) => {
if (table === 'workflow_mutations') {
capturedData = data;
}
return Promise.resolve(createMockSupabaseResponse());
}),
url: { href: '' },
headers: {},
select: vi.fn(),
upsert: vi.fn(),
update: vi.fn(),
delete: vi.fn()
} as any));
await batchProcessor.flush(undefined, undefined, [mutation]);
expect(capturedData).toBeDefined();
const savedNode = capturedData[0].workflow_after.nodes[0];
// Node fields should be preserved in camelCase (NOT snake_case)
expect(savedNode).toHaveProperty('typeVersion'); // NOT type_version
expect(savedNode).toHaveProperty('webhookId'); // NOT webhook_id
expect(savedNode).toHaveProperty('onError'); // NOT on_error
expect(savedNode).toHaveProperty('alwaysOutputData'); // NOT always_output_data
expect(savedNode).toHaveProperty('continueOnFail'); // NOT continue_on_fail
expect(savedNode).toHaveProperty('retryOnFail'); // NOT retry_on_fail
expect(savedNode).toHaveProperty('maxTries'); // NOT max_tries
expect(savedNode).toHaveProperty('notesInFlow'); // NOT notes_in_flow
expect(savedNode).toHaveProperty('waitBetweenTries'); // NOT wait_between_tries
expect(savedNode).toHaveProperty('executeOnce'); // NOT execute_once
// Verify values are preserved
expect(savedNode.typeVersion).toBe(2);
expect(savedNode.webhookId).toBe('abc123');
expect(savedNode.maxTries).toBe(3);
});
it('should convert only top-level mutation record fields to snake_case', async () => {
const mutation: WorkflowMutationRecord = {
userId: 'user1',
sessionId: 'session1',
workflowBefore: { nodes: [], connections: {} },
workflowAfter: { nodes: [], connections: {} },
workflowHashBefore: 'hash1',
workflowHashAfter: 'hash2',
workflowStructureHashBefore: 'struct1',
workflowStructureHashAfter: 'struct2',
isTrulySuccessful: true,
userIntent: 'Test intent',
intentClassification: IntentClassification.ADD_FUNCTIONALITY,
toolName: MutationToolName.UPDATE_PARTIAL,
operations: [{ type: 'addNode', node: { name: 'Test', type: 'n8n-nodes-base.set', position: [0, 0] } } as AddNodeOperation],
operationCount: 1,
operationTypes: ['addNode'],
validationBefore: { valid: false, errors: [] },
validationAfter: { valid: true, errors: [] },
validationImproved: true,
errorsResolved: 1,
errorsIntroduced: 0,
nodesAdded: 1,
nodesRemoved: 0,
nodesModified: 0,
connectionsAdded: 0,
connectionsRemoved: 0,
propertiesChanged: 0,
mutationSuccess: true,
mutationError: undefined,
durationMs: 150
};
let capturedData: any = null;
vi.mocked(mockSupabase.from).mockImplementation((table) => ({
insert: vi.fn().mockImplementation((data) => {
if (table === 'workflow_mutations') {
capturedData = data;
}
return Promise.resolve(createMockSupabaseResponse());
}),
url: { href: '' },
headers: {},
select: vi.fn(),
upsert: vi.fn(),
update: vi.fn(),
delete: vi.fn()
} as any));
await batchProcessor.flush(undefined, undefined, [mutation]);
expect(capturedData).toBeDefined();
const saved = capturedData[0];
// Top-level fields should be converted to snake_case
expect(saved).toHaveProperty('user_id', 'user1');
expect(saved).toHaveProperty('session_id', 'session1');
expect(saved).toHaveProperty('workflow_before');
expect(saved).toHaveProperty('workflow_after');
expect(saved).toHaveProperty('workflow_hash_before', 'hash1');
expect(saved).toHaveProperty('workflow_hash_after', 'hash2');
expect(saved).toHaveProperty('workflow_structure_hash_before', 'struct1');
expect(saved).toHaveProperty('workflow_structure_hash_after', 'struct2');
expect(saved).toHaveProperty('is_truly_successful', true);
expect(saved).toHaveProperty('user_intent', 'Test intent');
expect(saved).toHaveProperty('intent_classification');
expect(saved).toHaveProperty('tool_name');
expect(saved).toHaveProperty('operation_count', 1);
expect(saved).toHaveProperty('operation_types');
expect(saved).toHaveProperty('validation_before');
expect(saved).toHaveProperty('validation_after');
expect(saved).toHaveProperty('validation_improved', true);
expect(saved).toHaveProperty('errors_resolved', 1);
expect(saved).toHaveProperty('errors_introduced', 0);
expect(saved).toHaveProperty('nodes_added', 1);
expect(saved).toHaveProperty('nodes_removed', 0);
expect(saved).toHaveProperty('nodes_modified', 0);
expect(saved).toHaveProperty('connections_added', 0);
expect(saved).toHaveProperty('connections_removed', 0);
expect(saved).toHaveProperty('properties_changed', 0);
expect(saved).toHaveProperty('mutation_success', true);
expect(saved).toHaveProperty('duration_ms', 150);
});
});
});