03c94727be
## Summary
- Standard object and field metadata labels were plain strings instead
of being wrapped in Lingui `msg` template literals, which prevented
extraction into translation catalogs. This caused "Uncompiled message
detected!" warnings at runtime.
- The bug was introduced when the decorator-based approach
(`@WorkspaceEntity({ labelSingular: msg`...` })`) was replaced by flat
metadata builder utils with plain strings.
- Adds an `i18nLabel` helper to safely extract the message string from
`MessageDescriptor` objects.
- Re-runs `lingui extract` and `lingui compile` to update all locale PO
files and compiled catalogs.
- Adds an integration test that queries the metadata API with `x-locale`
headers to verify locale-aware label resolution.
- Includes a ClickHouse usage event writer integration test (small
unrelated fix noticed along the way).
## Test plan
- [ ] CI lint passes (prettier + oxlint)
- [ ] CI typecheck passes
- [ ] Server unit tests pass
- [ ] Integration tests pass (including new `object-metadata-i18n` test)
- [ ] No "Uncompiled message detected!" warnings for standard
object/field labels at runtime
Made with [Cursor](https://cursor.com)
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import process from 'process';
|
|
|
|
import {
|
|
type ClickHouseClient,
|
|
ClickHouseLogLevel,
|
|
createClient,
|
|
} from '@clickhouse/client';
|
|
|
|
import { formatDateForClickHouse } from 'src/database/clickHouse/clickHouse.util';
|
|
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
|
|
import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-resource-type.enum';
|
|
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
|
|
|
|
const buildUsageEventRow = (
|
|
workspaceId: string,
|
|
overrides: Partial<{
|
|
userWorkspaceId: string;
|
|
resourceType: UsageResourceType;
|
|
operationType: UsageOperationType;
|
|
quantity: number;
|
|
unit: UsageUnit;
|
|
creditsUsedMicro: number;
|
|
resourceId: string;
|
|
resourceContext: string;
|
|
}> = {},
|
|
) => ({
|
|
timestamp: formatDateForClickHouse(new Date()),
|
|
workspaceId,
|
|
userWorkspaceId: overrides.userWorkspaceId ?? '',
|
|
resourceType: overrides.resourceType ?? UsageResourceType.AI,
|
|
operationType: overrides.operationType ?? UsageOperationType.AI_TOKEN,
|
|
quantity: overrides.quantity ?? 0,
|
|
unit: overrides.unit ?? UsageUnit.TOKEN,
|
|
creditsUsedMicro: overrides.creditsUsedMicro ?? 0,
|
|
resourceId: overrides.resourceId ?? '',
|
|
resourceContext: overrides.resourceContext ?? '',
|
|
metadata: {},
|
|
});
|
|
|
|
describe('ClickHouse Usage Event Writer (integration)', () => {
|
|
let clickHouseClient: ClickHouseClient;
|
|
|
|
beforeAll(async () => {
|
|
jest.useRealTimers();
|
|
|
|
clickHouseClient = createClient({
|
|
url: process.env.CLICKHOUSE_URL,
|
|
log: { level: ClickHouseLogLevel.OFF },
|
|
});
|
|
|
|
await clickHouseClient.query({
|
|
query: 'TRUNCATE TABLE usageEvent',
|
|
format: 'JSONEachRow',
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (clickHouseClient) {
|
|
await clickHouseClient.close();
|
|
}
|
|
});
|
|
|
|
it('should insert a usage event row matching the schema produced by UsageEventWriterService', async () => {
|
|
const workspaceId = '00000000-0000-0000-0000-000000000001';
|
|
|
|
const row = buildUsageEventRow(workspaceId, {
|
|
userWorkspaceId: '00000000-0000-0000-0000-000000000002',
|
|
resourceType: UsageResourceType.AI,
|
|
operationType: UsageOperationType.AI_TOKEN,
|
|
quantity: 1500,
|
|
unit: UsageUnit.TOKEN,
|
|
creditsUsedMicro: 7500,
|
|
resourceId: 'agent-123',
|
|
resourceContext: 'gpt-4o',
|
|
});
|
|
|
|
await clickHouseClient.insert({
|
|
table: 'usageEvent',
|
|
values: [row],
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
const queryResult = await clickHouseClient.query({
|
|
query: `
|
|
SELECT *
|
|
FROM usageEvent
|
|
WHERE workspaceId = '${workspaceId}'
|
|
`,
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
const rows = await queryResult.json<Record<string, unknown>>();
|
|
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].workspaceId).toBe(workspaceId);
|
|
expect(rows[0].resourceType).toBe(UsageResourceType.AI);
|
|
expect(rows[0].operationType).toBe(UsageOperationType.AI_TOKEN);
|
|
expect(rows[0].quantity).toBe(1500);
|
|
expect(rows[0].unit).toBe(UsageUnit.TOKEN);
|
|
expect(rows[0].creditsUsedMicro).toBe(7500);
|
|
expect(rows[0].resourceId).toBe('agent-123');
|
|
expect(rows[0].resourceContext).toBe('gpt-4o');
|
|
});
|
|
|
|
it('should insert a usage event with empty optional fields', async () => {
|
|
const workspaceId = '00000000-0000-0000-0000-000000000003';
|
|
|
|
const row = buildUsageEventRow(workspaceId, {
|
|
resourceType: UsageResourceType.WORKFLOW,
|
|
operationType: UsageOperationType.WORKFLOW_EXECUTION,
|
|
quantity: 1,
|
|
unit: UsageUnit.INVOCATION,
|
|
creditsUsedMicro: 1,
|
|
});
|
|
|
|
await clickHouseClient.insert({
|
|
table: 'usageEvent',
|
|
values: [row],
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
const queryResult = await clickHouseClient.query({
|
|
query: `
|
|
SELECT *
|
|
FROM usageEvent
|
|
WHERE workspaceId = '${workspaceId}'
|
|
`,
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
const rows = await queryResult.json<Record<string, unknown>>();
|
|
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].userWorkspaceId).toBe('');
|
|
expect(rows[0].resourceId).toBe('');
|
|
expect(rows[0].resourceContext).toBe('');
|
|
});
|
|
});
|