fix: correct agent tool, schema, and save handling (#22089)

This commit is contained in:
buua436 authored and GitHub committed 2026-11-09 14:34:29 +08:00
1 parent 3f8dbf259b
commit cf0551433d
9 files changed
+147 -31

No files matched your search

@@ -0,0 +1,37 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import ArrayEditor from './array-editor';
jest.mock('../type-editor', () => ({
__esModule: true,
default: () => null,
}));
describe('ArrayEditor', () => {
it('writes the latest uniqueItems switch value to the schema', async () => {
const onChange = jest.fn();
render(
<ArrayEditor
schema={{ type: 'array', items: { type: 'string' } } as any}
validationNode={undefined}
onChange={onChange}
/>,
);
await act(async () => {
fireEvent.click(screen.getByRole('switch'));
});
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ uniqueItems: true }),
);
});
});
});
@@ -43,13 +43,13 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
);
// Handle validation settings change
const handleValidationChange = () => {
const handleValidationChange = (nextUniqueItems = uniqueItems) => {
const validationProps: ObjectJSONSchema = {
type: 'array',
...(isBooleanSchema(schema) ? {} : schema),
minItems: minItems,
maxItems: maxItems,
uniqueItems: uniqueItems || undefined,
uniqueItems: nextUniqueItems || undefined,
};
// Keep the items schema
@@ -68,6 +68,10 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
onChange(propsToKeep as ObjectJSONSchema);
};
const handleValidationBlur = () => {
handleValidationChange();
};
// Handle item schema changes
const handleItemSchemaChange = (updatedItemSchema: ObjectJSONSchema) => {
const updatedSchema: ObjectJSONSchema = {
@@ -125,7 +129,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
setMinItems(value);
// Don't update immediately to avoid too many rerenders
}}
onBlur={handleValidationChange}
onBlur={handleValidationBlur}
placeholder={t.arrayMinimumPlaceholder}
className={cn('h-8', !!minMaxError && 'border-destructive')}
/>
@@ -150,7 +154,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
setMaxItems(value);
// Don't update immediately to avoid too many rerenders
}}
onBlur={handleValidationChange}
onBlur={handleValidationBlur}
placeholder={t.arrayMaximumPlaceholder}
className={cn('h-8', !!minMaxError && 'border-destructive')}
/>
@@ -170,7 +174,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
checked={uniqueItems}
onCheckedChange={(checked) => {
setUniqueItems(checked);
setTimeout(handleValidationChange, 0);
handleValidationChange(checked);
}}
/>
<Label htmlFor={uniqueItemsId} className="cursor-pointer">
@@ -34,7 +34,7 @@ export const useHandleForm = () => {
globalVariables: param,
});
if (res.code === 0) {
if (res?.code === 0) {
refetch();
}
},
@@ -54,7 +54,7 @@ export const GlobalParamSheet = (props: IGlobalParamModalProps) => {
const res = await saveGraph(undefined, {
globalVariables: param,
});
if (res.code === 0) {
if (res?.code === 0) {
refetch();
}
};
@@ -1,5 +1,13 @@
import { renderHook } from '@testing-library/react';
import { Operator } from '../constant';
import { useInitializeOperatorParams } from './use-add-node';
import { useAgentToolInitialValues } from './use-agent-tool-initial-values';
import { getQueritAgentInitialValues } from './querit-agent-initial-values';
jest.mock('./use-add-node', () => ({
useInitializeOperatorParams: jest.fn(),
}));
describe('getQueritAgentInitialValues', () => {
it('creates Querit Agent params without runtime query or outputs', () => {
expect(
@@ -30,3 +38,27 @@ describe('getQueritAgentInitialValues', () => {
});
});
});
describe('useAgentToolInitialValues', () => {
it('does not reuse canvas defaults for a Tavily Search agent tool', () => {
jest.mocked(useInitializeOperatorParams).mockReturnValue({
initialFormValuesMap: {
[Operator.TavilySearch]: {
api_key: 'canvas-api-key',
search_depth: 'advanced',
max_results: 10,
},
[Operator.TavilyExtract]: {
api_key: 'canvas-api-key',
extract_depth: 'advanced',
},
},
} as any);
const { result } = renderHook(() => useAgentToolInitialValues());
expect(
result.current.initializeAgentToolValues(Operator.TavilySearch),
).toEqual({ api_key: '' });
});
});
@@ -17,7 +17,8 @@ export function useAgentToolInitialValues() {
...omit(initialValues, 'query'),
description: '',
};
case (Operator.TavilySearch, Operator.TavilyExtract):
case Operator.TavilySearch:
case Operator.TavilyExtract:
return {
api_key: '',
};
@@ -144,6 +144,35 @@ describe('Run preflight', () => {
expect(message.error).not.toHaveBeenCalled();
});
it('reports DSL build exceptions and resolves the save promise', async () => {
mockBuildDslData.mockImplementationOnce(() => {
throw new Error('Unable to build agent DSL');
});
const { result } = renderHook(() => useSaveGraph());
let saveResult: unknown;
await act(async () => {
saveResult = await result.current.saveGraph();
});
expect(saveResult).toBeUndefined();
expect(message.error).toHaveBeenCalledWith('Unable to build agent DSL');
expect(mockSaveAgent).not.toHaveBeenCalled();
});
it('reports rejected agent save requests and resolves the save promise', async () => {
mockSaveAgent.mockRejectedValueOnce(new Error('Unable to save agent'));
const { result } = renderHook(() => useSaveGraph());
let saveResult: unknown;
await act(async () => {
saveResult = await result.current.saveGraph();
});
expect(saveResult).toBeUndefined();
expect(message.error).toHaveBeenCalledWith('Unable to save agent');
});
it('validates explicit nextNodes before opening chat', async () => {
useGraphStore.setState({
nodes: [node('Message', { content: ['hello'] })],
+32 -19
View File
@@ -46,27 +46,40 @@ export const useSaveGraph = (
return;
}
const params: Record<string, any> = {
id,
title: data.title,
dsl: buildDslData(currentNodes, otherParam),
};
try {
const params: Record<string, any> = {
id,
title: data.title,
dsl: buildDslData(currentNodes, otherParam),
};
if (release) {
params.release = 'true';
}
// Both the debounced auto-save and the explicit Save button pass
// autosave:true so the backend refreshes only the live draft and never
// mints a new version. Only publishing (release) writes a version row,
// optionally carrying an alias into user_canvas_version.
if (autosave) {
params.autosave = true;
}
if (alias) {
params.alias = alias;
}
if (release) {
params.release = 'true';
}
// Both the debounced auto-save and the explicit Save button pass
// autosave:true so the backend refreshes only the live draft and never
// mints a new version. Only publishing (release) writes a version row,
// optionally carrying an alias into user_canvas_version.
if (autosave) {
params.autosave = true;
}
if (alias) {
params.alias = alias;
}
return saveAgent(params);
const result = await saveAgent(params);
if (result?.code !== undefined && result.code !== 0) {
message.error(result.message || i18n.t('message.requestError'));
}
return result;
} catch (error) {
message.error(
error instanceof Error && error.message
? error.message
: i18n.t('message.requestError'),
);
return undefined;
}
},
[id, data.title, buildDslData, saveAgent],
);
+4 -4
View File
@@ -120,12 +120,12 @@ export default function Agent() {
const { saveGraph, loading } = useSaveGraph();
// Manual Save is a draft save, same as auto-save: it refreshes the live
// canvas without minting a user_canvas_version row. Publishing does that.
const handleSave = useCallback(() => {
saveGraph(undefined, undefined, undefined, true);
const handleSave = useCallback(async () => {
await saveGraph(undefined, undefined, undefined, true);
}, [saveGraph]);
const handlePublish = useCallback(
(alias?: string) => {
saveGraph(undefined, undefined, true, undefined, alias);
async (alias?: string) => {
await saveGraph(undefined, undefined, true, undefined, alias);
},
[saveGraph],
);