Files
simstudioai--sim/apps/sim/lib/copilot/tools/server/files/file-intent-store.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

124 lines
3.8 KiB
TypeScript

import { generateShortId } from '@sim/utils/id'
import { describe, expect, it, vi } from 'vitest'
import {
consumeLatestFileIntent,
type PendingFileIntent,
storeFileIntent,
} from './file-intent-store'
// Force the in-memory store path so the test is deterministic and Redis-free.
vi.mock('@/lib/core/config/redis', () => ({ getRedisClient: () => null }))
function makeIntent(overrides: Partial<PendingFileIntent>): PendingFileIntent {
return {
operation: 'update',
fileId: 'file-x',
workspaceId: 'ws-1',
userId: 'user-1',
chatId: 'chat-1',
messageId: 'msg-1',
fileRecord: { id: overrides.fileId ?? 'file-x' } as unknown as PendingFileIntent['fileRecord'],
createdAt: Date.now(),
...overrides,
}
}
function uniqueWorkspace(): string {
return `ws-${generateShortId()}`
}
describe('file-intent-store channel scoping', () => {
it('consumes the intent for the requesting channel, not the latest in the message', async () => {
const ws = uniqueWorkspace()
const scope = { chatId: 'chat-1', messageId: 'msg-1' }
// Two concurrent file subagents: A declares fileA on channel F1 first, then
// B declares fileB on channel F2 (later createdAt = the "latest" in message).
await storeFileIntent(
ws,
'fileA',
makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() })
)
await storeFileIntent(
ws,
'fileB',
makeIntent({
workspaceId: ws,
fileId: 'fileB',
channelId: 'F2',
createdAt: Date.now() + 1000,
})
)
// edit_content from channel F1 must get fileA — NOT the latest (fileB).
const a = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F1' })
expect(a?.fileId).toBe('fileA')
// edit_content from channel F2 gets fileB.
const b = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F2' })
expect(b?.fileId).toBe('fileB')
})
it('only consumes its own channel, leaving the sibling intent intact', async () => {
const ws = uniqueWorkspace()
const scope = { chatId: 'chat-1', messageId: 'msg-1' }
await storeFileIntent(
ws,
'fileA',
makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() })
)
await storeFileIntent(
ws,
'fileB',
makeIntent({
workspaceId: ws,
fileId: 'fileB',
channelId: 'F2',
createdAt: Date.now() + 1000,
})
)
await consumeLatestFileIntent(ws, { ...scope, channelId: 'F1' })
// The sibling (F2) is untouched and still consumable afterward.
const b = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F2' })
expect(b?.fileId).toBe('fileB')
})
it('falls back to latest-in-message when no channelId (legacy / main-agent)', async () => {
const ws = uniqueWorkspace()
const scope = { chatId: 'chat-1', messageId: 'msg-1' }
await storeFileIntent(
ws,
'fileA',
makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() })
)
await storeFileIntent(
ws,
'fileB',
makeIntent({
workspaceId: ws,
fileId: 'fileB',
channelId: 'F2',
createdAt: Date.now() + 1000,
})
)
const latest = await consumeLatestFileIntent(ws, scope)
expect(latest?.fileId).toBe('fileB')
})
it('returns undefined when the requesting channel has no pending intent', async () => {
const ws = uniqueWorkspace()
await storeFileIntent(
ws,
'fileA',
makeIntent({ workspaceId: ws, fileId: 'fileA', channelId: 'F1', createdAt: Date.now() })
)
const none = await consumeLatestFileIntent(ws, {
chatId: 'chat-1',
messageId: 'msg-1',
channelId: 'F-absent',
})
expect(none).toBeUndefined()
})
})