Skip to content

Commit de0a084

Browse files
committed
Fix vfs dynamic context encoding
1 parent 89b542f commit de0a084

2 files changed

Lines changed: 86 additions & 15 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { describe, expect, it, vi } from 'vitest'
6+
7+
vi.mock('@sim/db', () => ({ db: {} }))
8+
vi.mock('@sim/db/schema', () => ({
9+
knowledgeBase: {},
10+
knowledgeConnector: {},
11+
mcpServers: {},
12+
userTableDefinitions: {},
13+
userTableRows: {},
14+
workflow: {},
15+
workflowFolder: {},
16+
workflowSchedule: {},
17+
}))
18+
19+
import { canonicalWorkflowVfsDir } from '@/lib/copilot/vfs/path-utils'
20+
import { buildWorkspaceMd, type WorkspaceMdData } from './workspace-context'
21+
22+
function baseData(overrides: Partial<WorkspaceMdData> = {}): WorkspaceMdData {
23+
return {
24+
workspace: { id: 'ws-1', name: 'WS', ownerId: 'u-1' },
25+
members: [],
26+
workflows: [],
27+
knowledgeBases: [],
28+
tables: [],
29+
files: [],
30+
oauthIntegrations: [],
31+
envVariables: [],
32+
...overrides,
33+
}
34+
}
35+
36+
describe('buildWorkspaceMd - workflow VFS state paths', () => {
37+
// `workflows[].folderPath` arrives ALREADY per-segment percent-encoded (it is
38+
// the value from buildVfsFolderPathMap / resolveFolderPath that also builds the
39+
// stored VFS keys). The advertised path must not re-encode it.
40+
it('emits a single-encoded state path for a folder name with a space', () => {
41+
const md = buildWorkspaceMd(
42+
baseData({
43+
workflows: [
44+
{ id: 'wf-1', name: 'The Elder', isDeployed: false, folderPath: 'The%20Elder' },
45+
],
46+
})
47+
)
48+
49+
expect(md).toContain('workflows/The%20Elder/The%20Elder/state.json')
50+
// The exact double-encoding regression: `%20` -> `%2520`.
51+
expect(md).not.toContain('The%2520Elder')
52+
})
53+
54+
it('matches the canonical VFS dir helper the materializer/pointers use', () => {
55+
const folderPath = 'My%20Folder/Sub%20Folder'
56+
const md = buildWorkspaceMd(
57+
baseData({
58+
workflows: [{ id: 'wf-1', name: 'My Flow', isDeployed: false, folderPath }],
59+
})
60+
)
61+
62+
const expected = `${canonicalWorkflowVfsDir({ name: 'My Flow', folderPath })}/state.json`
63+
expect(expected).toBe('workflows/My%20Folder/Sub%20Folder/My%20Flow/state.json')
64+
expect(md).toContain(expected)
65+
})
66+
67+
it('does not advertise a VFS state path for root-level workflows', () => {
68+
const md = buildWorkspaceMd(
69+
baseData({
70+
workflows: [{ id: 'wf-1', name: 'Root Flow', isDeployed: false, folderPath: null }],
71+
})
72+
)
73+
74+
expect(md).not.toContain('VFS state path')
75+
})
76+
})

apps/sim/lib/copilot/chat/workspace-context.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { createLogger } from '@sim/logger'
1313
import { toError } from '@sim/utils/errors'
1414
import { and, count, eq, inArray, isNull } from 'drizzle-orm'
1515
import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment'
16-
import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
16+
import { canonicalWorkflowVfsDir, canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
1717
import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
1818
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace'
1919
import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
@@ -76,21 +76,16 @@ export interface WorkspaceMdData {
7676
}>
7777
}
7878

79-
function normalizeFolderPathForVfs(folderPath?: string | null): string | null {
80-
if (!folderPath) return null
81-
const segments = folderPath
82-
.split('/')
83-
.map((segment) => normalizeVfsSegment(segment))
84-
.filter(Boolean)
85-
return segments.length > 0 ? segments.join('/') : null
86-
}
87-
79+
// `folderPath` is already per-segment percent-encoded: it comes from
80+
// buildVfsFolderPathMap / resolveFolderPath, the same maps that build the keys
81+
// the VFS materializer stores. Build the advertised path with
82+
// canonicalWorkflowVfsDir — the exact helper the materializer and @-mention
83+
// pointers use — so the WORKSPACE.md state path can never drift from the stored
84+
// VFS key. The workflow NAME is the raw display string, so the helper encodes it
85+
// once. Re-encoding the folder segment here is what produced double-encoded
86+
// paths like `The%2520Elder` for a folder named `The Elder`.
8887
function buildWorkflowStatePath(workflowName: string, folderPath?: string | null): string {
89-
const normalizedFolderPath = normalizeFolderPathForVfs(folderPath)
90-
const normalizedWorkflowName = normalizeVfsSegment(workflowName)
91-
return normalizedFolderPath
92-
? `workflows/${normalizedFolderPath}/${normalizedWorkflowName}/state.json`
93-
: `workflows/${normalizedWorkflowName}/state.json`
88+
return `${canonicalWorkflowVfsDir({ name: workflowName, folderPath })}/state.json`
9489
}
9590

9691
/**

0 commit comments

Comments
 (0)