fix: ensure LoopContainer required fields are set via schema-driven normalization

This commit is contained in:
Lucio Lelii 2026-05-20 15:54:33 +02:00
parent 0c64850b7d
commit 3dbb140131
3 changed files with 97 additions and 4 deletions

View File

@ -143,4 +143,64 @@ describe('ContainersCallService', () => {
'Invalid container catalog response: expected reduced catalog format with a descriptors array'
);
});
it('fills missing required boolean fields using descriptor schema defaults', async () => {
const typesRequest = firstValueFrom(service.retrieveAllContainerTypes());
httpMock.expectOne(`${environment.apiUrl}/containers/types/catalog`).flush({
descriptors: [
{
type: 'LoopContainer',
description: 'Loop container node',
userInteractive: false,
hasExampleContainer: true,
exampleContainerEndpoint: '/containers/types/LoopContainer/example',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
maxIterations: { type: 'integer' },
useLlm: { type: 'boolean' }
},
required: ['name', 'maxIterations', 'useLlm']
}
}
]
});
await typesRequest;
const request = firstValueFrom(service.createContainer('container-1', {
typeName: 'LoopContainer',
specificConfiguration: {
name: 'Loop',
maxIterations: 3
}
}));
const createRequest = httpMock.expectOne(`${environment.apiUrl}/containers`);
expect(createRequest.request.method).toBe('POST');
expect(createRequest.request.body.useLlm).toBe(false);
expect(createRequest.request.body.useLLM).toBeUndefined();
createRequest.flush({
id: 'container-1',
name: 'Loop',
typeName: 'LoopContainer',
specificConfiguration: {
type: 'LoopContainerConfiguration',
name: 'Loop',
maxIterations: 3,
useLlm: false
},
inputs: [],
outputs: []
});
await expect(request).resolves.toEqual(expect.objectContaining({
id: 'container-1',
typeName: 'LoopContainer',
nodeFamily: 'container'
}));
});
});

View File

@ -267,16 +267,48 @@ export class ContainersCallService extends ContainersCallServiceBase {
private buildContainerConfigurationPayload(containerType: string, configuration: Record<string, unknown>) {
const { typeName: _ignoreTypeName, ...sanitized } = configuration;
const configurationType = this.resolveConfigurationType(containerType, sanitized);
const normalized = this.normalizeConfigurationWithSchema(containerType, sanitized);
const configurationType = this.resolveConfigurationType(containerType, normalized);
return {
...sanitized,
...normalized,
type: configurationType,
name: typeof sanitized["name"] === "string" && sanitized["name"].length > 0
? sanitized["name"]
name: typeof normalized["name"] === "string" && normalized["name"].length > 0
? normalized["name"]
: containerType
};
}
private normalizeConfigurationWithSchema(containerType: string, configuration: Record<string, unknown>) {
const normalized = { ...configuration };
const descriptor = this.containerTypesCache
?.find((candidate) => candidate.type === containerType);
const schema = descriptor?.schema;
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
return normalized;
}
const required = Array.isArray(schema["required"])
? schema["required"].filter((value): value is string => typeof value === "string" && value.length > 0)
: [];
if (!required.length) return normalized;
const properties = this.toRecord(schema["properties"]);
for (const key of required) {
if (normalized[key] !== undefined) continue;
const propertySchema = this.toRecord(properties[key]);
const defaultValue = propertySchema["default"];
if (typeof defaultValue === "boolean" || typeof defaultValue === "number" || typeof defaultValue === "string") {
normalized[key] = defaultValue;
continue;
}
if (propertySchema["type"] === "boolean") {
normalized[key] = false;
}
}
return normalized;
}
private resolveConfigurationType(containerType: string, configuration: Record<string, unknown>) {
const explicitType = this.toNullableString(configuration["type"]);
if (explicitType) return explicitType;

View File

@ -289,6 +289,7 @@ export async function addBlockToEditor(
const currentConfiguration = cloneValue(currentLiveNode.data.specificConfiguration ?? {}) as Record<string, unknown>;
const nextConfiguration: Record<string, unknown> = {
...currentConfiguration,
name: String(currentConfiguration['name'] ?? currentLiveNode.data['name'] ?? 'Container'),
subFlow: candidateSubFlow
};