Skip to content

Commit 1e8d409

Browse files
amixclaude
andcommitted
fix(refs): tighten inbox/saved thread URL routing
Only classify `t/{thread}` with an optional `c/{comment}` suffix after inbox/saved; extra trailing segments stay unclassified so malformed URLs aren't misrouted (e.g. as a conversation). Groups the branch by route prefix and adds saved-thread + strictness test coverage. Addresses review on #30. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 54fbf7c commit 1e8d409

2 files changed

Lines changed: 40 additions & 6 deletions

File tree

src/lib/refs.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ describe('parseCommsUrl', () => {
160160
'https://comms.todoist.com/12345/saved/t/TH1',
161161
{ workspaceId: 12345, threadId: 'TH1' },
162162
],
163+
[
164+
'saved thread with comment URL',
165+
'https://comms.todoist.com/12345/saved/t/TH1/c/CM1',
166+
{ workspaceId: 12345, threadId: 'TH1', commentId: 'CM1' },
167+
],
163168
[
164169
'people URL as workspace-only',
165170
'https://comms.todoist.com/12345/people/u/678',
@@ -169,6 +174,18 @@ describe('parseCommsUrl', () => {
169174
expect(parseCommsUrl(url)).toEqual(expected)
170175
})
171176

177+
it('leaves inbox/saved URLs with extra trailing segments unclassified', () => {
178+
// Only `t/{thread}[/c/{comment}]` is valid after inbox/saved; extra
179+
// segments must not be misrouted (e.g. the trailing msg/CV1 as a
180+
// conversation).
181+
expect(parseCommsUrl('https://comms.todoist.com/12345/inbox/t/TH1/msg/CV1')).toEqual({
182+
workspaceId: 12345,
183+
})
184+
expect(parseCommsUrl('https://comms.todoist.com/12345/saved/t/TH1/extra')).toEqual({
185+
workspaceId: 12345,
186+
})
187+
})
188+
172189
it('parses conversation URL', () => {
173190
const result = parseCommsUrl('https://comms.todoist.com/a/12345/msg/CV1')
174191
expect(result).toEqual({ workspaceId: 12345, conversationId: 'CV1' })
@@ -293,6 +310,11 @@ describe('resolveThreadId', () => {
293310
'inbox thread URL with comment suffix',
294311
'https://comms.todoist.com/12345/inbox/t/TH1/c/CM1',
295312
],
313+
['saved thread URL', 'https://comms.todoist.com/12345/saved/t/TH1'],
314+
[
315+
'saved thread URL with comment suffix',
316+
'https://comms.todoist.com/12345/saved/t/TH1/c/CM1',
317+
],
296318
])('resolves %s', (_description, url) => {
297319
expect(resolveThreadId(url)).toBe('TH1')
298320
})

src/lib/refs.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,24 @@ export function parseCommsUrl(url: string): ParsedCommsUrl | null {
135135
}
136136
}
137137

138-
if (
139-
(segments[routeStart] === 'inbox' || segments[routeStart] === 'saved') &&
140-
segments[routeStart + 1] === 't'
141-
) {
142-
parseRoutePairs(routeStart + 1)
143-
} else if (segments[routeStart] !== 'inbox' && segments[routeStart] !== 'saved') {
138+
const route = segments[routeStart]
139+
if (route === 'inbox' || route === 'saved') {
140+
// Inbox/saved thread URLs only carry `t/{thread}` with an optional
141+
// `c/{comment}` suffix. Anything else (e.g. `inbox/done`, or extra
142+
// trailing segments) stays unclassified so a malformed URL can't be
143+
// misrouted — e.g. as a conversation.
144+
const rest = segments.slice(routeStart + 1)
145+
if (
146+
rest[0] === 't' &&
147+
rest[1] &&
148+
(rest.length === 2 || (rest.length === 4 && rest[2] === 'c'))
149+
) {
150+
result.threadId = rest[1]
151+
if (rest.length === 4) {
152+
result.commentId = rest[3]
153+
}
154+
}
155+
} else {
144156
parseRoutePairs(routeStart)
145157
}
146158

0 commit comments

Comments
 (0)