+6
-4
@@ -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);
|
||||
|
||||
|
||||
+20
-15
@@ -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);
|
||||
});
|
||||
|
||||
+4
-11
@@ -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);
|
||||
});
|
||||
|
||||
+3
-10
@@ -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',
|
||||
};
|
||||
|
||||
+37
@@ -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');
|
||||
};
|
||||
Reference in New Issue
Block a user