Compare commits

...

7 Commits

Author SHA1 Message Date
prastoin 60a203b371 test(server): integ test sync agent 2026-02-20 12:20:13 +01:00
prastoin 79529a7a26 splti test 2026-02-20 12:16:34 +01:00
prastoin 3c3a4daeb2 test(server): integration 2026-02-20 11:57:11 +01:00
prastoin 6926aa3c25 fixup 2026-02-20 11:56:50 +01:00
prastoin ad15eb371b feat(server): integ 2026-02-20 11:56:48 +01:00
prastoin 0d46d98943 feat(sdK): agent manifest implem 2026-02-20 11:56:25 +01:00
prastoin c206e2eee0 feat(shared): manifest types 2026-02-20 11:55:07 +01:00
26 changed files with 489 additions and 156 deletions
@@ -10,6 +10,7 @@ import {
export const EXPECTED_MANIFEST: Manifest = {
pageLayouts: [],
agents: [],
publicAssets: [
{
checksum: '99496069dcc2a1488e1cae9f826d2707',
@@ -348,6 +348,7 @@ export const EXPECTED_MANIFEST: Manifest = {
views: [],
navigationMenuItems: [],
pageLayouts: [],
agents: [],
roles: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
@@ -1,4 +1,5 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { getAgentBaseFile } from '@/cli/utilities/entity/entity-agent-template';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-navigation-menu-item-template';
@@ -158,6 +159,15 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.Agent: {
const name = await this.getEntityName(entity);
const file = getAgentBaseFile({
name,
});
return { name, file };
}
default:
assertUnreachable(entity);
}
@@ -1,10 +1,10 @@
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
import {
type ApplicationManifest,
type Manifest,
type FieldManifest,
type Manifest,
} from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
const validApplication: ApplicationManifest = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -33,6 +33,7 @@ const validManifest: Manifest = {
logicFunctions: [],
roles: [],
skills: [],
agents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
@@ -5,11 +5,13 @@ import {
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
import {
type ApplicationConfig,
type FrontComponentConfig,
type LogicFunctionConfig,
} from '@/sdk';
import { type AgentConfig } from '@/sdk/agents/agent-config';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
@@ -17,6 +19,7 @@ import { glob } from 'fast-glob';
import { readFile } from 'fs-extra';
import { basename, extname, relative } from 'path';
import {
type AgentManifest,
type ApplicationManifest,
type AssetManifest,
ASSETS_DIR,
@@ -33,7 +36,6 @@ import {
} from 'twenty-shared/application';
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
import { assertUnreachable } from 'twenty-shared/utils';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
@@ -66,6 +68,7 @@ export const buildManifest = async (
const fields: FieldManifest[] = [];
const roles: RoleManifest[] = [];
const skills: SkillManifest[] = [];
const agents: AgentManifest[] = [];
const logicFunctions: LogicFunctionManifest[] = [];
const frontComponents: FrontComponentManifest[] = [];
const publicAssets: AssetManifest[] = [];
@@ -78,6 +81,7 @@ export const buildManifest = async (
const fieldsFilePaths: string[] = [];
const rolesFilePaths: string[] = [];
const skillsFilePaths: string[] = [];
const agentsFilePaths: string[] = [];
const logicFunctionsFilePaths: string[] = [];
const frontComponentsFilePaths: string[] = [];
const publicAssetsFilePaths: string[] = [];
@@ -180,6 +184,16 @@ export const buildManifest = async (
skillsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Agents: {
const extract = await extractManifestFromFile<AgentConfig>({
appPath,
filePath,
});
agents.push(extract.config);
errors.push(...extract.errors);
agentsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.LogicFunctions: {
const extract = await extractManifestFromFile<LogicFunctionConfig>({
appPath,
@@ -311,6 +325,7 @@ export const buildManifest = async (
fields,
roles,
skills,
agents,
logicFunctions,
frontComponents,
publicAssets,
@@ -325,6 +340,7 @@ export const buildManifest = async (
fields: fieldsFilePaths,
roles: rolesFilePaths,
skills: skillsFilePaths,
agents: agentsFilePaths,
logicFunctions: logicFunctionsFilePaths,
frontComponents: frontComponentsFilePaths,
publicAssets: publicAssetsFilePaths,
@@ -1,6 +1,7 @@
import * as ts from 'typescript';
export enum TargetFunction {
DefineAgent = 'defineAgent',
DefineApplication = 'defineApplication',
DefineField = 'defineField',
DefineLogicFunction = 'defineLogicFunction',
@@ -20,6 +21,7 @@ export enum ManifestEntityKey {
Objects = 'objects',
Roles = 'roles',
Skills = 'skills',
Agents = 'agents',
FrontComponents = 'frontComponents',
PublicAssets = 'publicAssets',
Views = 'views',
@@ -33,6 +35,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
TargetFunction,
ManifestEntityKey
> = {
[TargetFunction.DefineAgent]: ManifestEntityKey.Agents,
[TargetFunction.DefineApplication]: ManifestEntityKey.Application,
[TargetFunction.DefineField]: ManifestEntityKey.Fields,
[TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions,
@@ -70,6 +70,7 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
frontComponents: SyncableEntity.FrontComponent,
roles: SyncableEntity.Role,
skills: SyncableEntity.Skill,
agents: SyncableEntity.Agent,
views: SyncableEntity.View,
navigationMenuItems: SyncableEntity.NavigationMenuItem,
pageLayouts: SyncableEntity.PageLayout,
@@ -1,10 +1,10 @@
import {
type OrchestratorState,
type OrchestratorStateEntityInfo,
type OrchestratorStateEvent,
type OrchestratorStateFileStatus,
type OrchestratorStateStepStatus,
type OrchestratorStateSyncStatus,
type OrchestratorStateEntityInfo,
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { SyncableEntity } from 'twenty-shared/application';
@@ -98,6 +98,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.Skill]: 'Skills',
[SyncableEntity.Agent]: 'Agents',
[SyncableEntity.View]: 'Views',
[SyncableEntity.NavigationMenuItem]: 'Navigation menu items',
[SyncableEntity.PageLayout]: 'Page layouts',
@@ -0,0 +1,25 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getAgentBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineAgent } from 'twenty-sdk';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineAgent({
universalIdentifier: ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER,
name: '${kebabCaseName}',
label: '${name}',
prompt: 'Add a prompt for your agent',
});
`;
};
@@ -0,0 +1,3 @@
import { type AgentManifest } from 'twenty-shared/application';
export type AgentConfig = AgentManifest;
@@ -0,0 +1,31 @@
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type AgentConfig } from '@/sdk/agents/agent-config';
export const defineAgent: DefineEntity<AgentConfig> = (config) => {
const errors: string[] = [];
if (!config.universalIdentifier) {
errors.push('Agent must have a universalIdentifier');
}
if (!config.name) {
errors.push('Agent must have a name');
}
if (!config.label) {
errors.push('Agent must have a label');
}
if (!config.prompt) {
errors.push('Agent must have a prompt');
}
if (config.responseFormat && config.responseFormat.type === 'json') {
if (!config.responseFormat.schema) {
errors.push('Agent JSON response format must have a schema');
}
}
return createValidationResult({ config, errors });
};
@@ -1,3 +1,4 @@
import { type AgentConfig } from '@/sdk/agents/agent-config';
import { type ApplicationConfig } from '@/sdk/application/application-config';
import { type FrontComponentConfig } from '@/sdk/front-component-config';
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
@@ -25,6 +26,7 @@ export type DefinableEntity =
| LogicFunctionConfig
| RoleManifest
| SkillManifest
| AgentConfig
| ViewConfig
| NavigationMenuItemManifest
| PageLayoutConfig;
+2
View File
@@ -1,3 +1,5 @@
export type { AgentConfig } from './agents/agent-config';
export { defineAgent } from './agents/define-agent';
export type { ApplicationConfig } from './application/application-config';
export { defineApplication } from './application/define-application';
export type {
@@ -7,6 +7,7 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
'frontComponent',
'role',
'skill',
'agent',
'view',
'viewField',
'viewFieldGroup',
@@ -1,6 +1,7 @@
import { type Manifest } from 'twenty-shared/application';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromAgentManifestToUniversalFlatAgent } from 'src/engine/core-modules/application/utils/from-agent-manifest-to-universal-flat-agent.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/utils/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util';
@@ -20,6 +21,7 @@ import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-m
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
@@ -143,6 +145,20 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
);
}
for (const agentManifest of manifest.agents ?? []) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
metadataName: 'agent',
universalFlatEntity: fromAgentManifestToUniversalFlatAgent({
agentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
},
);
}
for (const viewManifest of manifest.views ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewManifestToUniversalFlatView({
@@ -0,0 +1,31 @@
import { type AgentManifest } from 'twenty-shared/application';
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
export const fromAgentManifestToUniversalFlatAgent = ({
agentManifest,
applicationUniversalIdentifier,
now,
}: {
agentManifest: AgentManifest;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatAgent => {
return {
universalIdentifier: agentManifest.universalIdentifier,
applicationUniversalIdentifier,
name: agentManifest.name,
label: agentManifest.label,
icon: agentManifest.icon ?? null,
description: agentManifest.description ?? null,
prompt: agentManifest.prompt,
modelId: agentManifest.modelId ?? 'default-smart-model',
responseFormat: agentManifest.responseFormat ?? { type: 'text' },
modelConfiguration: agentManifest.modelConfiguration ?? null,
evaluationInputs: agentManifest.evaluationInputs ?? [],
isCustom: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 1`] = `
exports[`syncApplication - object and fields should return workspace migration actions on initial sync then on second sync with field rename and new role 1`] = `
{
"syncApplication": {
"actions": [
@@ -307,7 +307,7 @@ exports[`syncApplication should return workspace migration actions on initial sy
}
`;
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = `
exports[`syncApplication - object and fields should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = `
{
"syncApplication": {
"actions": [
@@ -348,77 +348,3 @@ exports[`syncApplication should return workspace migration actions on initial sy
},
}
`;
exports[`syncApplication should sync a skill then update it on second sync 1`] = `
{
"syncApplication": {
"actions": [
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A test role",
"icon": null,
"isEditable": true,
"label": "Test Role",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"content": "# Test Skill
This is a test skill.",
"createdAt": Any<String>,
"description": "A skill for testing",
"icon": "IconBrain",
"isActive": true,
"isCustom": false,
"label": "Test Skill",
"name": "test-skill",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "skill",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication should sync a skill then update it on second sync 2`] = `
{
"syncApplication": {
"actions": [
{
"metadataName": "skill",
"type": "update",
"universalIdentifier": Any<String>,
"update": {
"content": "# Test Skill
This is an updated test skill with more content.",
"description": "An updated skill for testing",
"label": "Test Skill Updated",
},
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`syncApplication - skill should sync a skill then update it on second sync 1`] = `
{
"syncApplication": {
"actions": [
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A test role",
"icon": null,
"isEditable": true,
"label": "Test Role",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"content": "# Test Skill
This is a test skill.",
"createdAt": Any<String>,
"description": "A skill for testing",
"icon": "IconBrain",
"isActive": true,
"isCustom": false,
"label": "Test Skill",
"name": "test-skill",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "skill",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication - skill should sync a skill then update it on second sync 2`] = `
{
"syncApplication": {
"actions": [
{
"metadataName": "skill",
"type": "update",
"universalIdentifier": Any<String>,
"update": {
"content": "# Test Skill
This is an updated test skill with more content.",
"description": "An updated skill for testing",
"label": "Test Skill Updated",
},
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
@@ -45,6 +45,7 @@ const buildBaseManifest = (
logicFunctions: [],
frontComponents: [],
publicAssets: [],
agents: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
@@ -0,0 +1,117 @@
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type Manifest } from 'twenty-shared/application';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_AGENT_ID = uuidv4();
describe('syncApplication - agent', () => {
let appCreated = false;
beforeAll(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'A test application',
sourcePath: 'test-sync',
});
appCreated = true;
}, 60000);
afterEach(async () => {
if (!appCreated) {
return;
}
await uninstallApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
});
it('should sync an agent then update it on second sync', async () => {
const initialManifest: Manifest = {
application: {
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test Application',
description: 'A test application for workspace migration',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
apiClientChecksum: null,
},
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'test-agent',
label: 'Test Agent',
description: 'An agent for testing',
icon: 'IconRobot',
prompt: 'You are a helpful test assistant.',
modelId: 'gpt-4o',
responseFormat: { type: 'text' },
evaluationInputs: ['test input 1'],
},
],
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
skills: [],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const { data: firstSyncData } = await syncApplication({
manifest: initialManifest,
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const updatedManifest: Manifest = {
...initialManifest,
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'test-agent',
label: 'Test Agent Updated',
description: 'An updated agent for testing',
icon: 'IconRobot',
prompt:
'You are an updated helpful test assistant with more capabilities.',
modelId: 'gpt-4o',
responseFormat: { type: 'text' },
evaluationInputs: ['test input 1', 'test input 2'],
},
],
};
const { data: secondSyncData } = await syncApplication({
manifest: updatedManifest,
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
});
@@ -11,7 +11,6 @@ const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_SECOND_ROLE_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_SKILL_ID = uuidv4();
const TEST_OBJECT = buildDefaultObjectManifest({
nameSingular: 'ticket',
@@ -22,7 +21,7 @@ const TEST_OBJECT = buildDefaultObjectManifest({
icon: 'IconTicket',
});
describe('syncApplication', () => {
describe('syncApplication - object and fields', () => {
let appCreated = false;
beforeAll(async () => {
@@ -83,6 +82,7 @@ describe('syncApplication', () => {
logicFunctions: [],
frontComponents: [],
publicAssets: [],
agents: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
@@ -133,78 +133,4 @@ describe('syncApplication', () => {
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
it('should sync a skill then update it on second sync', async () => {
const initialManifest: Manifest = {
application: {
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test Application',
description: 'A test application for workspace migration',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
apiClientChecksum: null,
},
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill',
description: 'A skill for testing',
icon: 'IconBrain',
content: '# Test Skill\n\nThis is a test skill.',
},
],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const { data: firstSyncData } = await syncApplication({
manifest: initialManifest,
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const updatedManifest: Manifest = {
...initialManifest,
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill Updated',
description: 'An updated skill for testing',
icon: 'IconBrain',
content:
'# Test Skill\n\nThis is an updated test skill with more content.',
},
],
};
const { data: secondSyncData } = await syncApplication({
manifest: updatedManifest,
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
});
@@ -0,0 +1,111 @@
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type Manifest } from 'twenty-shared/application';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_SKILL_ID = uuidv4();
describe('syncApplication - skill', () => {
let appCreated = false;
beforeAll(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'A test application',
sourcePath: 'test-sync',
});
appCreated = true;
}, 60000);
afterEach(async () => {
if (!appCreated) {
return;
}
await uninstallApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
});
it('should sync a skill then update it on second sync', async () => {
const initialManifest: Manifest = {
application: {
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test Application',
description: 'A test application for workspace migration',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
apiClientChecksum: null,
},
agents: [],
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill',
description: 'A skill for testing',
icon: 'IconBrain',
content: '# Test Skill\n\nThis is a test skill.',
},
],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const { data: firstSyncData } = await syncApplication({
manifest: initialManifest,
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const updatedManifest: Manifest = {
...initialManifest,
skills: [
{
universalIdentifier: TEST_SKILL_ID,
name: 'test-skill',
label: 'Test Skill Updated',
description: 'An updated skill for testing',
icon: 'IconBrain',
content:
'# Test Skill\n\nThis is an updated test skill with more content.',
},
],
};
const { data: secondSyncData } = await syncApplication({
manifest: updatedManifest,
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
});
@@ -0,0 +1,23 @@
import { type AgentResponseSchema, type ModelConfiguration } from '@/ai';
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type AgentTextResponseFormat = { type: 'text' };
export type AgentJsonResponseFormat = {
type: 'json';
schema: AgentResponseSchema;
};
export type AgentResponseFormatManifest =
| AgentTextResponseFormat
| AgentJsonResponseFormat;
export type AgentManifest = SyncableEntityOptions & {
name: string;
label: string;
icon?: string;
description?: string;
prompt: string;
modelId?: string;
responseFormat?: AgentResponseFormatManifest;
modelConfiguration?: ModelConfiguration;
evaluationInputs?: string[];
};
@@ -5,6 +5,7 @@ export enum SyncableEntity {
FrontComponent = 'frontComponent',
Role = 'role',
Skill = 'skill',
Agent = 'agent',
View = 'view',
NavigationMenuItem = 'navigationMenuItem',
PageLayout = 'pageLayout',
@@ -7,6 +7,12 @@
* |___/
*/
export type {
AgentTextResponseFormat,
AgentJsonResponseFormat,
AgentResponseFormatManifest,
AgentManifest,
} from './agentManifestType';
export type {
ApplicationMarketplaceData,
ApplicationManifest,
@@ -1,3 +1,4 @@
import { type AgentManifest } from './agentManifestType';
import { type ApplicationManifest } from './applicationType';
import { type AssetManifest } from './assetManifestType';
import { type FieldManifest } from './fieldManifestType';
@@ -18,6 +19,7 @@ export type Manifest = {
frontComponents: FrontComponentManifest[];
roles: RoleManifest[];
skills: SkillManifest[];
agents: AgentManifest[];
publicAssets: AssetManifest[];
views: ViewManifest[];
navigationMenuItems: NavigationMenuItemManifest[];