Skip to content

Commit 5ea2699

Browse files
committed
fix(chat): resolve uploads whose name contains a literal percent
A name like test%2A.zip is exposed double-encoded by glob/upload-context (test%252A.zip) but canonicalUploadKey decodes the input first, so a literal %2A is indistinguishable from an encoded * and the lookup misses. Add an encoded-form fallback (encode the stored name, compare to the raw input) which recovers the row without affecting the U+202F normalization path.
1 parent 1f729ef commit 5ea2699

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ describe('findMothershipUploadRowByChatAndName', () => {
138138

139139
expect(result?.id).toBe('wf_3')
140140
})
141+
142+
it('resolves a literal-% name via its encoded glob form', async () => {
143+
// Stored name has a literal `%`; glob/upload-context expose it double-encoded
144+
// (`test%252A.zip`). The encoded-form fallback recovers the row.
145+
const row = makeRow({
146+
id: 'wf_pct',
147+
displayName: 'test%2A.zip',
148+
contentType: 'application/zip',
149+
})
150+
mockOrderByThenLimit([])
151+
dbChainMockFns.orderBy.mockResolvedValueOnce([row] as never)
152+
153+
const result = await findMothershipUploadRowByChatAndName(CHAT_ID, 'test%252A.zip')
154+
155+
expect(result?.id).toBe('wf_pct')
156+
})
141157
})
142158

143159
describe('listChatUploads', () => {

apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ function canonicalUploadKey(name: string): string {
5656
}
5757
}
5858

59+
/**
60+
* Per-segment encode of a stored name (no decode first), so a name containing a
61+
* literal `%` (e.g. `test%2A.zip`) round-trips: glob/upload-context expose it as
62+
* `encodeVfsSegment(name)`, and matching that encoded form back recovers the row.
63+
* {@link canonicalUploadKey} can't, because it decodes the input first and a
64+
* literal `%2A` is indistinguishable from an encoded `*`.
65+
*/
66+
function encodeUploadName(name: string): string {
67+
try {
68+
return encodeVfsSegment(name)
69+
} catch {
70+
return name.trim()
71+
}
72+
}
73+
5974
/** VFS-visible name. Coalesces to originalName for legacy rows that predate displayName. */
6075
function vfsName(row: typeof workspaceFiles.$inferSelect): string {
6176
return row.displayName ?? row.originalName
@@ -127,7 +142,15 @@ export async function findMothershipUploadRowByChatAndName(
127142
.orderBy(desc(workspaceFiles.uploadedAt), desc(workspaceFiles.id))
128143

129144
const segmentKey = canonicalUploadKey(fileName)
130-
return allRows.find((r) => canonicalUploadKey(vfsName(r)) === segmentKey) ?? null
145+
return (
146+
allRows.find((r) => {
147+
const stored = vfsName(r)
148+
// Canonical-key match handles visually-equivalent spellings (U+202F vs
149+
// space); the encoded-form match handles literal `%` names that survive
150+
// encode but not decode.
151+
return canonicalUploadKey(stored) === segmentKey || encodeUploadName(stored) === fileName
152+
}) ?? null
153+
)
131154
}
132155

133156
/**

0 commit comments

Comments
 (0)