Files
simstudioai--sim/apps/sim/ee/workspace-forking/lib/copy/copy-chats.test.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

154 lines
4.8 KiB
TypeScript

/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import type { DbOrTx } from '@/lib/db/types'
import { copyForkChatDeployments } from '@/ee/workspace-forking/lib/copy/copy-chats'
/**
* Sequenced select mock: each `.select().from().where()` resolves the next queued result.
* The chat copy issues (1) source chats, (2) target chat rows, then per identifier attempt
* (3+) the taken-identifier check; inserts are captured.
*/
function makeTx(selectResults: unknown[][]) {
const inserted: Array<Record<string, unknown>> = []
let call = 0
const tx = {
select: () => ({
from: () => ({ where: () => Promise.resolve(selectResults[call++] ?? []) }),
}),
insert: () => ({
values: (values: Array<Record<string, unknown>>) => {
inserted.push(...values)
return Promise.resolve()
},
}),
}
return { tx: tx as unknown as DbOrTx, inserted }
}
const sourceChat = (overrides: Record<string, unknown> = {}) => ({
id: 'chat-src',
workflowId: 'wf-src',
userId: 'src-user',
identifier: 'original-chat',
title: 'Support Chat',
description: 'desc',
isActive: true,
customizations: { welcomeMessage: 'hi' },
authType: 'password',
password: 'hashed-secret',
allowedEmails: ['a@b.com'],
outputConfigs: [{ blockId: 'block-src', path: 'content' }],
archivedAt: null,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
...overrides,
})
const pair = {
sourceWorkflowId: 'wf-src',
targetWorkflowId: 'wf-tgt',
workflowName: 'Support Flow',
}
describe('copyForkChatDeployments', () => {
it('copies a live source chat with a generated identifier and remapped output block ids', async () => {
const { tx, inserted } = makeTx([
[sourceChat()],
[], // target has no chat rows
[], // no identifier collisions
])
const result = await copyForkChatDeployments({
tx,
pairs: [pair],
targetWorkspaceName: 'My Team WS',
userId: 'user-1',
now: new Date('2026-07-07'),
resolveBlockId: (targetWorkflowId, sourceBlockId) =>
`${targetWorkflowId}:${sourceBlockId}:mapped`,
})
expect(result.created).toBe(1)
expect(inserted).toHaveLength(1)
const copy = inserted[0]
expect(copy.id).not.toBe('chat-src')
expect(copy.workflowId).toBe('wf-tgt')
expect(copy.userId).toBe('user-1')
// `{workspace}-{workflow}-{randomnum}` in the identifier charset.
expect(copy.identifier).toMatch(/^my-team-ws-support-flow-\d{6}$/)
// Config copies verbatim - auth (hashed password) included.
expect(copy.title).toBe('Support Chat')
expect(copy.authType).toBe('password')
expect(copy.password).toBe('hashed-secret')
expect(copy.allowedEmails).toEqual(['a@b.com'])
// Output configs bind to the target's block ids via the sync's block resolver.
expect(copy.outputConfigs).toEqual([{ blockId: 'wf-tgt:block-src:mapped', path: 'content' }])
expect(copy.archivedAt).toBeNull()
})
it('leaves a target that already has ANY chat row untouched (live or archived - never resurrects)', async () => {
const { tx, inserted } = makeTx([
[sourceChat()],
[{ workflowId: 'wf-tgt' }], // target already has a chat row
])
const result = await copyForkChatDeployments({
tx,
pairs: [pair],
targetWorkspaceName: 'WS',
userId: 'user-1',
now: new Date(),
resolveBlockId: (_wf, blockId) => blockId,
})
expect(result.created).toBe(0)
expect(inserted).toHaveLength(0)
})
it('never emits duplicate identifiers within a batch (same workspace + workflow slug)', async () => {
const twoChats = makeTx([
[sourceChat(), sourceChat({ id: 'chat-src-2', identifier: 'other' })],
[], // target has no chat rows
[], // no live-identifier collisions
])
const result = await copyForkChatDeployments({
tx: twoChats.tx,
pairs: [pair],
targetWorkspaceName: 'WS',
userId: 'user-1',
now: new Date(),
resolveBlockId: (_wf, blockId) => blockId,
})
expect(result.created).toBe(2)
const identifiers = twoChats.inserted.map((row) => row.identifier)
expect(new Set(identifiers).size).toBe(2)
})
it('no-ops with no pairs or no live source chats', async () => {
const empty = makeTx([[]])
expect(
(
await copyForkChatDeployments({
tx: empty.tx,
pairs: [pair],
targetWorkspaceName: 'WS',
userId: 'user-1',
now: new Date(),
resolveBlockId: (_wf, blockId) => blockId,
})
).created
).toBe(0)
expect(
(
await copyForkChatDeployments({
tx: empty.tx,
pairs: [],
targetWorkspaceName: 'WS',
userId: 'user-1',
now: new Date(),
resolveBlockId: (_wf, blockId) => blockId,
})
).created
).toBe(0)
})
})