Scope empty fixture workspaces to upgrade integration tests (#21778)

The dev seeder activated Empty3/Empty4 workspaces without creating their
DB schema, so every workspace-iterating job (e.g. the workflow cron
trigger) logged 'relation does not exist' for those schemas on each run.


```
[1] query failed: SELECT * FROM workspace_4rdlooovb6mo66rdmgupv06zi."workflowAutomatedTrigger" WHERE type = 'CRON'
[1] error: error: relation "workspace_4rdlooovb6mo66rdmgupv06zi.workflowAutomatedTrigger" does not exist
[1] [Nest] 51868  - 18/06/2026, 5:07:04 pm   ERROR [WorkflowCronTriggerCronJob] Error processing workspace 506915ec-21ca-431b-a04a-257eb216865e: QueryFailedError: relation "workspace_4rdlooovb6mo66rdmgupv06zi.workflowAutomatedTrigger" does not exist
[1] Exception Captured
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21778?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
neo773
2026-06-19 04:51:50 +05:30
committed by GitHub
parent 9f30915f6f
commit c07dd53a48
4 changed files with 70 additions and 126 deletions
@@ -4,10 +4,7 @@ import { Command, CommandRunner, Option } from 'nest-commander';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_EMPTY_WORKSPACE_3_ID,
SEED_EMPTY_WORKSPACE_4_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
SeededEmptyWorkspacesIds,
SeededWorkspacesIds,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
@@ -31,7 +28,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
@Option({
flags: '--light',
description:
'Light seed: only seed the Apple workspace (skip YCombinator and the Empty3/Empty4 fixtures used by integration tests), skip demo custom objects (Pet, Survey, etc.) and limit records to 5 per object',
'Light seed: only seed the Apple workspace (skip YCombinator), skip demo custom objects (Pet, Survey, etc.) and limit records to 5 per object',
})
parseLight(): boolean {
return true;
@@ -41,28 +38,16 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
_passedParams: string[],
options: DataSeedWorkspaceOptions,
): Promise<void> {
// --light seeds a single workspace (Apple) for thin dev containers like
// twenty-app-dev. The default (no flag) seeds all four workspaces — Apple,
// YCombinator and the Empty3/Empty4 fixtures consumed by upgrade-sequence
// integration tests.
const workspaceIds: SeededWorkspacesIds[] = options.light
? [SEED_APPLE_WORKSPACE_ID]
: [SEED_APPLE_WORKSPACE_ID, SEED_YCOMBINATOR_WORKSPACE_ID];
const emptyWorkspaceIds: SeededEmptyWorkspacesIds[] = options.light
? []
: [SEED_EMPTY_WORKSPACE_3_ID, SEED_EMPTY_WORKSPACE_4_ID];
try {
for (const workspaceId of workspaceIds) {
await this.devSeederService.seedDev(workspaceId, {
light: options.light,
});
}
for (const workspaceId of emptyWorkspaceIds) {
await this.devSeederService.seedEmptyWorkspace(workspaceId);
}
} catch (error) {
this.logger.error(error);
this.logger.error(error.stack);
@@ -31,10 +31,6 @@ export type SeededWorkspacesIds =
| typeof SEED_APPLE_WORKSPACE_ID
| typeof SEED_YCOMBINATOR_WORKSPACE_ID;
export type SeededEmptyWorkspacesIds =
| typeof SEED_EMPTY_WORKSPACE_3_ID
| typeof SEED_EMPTY_WORKSPACE_4_ID;
export const SEEDER_CREATE_WORKSPACE_INPUT = {
[SEED_APPLE_WORKSPACE_ID]: {
id: SEED_APPLE_WORKSPACE_ID,
@@ -58,29 +54,3 @@ export const SEEDER_CREATE_WORKSPACE_INPUT = {
SeededWorkspacesIds,
Omit<CreateWorkspaceInput, 'workspaceCustomApplicationId'>
>;
// Empty workspaces with no users, metadata, or data — used by integration tests
// that need more than 2 workspaces (e.g. upgrade sequence runner tests).
export const SEEDER_CREATE_EMPTY_WORKSPACE_INPUT = {
[SEED_EMPTY_WORKSPACE_3_ID]: {
id: SEED_EMPTY_WORKSPACE_3_ID,
displayName: 'Empty3',
subdomain: 'empty3',
inviteHash: 'empty3.dev-invite-hash',
logo: '',
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
isTwoFactorAuthenticationEnforced: false,
},
[SEED_EMPTY_WORKSPACE_4_ID]: {
id: SEED_EMPTY_WORKSPACE_4_ID,
displayName: 'Empty4',
subdomain: 'empty4',
inviteHash: 'empty4.dev-invite-hash',
logo: '',
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
isTwoFactorAuthenticationEnforced: false,
},
} as const satisfies Record<
SeededEmptyWorkspacesIds,
Omit<CreateWorkspaceInput, 'workspaceCustomApplicationId'>
>;
@@ -22,9 +22,7 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
import {
type SeededEmptyWorkspacesIds,
type SeededWorkspacesIds,
SEEDER_CREATE_EMPTY_WORKSPACE_INPUT,
SEEDER_CREATE_WORKSPACE_INPUT,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
@@ -194,84 +192,6 @@ export class DevSeederService {
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
}
public async seedEmptyWorkspace(
workspaceId: SeededEmptyWorkspacesIds,
): Promise<void> {
const appVersion = this.twentyConfigService.get('APP_VERSION') ?? 'unknown';
const lastAttemptedInstanceCommand =
await this.upgradeMigrationService.getLastAttemptedInstanceCommandOrThrow();
const initialCursor =
this.upgradeSequenceReaderService.getInitialCursorForNewWorkspace(
lastAttemptedInstanceCommand,
);
const createWorkspaceStaticInput =
SEEDER_CREATE_EMPTY_WORKSPACE_INPUT[workspaceId];
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const workspaceCustomApplicationId = v4();
await createWorkspace({
queryRunner,
schemaName: 'core',
createWorkspaceInput: {
...createWorkspaceStaticInput,
workspaceCustomApplicationId,
},
});
await this.applicationService.createWorkspaceCustomApplication(
{
workspaceId,
applicationId: workspaceCustomApplicationId,
},
queryRunner,
);
await this.applicationService.createTwentyStandardApplication(
{
workspaceId,
skipCacheInvalidation: true,
},
queryRunner,
);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.devSeederPermissionsService.initMinimalPermissionsAndActivateWorkspace(
{
workspaceId,
workspaceCustomFlatApplication,
},
);
await this.upgradeMigrationService.markAsWorkspaceInitial({
name: initialCursor.name,
workspaceId,
executedByVersion: appVersion,
status: initialCursor.status,
});
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
}
private async seedCoreSchema({
workspaceId,
appVersion,
@@ -3,8 +3,10 @@ import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import { config } from 'dotenv';
import { DataSource, type Repository } from 'typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { InstanceCommandRunnerService } from 'src/engine/core-modules/upgrade/services/instance-command-runner.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
@@ -24,6 +26,7 @@ import {
SEED_EMPTY_WORKSPACE_4_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { createWorkspace } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspace.util';
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
jest.useRealTimers();
@@ -38,6 +41,70 @@ export const WS_2 = SEED_YCOMBINATOR_WORKSPACE_ID;
export const WS_3 = SEED_EMPTY_WORKSPACE_3_ID;
export const WS_4 = SEED_EMPTY_WORKSPACE_4_ID;
const FK_WORKSPACE_FIXTURES = [
{
workspaceId: WS_3,
applicationId: 'f1c0ffee-0000-4000-8000-0000000000c3',
subdomain: 'upgrade-test-fixture-3',
},
{
workspaceId: WS_4,
applicationId: 'f1c0ffee-0000-4000-8000-0000000000c4',
subdomain: 'upgrade-test-fixture-4',
},
];
const seedEmptyWorkspaces = async (dataSource: DataSource) => {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
for (const {
workspaceId,
applicationId,
subdomain,
} of FK_WORKSPACE_FIXTURES) {
await createWorkspace({
queryRunner,
schemaName: 'core',
createWorkspaceInput: {
id: workspaceId,
displayName: subdomain,
subdomain,
inviteHash: `${subdomain}.dev-invite-hash`,
logo: '',
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
isTwoFactorAuthenticationEnforced: false,
workspaceCustomApplicationId: applicationId,
},
});
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(ApplicationEntity)
.values({
id: applicationId,
universalIdentifier: applicationId,
name: 'upgrade-test-fixture',
sourcePath: '',
workspaceId,
})
.orIgnore()
.execute();
}
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
};
const EXECUTED_BY_VERSION = '42.42.42';
const noopAsync = async () => {};
@@ -107,6 +174,8 @@ export const createUpgradeSequenceRunnerIntegrationTestModule = async () => {
await dataSource.initialize();
await seedEmptyWorkspaces(dataSource);
const migrationRepo: Repository<UpgradeMigrationEntity> =
dataSource.getRepository(UpgradeMigrationEntity);