chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => ({ db: {} }))
|
||||
vi.mock('@sim/db/schema', () => ({
|
||||
knowledgeBase: {},
|
||||
knowledgeConnector: {},
|
||||
mcpServers: {},
|
||||
userTableDefinitions: {},
|
||||
userTableRows: {},
|
||||
workflow: {},
|
||||
workflowFolder: {},
|
||||
workflowSchedule: {},
|
||||
}))
|
||||
|
||||
import { canonicalWorkflowVfsDir } from '@/lib/copilot/vfs/path-utils'
|
||||
import { buildVfsSnapshot, buildWorkspaceMd, type WorkspaceMdData } from './workspace-context'
|
||||
|
||||
function baseData(overrides: Partial<WorkspaceMdData> = {}): WorkspaceMdData {
|
||||
return {
|
||||
workspace: { id: 'ws-1', name: 'WS', ownerId: 'u-1' },
|
||||
members: [],
|
||||
workflows: [],
|
||||
knowledgeBases: [],
|
||||
tables: [],
|
||||
files: [],
|
||||
oauthIntegrations: [],
|
||||
envVariables: [],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildWorkspaceMd - workflow VFS state paths', () => {
|
||||
// `workflows[].folderPath` arrives ALREADY per-segment percent-encoded (it is
|
||||
// the value from buildVfsFolderPathMap / resolveFolderPath that also builds the
|
||||
// stored VFS keys). The advertised path must not re-encode it.
|
||||
it('emits a single-encoded state path for a folder name with a space', () => {
|
||||
const md = buildWorkspaceMd(
|
||||
baseData({
|
||||
workflows: [
|
||||
{ id: 'wf-1', name: 'The Elder', isDeployed: false, folderPath: 'The%20Elder' },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
expect(md).toContain('workflows/The%20Elder/The%20Elder/state.json')
|
||||
// The exact double-encoding regression: `%20` -> `%2520`.
|
||||
expect(md).not.toContain('The%2520Elder')
|
||||
})
|
||||
|
||||
it('matches the canonical VFS dir helper the materializer/pointers use', () => {
|
||||
const folderPath = 'My%20Folder/Sub%20Folder'
|
||||
const md = buildWorkspaceMd(
|
||||
baseData({
|
||||
workflows: [{ id: 'wf-1', name: 'My Flow', isDeployed: false, folderPath }],
|
||||
})
|
||||
)
|
||||
|
||||
const expected = `${canonicalWorkflowVfsDir({ name: 'My Flow', folderPath })}/state.json`
|
||||
expect(expected).toBe('workflows/My%20Folder/Sub%20Folder/My%20Flow/state.json')
|
||||
expect(md).toContain(expected)
|
||||
})
|
||||
|
||||
it('advertises canonical encoded VFS paths for root-level workflows', () => {
|
||||
const md = buildWorkspaceMd(
|
||||
baseData({
|
||||
workflows: [{ id: 'wf-1', name: 'Root Flow', isDeployed: false, folderPath: null }],
|
||||
})
|
||||
)
|
||||
|
||||
expect(md).toContain('VFS dir: `workflows/Root%20Flow`')
|
||||
expect(md).toContain('VFS state path: `workflows/Root%20Flow/state.json`')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildWorkspaceMd - connected integrations / credentials', () => {
|
||||
it('lists each connected account with its credentialId and never leaks tokens', () => {
|
||||
const md = buildWorkspaceMd(
|
||||
baseData({
|
||||
oauthIntegrations: [
|
||||
{
|
||||
id: 'cred-abc',
|
||||
providerId: 'google-email',
|
||||
displayName: 'alice@example.com',
|
||||
role: 'admin',
|
||||
},
|
||||
{ id: 'cred-def', providerId: 'slack', displayName: 'Workspace Bot', role: 'member' },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
// credentialId must be present so the superagent can pass it without reading credentials.json.
|
||||
expect(md).toContain('credentialId: `cred-abc`')
|
||||
expect(md).toContain('credentialId: `cred-def`')
|
||||
expect(md).toContain('google-email')
|
||||
expect(md).toContain('slack')
|
||||
|
||||
// No OAuth secrets/tokens may ever appear in the workspace context.
|
||||
for (const secret of [
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
'idToken',
|
||||
'clientSecret',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
]) {
|
||||
expect(md).not.toContain(secret)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders (none) when no integrations are connected', () => {
|
||||
const md = buildWorkspaceMd(baseData({ oauthIntegrations: [] }))
|
||||
expect(md).toContain('## Connected Integrations\n(none)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildWorkspaceMd - determinism (prompt-cache stability)', () => {
|
||||
it('is byte-identical regardless of input row order', () => {
|
||||
const a = buildWorkspaceMd(
|
||||
baseData({
|
||||
members: [
|
||||
{ name: 'Bob', email: 'bob@x.com', permissionType: 'admin' },
|
||||
{ name: 'Amy', email: 'amy@x.com', permissionType: 'write' },
|
||||
],
|
||||
workflows: [
|
||||
{ id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null },
|
||||
{ id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null },
|
||||
],
|
||||
tables: [
|
||||
{ id: 't-2', name: 'Orders', description: null, rowCount: 5 },
|
||||
{ id: 't-1', name: 'Customers', description: null, rowCount: 9 },
|
||||
],
|
||||
knowledgeBases: [
|
||||
{ id: 'kb-2', name: 'Docs', connectorTypes: ['notion', 'github'] },
|
||||
{ id: 'kb-1', name: 'Articles', connectorTypes: ['github', 'notion'] },
|
||||
],
|
||||
oauthIntegrations: [
|
||||
{ id: 'c-2', providerId: 'slack', displayName: null, role: null },
|
||||
{ id: 'c-1', providerId: 'github', displayName: null, role: null },
|
||||
],
|
||||
envVariables: ['ZED', 'API_KEY'],
|
||||
customTools: [
|
||||
{ id: 'ct-2', name: 'Beta Tool' },
|
||||
{ id: 'ct-1', name: 'Alpha Tool' },
|
||||
],
|
||||
mcpServers: [
|
||||
{ id: 'mcp-2', name: 'Zulu', url: null, enabled: false },
|
||||
{ id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true },
|
||||
],
|
||||
skills: [
|
||||
{ id: 'sk-2', name: 'Writer', description: 'writes' },
|
||||
{ id: 'sk-1', name: 'Editor', description: 'edits' },
|
||||
],
|
||||
jobs: [
|
||||
{
|
||||
id: 'j-2',
|
||||
title: 'Nightly',
|
||||
prompt: 'run nightly',
|
||||
cronExpression: '0 0 * * *',
|
||||
status: 'active',
|
||||
lifecycle: 'persistent',
|
||||
sourceTaskName: null,
|
||||
},
|
||||
{
|
||||
id: 'j-1',
|
||||
title: 'Hourly',
|
||||
prompt: 'run hourly',
|
||||
cronExpression: '0 * * * *',
|
||||
status: 'active',
|
||||
lifecycle: 'persistent',
|
||||
sourceTaskName: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
const b = buildWorkspaceMd(
|
||||
baseData({
|
||||
members: [
|
||||
{ name: 'Amy', email: 'amy@x.com', permissionType: 'write' },
|
||||
{ name: 'Bob', email: 'bob@x.com', permissionType: 'admin' },
|
||||
],
|
||||
workflows: [
|
||||
{ id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null },
|
||||
{ id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null },
|
||||
],
|
||||
tables: [
|
||||
{ id: 't-1', name: 'Customers', description: null, rowCount: 9 },
|
||||
{ id: 't-2', name: 'Orders', description: null, rowCount: 5 },
|
||||
],
|
||||
knowledgeBases: [
|
||||
{ id: 'kb-1', name: 'Articles', connectorTypes: ['notion', 'github'] },
|
||||
{ id: 'kb-2', name: 'Docs', connectorTypes: ['github', 'notion'] },
|
||||
],
|
||||
oauthIntegrations: [
|
||||
{ id: 'c-1', providerId: 'github', displayName: null, role: null },
|
||||
{ id: 'c-2', providerId: 'slack', displayName: null, role: null },
|
||||
],
|
||||
envVariables: ['API_KEY', 'ZED'],
|
||||
customTools: [
|
||||
{ id: 'ct-1', name: 'Alpha Tool' },
|
||||
{ id: 'ct-2', name: 'Beta Tool' },
|
||||
],
|
||||
mcpServers: [
|
||||
{ id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true },
|
||||
{ id: 'mcp-2', name: 'Zulu', url: null, enabled: false },
|
||||
],
|
||||
skills: [
|
||||
{ id: 'sk-1', name: 'Editor', description: 'edits' },
|
||||
{ id: 'sk-2', name: 'Writer', description: 'writes' },
|
||||
],
|
||||
jobs: [
|
||||
{
|
||||
id: 'j-1',
|
||||
title: 'Hourly',
|
||||
prompt: 'run hourly',
|
||||
cronExpression: '0 * * * *',
|
||||
status: 'active',
|
||||
lifecycle: 'persistent',
|
||||
sourceTaskName: null,
|
||||
},
|
||||
{
|
||||
id: 'j-2',
|
||||
title: 'Nightly',
|
||||
prompt: 'run nightly',
|
||||
cronExpression: '0 0 * * *',
|
||||
status: 'active',
|
||||
lifecycle: 'persistent',
|
||||
sourceTaskName: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
expect(a).toBe(b)
|
||||
})
|
||||
|
||||
it('ignores volatile workflow run timestamps', () => {
|
||||
const withRun = buildWorkspaceMd(
|
||||
baseData({
|
||||
workflows: [
|
||||
{
|
||||
id: 'wf-1',
|
||||
name: 'Alpha',
|
||||
isDeployed: false,
|
||||
folderPath: null,
|
||||
lastRunAt: new Date('2026-06-18T12:00:00Z'),
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
const withoutRun = buildWorkspaceMd(
|
||||
baseData({
|
||||
workflows: [{ id: 'wf-1', name: 'Alpha', isDeployed: false, folderPath: null }],
|
||||
})
|
||||
)
|
||||
expect(withRun).toBe(withoutRun)
|
||||
expect(withRun).not.toContain('last run')
|
||||
})
|
||||
|
||||
it('ignores volatile table row counts', () => {
|
||||
const a = buildWorkspaceMd(
|
||||
baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 1 }] })
|
||||
)
|
||||
const b = buildWorkspaceMd(
|
||||
baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 9999 }] })
|
||||
)
|
||||
expect(a).toBe(b)
|
||||
expect(a).not.toContain('rows')
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom blocks', () => {
|
||||
const customBlocks = [
|
||||
{ type: 'custom_block_abc', name: 'Invoice Parser', description: 'Parses invoices' },
|
||||
]
|
||||
|
||||
it('renders a Custom Blocks section in the workspace markdown', () => {
|
||||
const md = buildWorkspaceMd(baseData({ customBlocks }))
|
||||
expect(md).toContain('## Custom Blocks (1)')
|
||||
expect(md).toContain('- **Invoice Parser** (custom_block_abc) — Parses invoices')
|
||||
})
|
||||
|
||||
it('omits the section when there are no custom blocks', () => {
|
||||
expect(buildWorkspaceMd(baseData())).not.toContain('## Custom Blocks')
|
||||
})
|
||||
|
||||
it('never leaks custom blocks into the typed snapshot Go diffs (diff-safety)', () => {
|
||||
const withBlocks = buildVfsSnapshot(baseData({ customBlocks }))
|
||||
const without = buildVfsSnapshot(baseData())
|
||||
expect('customBlocks' in withBlocks).toBe(false)
|
||||
expect(JSON.stringify(withBlocks)).toBe(JSON.stringify(without))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user