Files
simstudioai--sim/apps/sim/lib/execution/payloads/large-value-metadata.test.ts
T
wehub-resource-sync d25d482dc2
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

458 lines
14 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockAnd,
mockDelete,
mockEq,
mockExecute,
mockInsert,
mockOnConflictDoNothing,
mockSelect,
mockSelectFrom,
mockSelectLimit,
mockSelectWhere,
mockTransaction,
mockTxDelete,
mockTxInsert,
mockTxSelect,
mockTxSelectDistinct,
mockTxSelectFrom,
mockTxSelectLimit,
mockTxSelectWhere,
mockTxValues,
mockValues,
mockWhere,
mockTxWhere,
mockNotInArray,
} = vi.hoisted(() => {
const mockOnConflictDoNothing = vi.fn(async () => undefined)
const mockValues = vi.fn(() => ({ onConflictDoNothing: mockOnConflictDoNothing }))
const mockInsert = vi.fn(() => ({ values: mockValues }))
const mockWhere = vi.fn(async () => undefined)
const mockDelete = vi.fn(() => ({ where: mockWhere }))
const mockSelectLimit = vi.fn(async () => [])
const mockSelectWhere = vi.fn(() => ({ limit: mockSelectLimit }))
const mockSelectFrom = vi.fn(() => ({ where: mockSelectWhere }))
const mockSelect = vi.fn(() => ({ from: mockSelectFrom }))
const mockTxValues = vi.fn(() => ({ onConflictDoNothing: mockOnConflictDoNothing }))
const mockTxInsert = vi.fn(() => ({ values: mockTxValues }))
const mockTxWhere = vi.fn(async () => undefined)
const mockTxDelete = vi.fn(() => ({ where: mockTxWhere }))
const mockTxSelectLimit = vi.fn(async () => [])
const mockTxSelectWhere = vi.fn(() => ({ limit: mockTxSelectLimit }))
const mockTxSelectFrom = vi.fn(() => ({ where: mockTxSelectWhere }))
const mockTxSelect = vi.fn(() => ({ from: mockTxSelectFrom }))
const mockTxSelectDistinct = vi.fn(() => ({ from: mockTxSelectFrom }))
return {
mockAnd: vi.fn((...args: unknown[]) => ({ op: 'and', args })),
mockDelete,
mockEq: vi.fn((...args: unknown[]) => ({ op: 'eq', args })),
mockExecute: vi.fn(async () => [{ count: 0 }]),
mockInsert,
mockNotInArray: vi.fn((...args: unknown[]) => ({ op: 'notInArray', args })),
mockOnConflictDoNothing,
mockSelect,
mockSelectFrom,
mockSelectLimit,
mockSelectWhere,
mockTransaction: vi.fn(async (callback) =>
callback({
delete: mockTxDelete,
insert: mockTxInsert,
select: mockTxSelect,
selectDistinct: mockTxSelectDistinct,
})
),
mockTxDelete,
mockTxInsert,
mockTxSelect,
mockTxSelectDistinct,
mockTxSelectFrom,
mockTxSelectLimit,
mockTxSelectWhere,
mockTxValues,
mockValues,
mockWhere,
mockTxWhere,
}
})
vi.mock('@sim/db', () => ({
db: {
delete: mockDelete,
execute: mockExecute,
insert: mockInsert,
select: mockSelect,
transaction: mockTransaction,
},
}))
vi.mock('@sim/db/schema', () => ({
executionLargeValueDependencies: {
childKey: 'executionLargeValueDependencies.childKey',
parentKey: 'executionLargeValueDependencies.parentKey',
workspaceId: 'executionLargeValueDependencies.workspaceId',
},
executionLargeValueReferences: {
executionId: 'executionLargeValueReferences.executionId',
key: 'executionLargeValueReferences.key',
source: 'executionLargeValueReferences.source',
workspaceId: 'executionLargeValueReferences.workspaceId',
},
executionLargeValues: {
key: 'executionLargeValues.key',
ownerExecutionId: 'executionLargeValues.ownerExecutionId',
workspaceId: 'executionLargeValues.workspaceId',
},
pausedExecutions: {
executionId: 'pausedExecutions.executionId',
status: 'pausedExecutions.status',
},
workflowExecutionLogs: {
executionId: 'workflowExecutionLogs.executionId',
},
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn(() => ({
warn: vi.fn(),
})),
}))
vi.mock('drizzle-orm', () => ({
and: mockAnd,
eq: mockEq,
inArray: vi.fn((...args: unknown[]) => ({ op: 'inArray', args })),
notInArray: mockNotInArray,
sql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values })),
}))
import {
addLargeValueReference,
MAX_LARGE_VALUE_REFERENCES_PER_SCOPE,
pruneLargeValueMetadata,
registerLargeValueOwner,
replaceLargeValueReferences,
} from '@/lib/execution/payloads/large-value-metadata'
function largeValueKey(id: string, executionId = 'source-execution'): string {
return `execution/workspace-1/workflow-1/${executionId}/large-value-lv_${id}.json`
}
describe('large value metadata', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('registers valid large value owner metadata', async () => {
const registered = await registerLargeValueOwner({
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_abcdefghijkl.json',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123.4,
})
expect(registered).toBe(true)
expect(mockTxInsert).toHaveBeenCalledOnce()
expect(mockTxValues).toHaveBeenCalledWith({
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_abcdefghijkl.json',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
ownerExecutionId: 'execution-1',
size: 124,
})
expect(mockOnConflictDoNothing).toHaveBeenCalledOnce()
})
it('skips malformed owner keys', async () => {
const registered = await registerLargeValueOwner({
key: 'execution/workspace-1/workflow-1/other-execution/large-value-lv_abcdefghijkl.json',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
})
expect(registered).toBe(false)
expect(mockTxInsert).not.toHaveBeenCalled()
})
it('records dependency closure for nested large value refs', async () => {
const directKey = largeValueKey('abcdefghijkl')
const transitiveKey = largeValueKey('mnopqrstuvwx', 'root-execution')
const deepTransitiveKey = largeValueKey('deepqrstuvwx', 'deep-execution')
mockTxSelectLimit
.mockResolvedValueOnce([{ childKey: transitiveKey }])
.mockResolvedValueOnce([{ childKey: deepTransitiveKey }])
.mockResolvedValueOnce([])
const registered = await registerLargeValueOwner(
{
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_zyxwvutsrqpo.json',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
},
[directKey]
)
expect(registered).toBe(true)
expect(mockTxSelectDistinct).toHaveBeenCalledTimes(3)
expect(mockTxValues).toHaveBeenLastCalledWith([
{
parentKey: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_zyxwvutsrqpo.json',
childKey: directKey,
workspaceId: 'workspace-1',
},
{
parentKey: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_zyxwvutsrqpo.json',
childKey: transitiveKey,
workspaceId: 'workspace-1',
},
{
parentKey: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_zyxwvutsrqpo.json',
childKey: deepTransitiveKey,
workspaceId: 'workspace-1',
},
])
})
it('chunks dependency writes instead of emitting one oversized VALUES statement', async () => {
const keys = Array.from({ length: 501 }, (_, index) =>
largeValueKey(`a${index.toString(36).padStart(11, '0')}`)
)
await registerLargeValueOwner(
{
key: largeValueKey('zyxwvutsrqpo', 'execution-1'),
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
},
keys
)
expect(mockTxValues).toHaveBeenCalledTimes(3)
expect(mockTxValues.mock.calls[1]?.[0]).toHaveLength(500)
expect(mockTxValues.mock.calls[2]?.[0]).toHaveLength(1)
})
it('rejects reference sets over the metadata cardinality limit', async () => {
const keys = Array.from({ length: MAX_LARGE_VALUE_REFERENCES_PER_SCOPE + 1 }, (_, index) =>
largeValueKey(`b${index.toString(36).padStart(11, '0')}`)
)
await expect(
registerLargeValueOwner(
{
key: largeValueKey('zyxwvutsrqpo', 'execution-1'),
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
},
keys
)
).rejects.toThrow('exceeding the limit')
})
it('limits dependency closure reads to the remaining reference budget', async () => {
const directKey = largeValueKey('a00000000000')
mockTxSelectLimit.mockResolvedValueOnce(
Array.from({ length: MAX_LARGE_VALUE_REFERENCES_PER_SCOPE }, (_, index) => ({
childKey: largeValueKey(`c${index.toString(36).padStart(11, '0')}`),
}))
)
await expect(
registerLargeValueOwner(
{
key: largeValueKey('zyxwvutsrqpo', 'execution-1'),
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
},
[directKey]
)
).rejects.toThrow('Large value dependency closure exceeds the limit')
expect(mockTxSelectLimit).toHaveBeenCalledWith(MAX_LARGE_VALUE_REFERENCES_PER_SCOPE)
})
it('filters known dependency children before applying the remaining reference budget', async () => {
const directKeys = Array.from({ length: MAX_LARGE_VALUE_REFERENCES_PER_SCOPE }, (_, index) =>
largeValueKey(`e${index.toString(36).padStart(11, '0')}`)
)
const knownChildKey = directKeys[1]
const unseenChildKey = largeValueKey('unseenchild1', 'source-execution')
mockTxSelectLimit.mockImplementationOnce(async () => {
const filtersKnownChildren = mockNotInArray.mock.calls.some(
([field, values]) =>
field === 'executionLargeValueDependencies.childKey' &&
Array.isArray(values) &&
values.includes(knownChildKey)
)
return [{ childKey: filtersKnownChildren ? unseenChildKey : knownChildKey }]
})
await expect(
registerLargeValueOwner(
{
key: largeValueKey('zyxwvutsrqpo', 'execution-1'),
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
size: 123,
},
directKeys
)
).rejects.toThrow('Large value dependency closure exceeds the limit')
expect(mockTxSelectLimit).toHaveBeenCalledWith(1)
})
it('replaces an execution reference set with same-workspace unique keys', async () => {
const matchingKey = largeValueKey('abcdefghijkl')
const otherWorkspaceKey =
'execution/workspace-2/workflow-1/source-execution/large-value-lv_abcdefghijkl.json'
await replaceLargeValueReferences(
{
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-2',
source: 'execution_log',
},
{
a: {
__simLargeValueRef: true,
version: 1,
id: 'lv_abcdefghijkl',
kind: 'json',
size: 123,
key: matchingKey,
},
duplicate: {
__simLargeValueRef: true,
version: 1,
id: 'lv_abcdefghijkl',
kind: 'json',
size: 123,
key: matchingKey,
},
ignored: {
__simLargeValueRef: true,
version: 1,
id: 'lv_abcdefghijkl',
kind: 'json',
size: 123,
key: otherWorkspaceKey,
},
}
)
expect(mockTransaction).toHaveBeenCalledOnce()
expect(mockTxDelete).toHaveBeenCalledOnce()
expect(mockEq).toHaveBeenCalledWith('executionLargeValueReferences.source', 'execution_log')
expect(mockTxValues).toHaveBeenCalledWith([
{
key: matchingKey,
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-2',
source: 'execution_log',
},
])
})
it('adds a materialized reference only when the scope is below the reference cap', async () => {
const key = largeValueKey('abcdefghijkl')
await addLargeValueReference(
{
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-2',
source: 'execution_log',
},
key
)
expect(mockSelectLimit).toHaveBeenCalledWith(1)
expect(mockSelectLimit).toHaveBeenCalledWith(MAX_LARGE_VALUE_REFERENCES_PER_SCOPE + 1)
expect(mockValues).toHaveBeenCalledWith({
key,
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-2',
source: 'execution_log',
})
})
it('rejects materialized references once the scope reaches the reference cap', async () => {
mockSelectLimit.mockResolvedValueOnce([]).mockResolvedValueOnce(
Array.from({ length: MAX_LARGE_VALUE_REFERENCES_PER_SCOPE }, (_, index) => ({
key: largeValueKey(`d${index.toString(36).padStart(11, '0')}`),
}))
)
await expect(
addLargeValueReference(
{
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-2',
source: 'execution_log',
},
largeValueKey('zyxwvutsrqpo')
)
).rejects.toThrow('exceeding the limit')
expect(mockInsert).not.toHaveBeenCalled()
})
it('prunes large value metadata in bounded batches', async () => {
mockExecute
.mockResolvedValueOnce([{ count: 2 }])
.mockResolvedValueOnce([{ count: 3 }])
.mockResolvedValueOnce([{ count: 4 }])
await expect(
pruneLargeValueMetadata({
workspaceIds: ['workspace-1'],
tombstonesDeletedBefore: new Date('2026-01-01T00:00:00Z'),
batchSize: 10,
maxRowsPerTable: 100,
})
).resolves.toEqual({
referencesDeleted: 2,
dependenciesDeleted: 3,
tombstonesDeleted: 4,
})
})
it('uses source-specific liveness when pruning stale references', async () => {
await pruneLargeValueMetadata({
workspaceIds: ['workspace-1'],
tombstonesDeletedBefore: new Date('2026-01-01T00:00:00Z'),
batchSize: 10,
maxRowsPerTable: 100,
})
const [query] = mockExecute.mock.calls[0] ?? []
const sqlText = Array.isArray(query?.strings) ? query.strings.join(' ') : ''
expect(sqlText).toContain("ref.source = 'execution_log'")
expect(sqlText).toContain("ref.source = 'paused_snapshot'")
})
})