Files
simstudioai--sim/apps/sim/lib/logs/log-views.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

184 lines
6.4 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
isLargeArrayManifestMock,
isLargeValueRefMock,
readLargeArrayManifestSliceMock,
materializeLargeArrayManifestMock,
materializeLargeValueRefMock,
} = vi.hoisted(() => ({
isLargeArrayManifestMock: vi.fn(),
isLargeValueRefMock: vi.fn(),
readLargeArrayManifestSliceMock: vi.fn(),
materializeLargeArrayManifestMock: vi.fn(),
materializeLargeValueRefMock: vi.fn(),
}))
vi.mock('@/lib/execution/payloads/large-array-manifest-metadata', () => ({
isLargeArrayManifest: isLargeArrayManifestMock,
}))
vi.mock('@/lib/execution/payloads/large-value-ref', () => ({
isLargeValueRef: isLargeValueRefMock,
}))
vi.mock('@/lib/execution/payloads/large-array-manifest', () => ({
readLargeArrayManifestSlice: readLargeArrayManifestSliceMock,
materializeLargeArrayManifest: materializeLargeArrayManifestMock,
}))
vi.mock('@/lib/execution/payloads/store', () => ({
materializeLargeValueRef: materializeLargeValueRefMock,
}))
import type { TraceSpan } from '@/lib/logs/types'
import { grepSpans, type LogViewContext, toFull, toOverview } from './log-views'
const ctx: LogViewContext = {
workspaceId: 'ws-1',
workflowId: 'wf-1',
executionId: 'exec-1',
}
// Fixture helpers — the mocked type guards key off `__sim`.
const manifest = (totalCount: number, preview: unknown[] = []) => ({
__sim: 'manifest',
totalCount,
preview,
})
const ref = (preview: unknown) => ({ __sim: 'ref', preview, size: 100 })
function span(overrides: Partial<TraceSpan> = {}): TraceSpan {
return {
id: 'span-1',
name: 'Agent 1',
type: 'agent',
duration: 100,
startTime: '2026-01-01T00:00:00.000Z',
endTime: '2026-01-01T00:00:00.100Z',
...overrides,
} as TraceSpan
}
beforeEach(() => {
vi.clearAllMocks()
isLargeArrayManifestMock.mockImplementation((v: any) => v?.__sim === 'manifest')
isLargeValueRefMock.mockImplementation((v: any) => v?.__sim === 'ref')
})
describe('toOverview', () => {
it('keeps timing/cost/hierarchy and omits input/output without materializing refs', () => {
const spans: TraceSpan[] = [
span({
id: 'root',
cost: { total: 0.5 },
input: { secret: 'in' },
output: ref('out-preview') as unknown as Record<string, unknown>,
children: [span({ id: 'child', name: 'Tool', type: 'tool' })],
}),
]
const out = toOverview(spans)
expect(out[0]).toMatchObject({
id: 'root',
name: 'Agent 1',
type: 'agent',
durationMs: 100,
cost: { total: 0.5 },
})
expect(out[0]).not.toHaveProperty('input')
expect(out[0]).not.toHaveProperty('output')
expect(out[0].children?.[0]).toMatchObject({ id: 'child', name: 'Tool' })
expect(materializeLargeArrayManifestMock).not.toHaveBeenCalled()
expect(materializeLargeValueRefMock).not.toHaveBeenCalled()
})
})
describe('toFull', () => {
it('includes inline input/output', async () => {
const out = await toFull([span({ input: { a: 1 }, output: { b: 2 } })], ctx)
expect(out[0]).toMatchObject({ input: { a: 1 }, output: { b: 2 } })
})
it('block scoping returns only the selected subtree', async () => {
const spans: TraceSpan[] = [
span({ id: 's1', blockId: 'blk-a', name: 'A' }),
span({ id: 's2', blockId: 'blk-b', name: 'B', output: { keep: true } }),
]
const out = await toFull(spans, ctx, { blockId: 'blk-b' })
expect(out).toHaveLength(1)
expect(out[0]).toMatchObject({ blockId: 'blk-b', output: { keep: true } })
})
it('materializes a large-array manifest field', async () => {
materializeLargeArrayManifestMock.mockResolvedValue([1, 2, 3])
const out = await toFull([span({ output: manifest(3) as any })], ctx)
expect(out[0].output).toEqual([1, 2, 3])
expect(materializeLargeArrayManifestMock).toHaveBeenCalledTimes(1)
})
it('falls back to ref preview when a single ref is unavailable', async () => {
materializeLargeValueRefMock.mockResolvedValue(undefined)
const out = await toFull([span({ output: ref('the-preview') as any })], ctx)
expect(out[0].output).toBe('the-preview')
})
})
describe('grepSpans', () => {
it('matches inline output text and error text', async () => {
const spans = [
span({ output: { msg: 'request timeout occurred' }, errorMessage: 'boom failure' }),
]
const outMatch = await grepSpans(spans, 'timeout', ctx)
expect(outMatch.matches.some((m) => m.field === 'output')).toBe(true)
expect(outMatch.truncated).toBe(false)
const errMatch = await grepSpans(spans, 'boom', ctx)
expect(errMatch.matches.some((m) => m.field === 'error')).toBe(true)
})
it('streams a large-array manifest slice-by-slice with advancing offsets', async () => {
// totalCount 500, batch 200 → starts 0, 200, 400 (3 slices). Needle in slice 3.
readLargeArrayManifestSliceMock.mockImplementation(async (_m: unknown, start: number) => {
if (start === 400) return [{ v: 'found the needle here' }]
return [{ v: 'nothing' }]
})
const spans = [span({ output: manifest(500) as any })]
const result = await grepSpans(spans, 'needle', ctx)
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
const starts = readLargeArrayManifestSliceMock.mock.calls.map((c) => c[1])
expect(starts).toEqual([0, 200, 400])
})
it('caps matches and marks truncated', async () => {
const spans = [span({ id: 'a', name: 'needle one', output: { v: 'needle two' } })]
const result = await grepSpans(spans, 'needle', ctx, { maxMatches: 1 })
expect(result.matches).toHaveLength(1)
expect(result.truncated).toBe(true)
})
it('falls back to ref preview when ref access is rejected (no throw)', async () => {
materializeLargeValueRefMock.mockRejectedValue(new Error('not available in this execution'))
const spans = [span({ output: ref('secret-token') as any })]
const result = await grepSpans(spans, 'secret-token', ctx)
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
})
it('falls back to literal substring on invalid regex', async () => {
const spans = [span({ output: { v: 'value a(b found' } })]
const result = await grepSpans(spans, '(', ctx)
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
})
it('returns empty for empty traceSpans', async () => {
const result = await grepSpans([], 'anything', ctx)
expect(result.matches).toEqual([])
expect(result.truncated).toBe(false)
})
})