Fix ci + improvements (#17795)

as title
This commit is contained in:
martmull
2026-02-10 11:48:10 +01:00
committed by GitHub
parent ee0474e287
commit 52fe21c04b
26 changed files with 182 additions and 248 deletions
@@ -1,17 +1,19 @@
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
const APP_PATH = join(__dirname, '..');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, OUTPUT_DIR, 'ioi', 'manifest.json');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, OUTPUT_DIR, 'manifest.json');
describe('invalid-app manifest', () => {
it('should fail to build manifest due to duplicate universalIdentifier', async () => {
const result = await runAppDev({ appPath: APP_PATH, timeout: 10000 });
const result = await runAppDevInProcess({
appPath: APP_PATH,
timeout: 10000,
});
expect(result.success).toBe(false);
expect(result.output).toContain('Duplicate universal identifiers');
const manifestExists = await fs.pathExists(MANIFEST_OUTPUT_PATH);
@@ -1,47 +1,52 @@
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { AppUninstallCommand } from '@/cli/commands/app/app-uninstall';
import { existsSync } from 'fs';
import { join, resolve } from 'path';
import { inspect } from 'util';
import { runCliCommand } from '@/cli/__tests__/integration/utils/run-cli-command.util';
inspect.defaultOptions.depth = 10;
describe('Application: install delete and reinstall rich-app', () => {
const applicationName = 'rich-app';
const deleteCommand = new AppUninstallCommand();
const appPath = resolve(__dirname, '../');
beforeAll(async () => {
expect(existsSync(appPath)).toBe(true);
});
afterAll(async () => {
const result = await deleteCommand.execute({
appPath,
askForConfirmation: false,
const result = await runCliCommand({
command: 'auth:status',
args: [appPath],
timeout: 5_000,
waitForOutput: '✓ Valid',
});
expect(result.success).toBe(true);
});
it(`should successfully install ${applicationName} application`, async () => {
await runAppDev({ appPath });
await runCliCommand({
command: 'app:dev',
args: [appPath],
waitForOutput: '✓ Synced',
});
expect(existsSync(join(appPath, OUTPUT_DIR, 'manifest.json'))).toBe(true);
});
it(`should successfully delete ${applicationName} application`, async () => {
const result = await deleteCommand.execute({
appPath,
askForConfirmation: false,
await runCliCommand({
command: 'app:uninstall',
args: [appPath, '-y'],
waitForOutput: 'Application uninstalled successfully',
});
expect(result.success).toBe(true);
});
it(`should successfully re-install ${applicationName} application`, async () => {
await runAppDev({ appPath });
await runCliCommand({
command: 'app:dev',
args: [appPath],
waitForOutput: '✓ Synced',
});
expect(existsSync(join(appPath, OUTPUT_DIR, 'manifest.json'))).toBe(true);
});
@@ -1,25 +1,18 @@
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { join } from 'path';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineManifestTests } from './tests/manifest.tests';
import { defineEntitiesTests } from './tests/entities.tests';
import { defineManifestTests } from './tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
describe('rich-app app:dev', () => {
let result: RunCliCommandResult;
beforeAll(async () => {
result = await runAppDev({ appPath: APP_PATH });
if (!result.success) {
console.log(result.output.slice(undefined, 20_000));
}
const result = await runAppDevInProcess({ appPath: APP_PATH });
expect(result.success).toBe(true);
}, 60000);
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineEntitiesTests(APP_PATH);
});
@@ -1,8 +1,6 @@
import { join } from 'path';
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineLogicFunctionsTests } from './tests/logic-functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
@@ -10,17 +8,12 @@ import { defineManifestTests } from './tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
describe('root-app app:dev', () => {
let result: RunCliCommandResult;
beforeAll(async () => {
result = await runAppDev({ appPath: APP_PATH });
if (!result.success) {
console.log(result.output.slice(undefined, 20_000));
}
const result = await runAppDevInProcess({ appPath: APP_PATH });
expect(result.success).toBe(true);
}, 60000);
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineLogicFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
@@ -1,11 +1,19 @@
import { ConfigService } from '@/cli/utilities/config/config-service';
import { testConfig } from '@/cli/__tests__/constants/testConfig';
import { vi, beforeAll, afterAll } from 'vitest';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
import * as fs from 'fs-extra';
import * as path from 'path';
import { beforeAll } from 'vitest';
beforeAll(() => {
vi.spyOn(ConfigService.prototype, 'getConfig').mockResolvedValue(testConfig);
});
const testConfigPath = getConfigPath();
afterAll(() => {
vi.restoreAllMocks();
beforeAll(async () => {
await fs.ensureDir(path.dirname(testConfigPath));
const configFile = {
profiles: {
default: testConfig,
},
};
await fs.writeFile(testConfigPath, JSON.stringify(configFile, null, 2));
});
@@ -1,7 +1,7 @@
import { type TwentyConfig } from '@/cli/utilities/config/config-service';
export const testConfig: TwentyConfig = {
apiUrl: 'http://localhost:3000',
apiUrl: 'http://apple.localhost:3000',
apiKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
};
@@ -0,0 +1,37 @@
import { AppDevCommand } from '@/cli/commands/app/app-dev';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
export type RunAppDevResult = {
success: boolean;
};
export const runAppDevInProcess = async (options: {
appPath: string;
timeout?: number;
}): Promise<RunAppDevResult> => {
const { appPath, timeout = 30_000 } = options;
const manifestPath = join(appPath, OUTPUT_DIR, 'manifest.json');
const command = new AppDevCommand();
await command.execute({ appPath });
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await fs.pathExists(manifestPath)) {
// Small delay to let any pending writes finish
await new Promise((resolve) => setTimeout(resolve, 500));
await command.close();
return { success: true };
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
await command.close();
return { success: false };
};
@@ -1,22 +0,0 @@
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppDevOptions = {
appPath: string;
timeout?: number;
};
export const runAppDev = (
options: RunAppDevOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 60_000 } = options;
return runCliCommand({
command: 'app:dev',
args: [appPath],
waitForOutput: ['✓ Synced'],
timeout,
});
};
@@ -23,7 +23,7 @@ export type RunCliCommandResult = {
export const runCliCommand = (
options: RunCliCommandOptions,
): Promise<RunCliCommandResult> => {
const { command, args = [], waitForOutput, timeout = 60_000 } = options;
const { command, args = [], waitForOutput, timeout = 30_000 } = options;
return new Promise((resolve) => {
// Run from CLI directory to use twenty-sdk's tsconfig paths
@@ -43,6 +43,7 @@ export const runCliCommand = (
let output = '';
const timeoutId = setTimeout(() => {
child.kill();
console.log(`RunCliCommand ${command} timeout after ${timeout / 1_000}s`);
resolve({ success: false, output });
}, timeout);
@@ -76,11 +77,7 @@ export const runCliCommand = (
child.on('close', (code) => {
clearTimeout(timeoutId);
if (waitForOutputs.length === 0) {
resolve({ success: code === 0, output });
} else {
resolve({ success: false, output });
}
resolve({ success: code === 0, output });
});
child.on('error', () => {
@@ -0,0 +1,33 @@
import { vi } from 'vitest';
const mockApiService = {
validateAuth: vi.fn().mockResolvedValue({ authValid: true, serverUp: true }),
checkApplicationExist: vi
.fn()
.mockResolvedValue({ success: true, data: false }),
createApplication: vi
.fn()
.mockResolvedValue({ success: true, data: { id: 'mock-id' } }),
syncApplication: vi.fn().mockResolvedValue({ success: true, data: true }),
uploadFile: vi.fn().mockResolvedValue({ success: true, data: true }),
};
vi.mock('@/cli/utilities/api/api-service', () => ({
ApiService: class {
validateAuth = mockApiService.validateAuth;
checkApplicationExist = mockApiService.checkApplicationExist;
createApplication = mockApiService.createApplication;
syncApplication = mockApiService.syncApplication;
uploadFile = mockApiService.uploadFile;
},
}));
vi.mock('@/cli/utilities/file/file-uploader', () => ({
FileUploader: class {
uploadFile = vi.fn().mockResolvedValue({ success: true, data: true });
},
}));
vi.mock('@/cli/utilities/dev/dev-ui', () => ({
renderDevUI: vi.fn().mockResolvedValue({ unmount: vi.fn() }),
}));
@@ -79,11 +79,12 @@ export const registerCommands = (program: Command): void => {
program
.command('app:uninstall [appPath]')
.description('Uninstall application from Twenty')
.action(async (appPath?: string) => {
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (appPath?: string, options?: { yes?: boolean }) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
askForConfirmation: !options?.yes,
});
process.exit(result.success ? 0 : 1);
} catch {
@@ -31,6 +31,18 @@ export class AppDevCommand {
private uiStateManager: DevUiStateManager | null = null;
private unmountUI: (() => void) | null = null;
async close(): Promise<void> {
this.unmountUI?.();
await Promise.all([
this.manifestWatcher?.close(),
this.logicFunctionsWatcher?.close(),
this.frontComponentsWatcher?.close(),
this.assetWatcher?.close(),
this.dependencyWatcher?.close(),
]);
}
async execute(options: AppDevOptions): Promise<void> {
this.appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
@@ -163,21 +175,9 @@ export class AppDevCommand {
}
private setupGracefulShutdown(): void {
const shutdown = async () => {
this.unmountUI?.();
const shutdown = () => void this.close().then(() => process.exit(0));
await Promise.all([
this.manifestWatcher?.close(),
this.logicFunctionsWatcher?.close(),
this.frontComponentsWatcher?.close(),
this.assetWatcher?.close(),
this.dependencyWatcher?.close(),
]);
process.exit(0);
};
process.on('SIGINT', () => void shutdown());
process.on('SIGTERM', () => void shutdown());
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
}
@@ -79,7 +79,7 @@ export class LogicFunctionExecuteCommand {
} else {
console.log(
chalk.yellow(
'No functions found for this application. Have you synced your app with `yarn app:sync`?',
'No functions found for this application. Have you synced your app with `yarn app:dev`?',
),
);
}
@@ -1,7 +1,8 @@
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
export type TwentyConfig = {
apiUrl: string;
apiKey?: string;
@@ -19,7 +20,7 @@ export class ConfigService {
private static activeWorkspace = DEFAULT_WORKSPACE_NAME;
constructor() {
this.configPath = path.join(os.homedir(), '.twenty', 'config.json');
this.configPath = getConfigPath();
}
static setActiveWorkspace(name?: string) {
@@ -0,0 +1,12 @@
import * as os from 'os';
import * as path from 'path';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
export const getConfigPath = (): string => {
if (process.env.NODE_ENV === 'test') {
return path.join(TEST_CONFIG_DIR, 'config.json');
}
return path.join(os.homedir(), '.twenty', 'config.json');
};