Skip to content

Commit b653bab

Browse files
committed
fix: correct threadId extraction in SuperhumanDraftProvider (closes #14)
The userdata.getThreads API returns threadId at the top level (threadItem.id), not inside the thread object (threadItem.thread.id). This caused threadId to be undefined, producing invalid API paths like threads/undefined/messages/... which returned HTTP 400. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1892bc4 commit b653bab

3 files changed

Lines changed: 80 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "superhuman-cli",
3-
"version": "0.13.1",
3+
"version": "0.13.2",
44
"module": "src/index.ts",
55
"type": "module",
66
"private": true,

src/__tests__/draft-service.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { describe, it, expect } from "bun:test";
88
import { DraftService, type IDraftProvider, type Draft } from "../services/draft-service";
9+
import { SuperhumanDraftProvider } from "../providers/superhuman-draft-provider";
910

1011
// Mock provider for testing
1112
class MockProvider implements IDraftProvider {
@@ -118,3 +119,78 @@ describe("DraftService", () => {
118119
expect(() => new DraftService(notAnArray)).toThrow(TypeError);
119120
});
120121
});
122+
123+
describe("SuperhumanDraftProvider", () => {
124+
it("should correctly extract threadId from getThreads response", () => {
125+
// Regression test: threadId lives at threadItem.id (top level), NOT threadItem.thread.id
126+
// The userdata.getThreads API returns: { id: "draft00...", thread: { historyId: ..., messages: {...} } }
127+
const mockThreadList = [
128+
{
129+
id: "draft00bc30654cd5d898",
130+
thread: {
131+
historyId: 49480,
132+
messages: {
133+
"draft00bc30654cd5d898": {
134+
draft: {
135+
id: "draft00bc30654cd5d898",
136+
subject: "Test Draft",
137+
to: ["recipient@example.com"],
138+
from: "sender@example.com",
139+
snippet: "Draft body preview",
140+
date: "2026-02-26T10:00:00Z",
141+
},
142+
},
143+
},
144+
},
145+
},
146+
];
147+
148+
// Access the private parseThreadList method via prototype
149+
const provider = new SuperhumanDraftProvider({
150+
superhumanToken: { token: "fake" },
151+
} as any);
152+
153+
// Call parseThreadList using bracket notation to access private method
154+
const drafts = (provider as any).parseThreadList(mockThreadList);
155+
156+
expect(drafts).toHaveLength(1);
157+
expect(drafts[0].threadId).toBe("draft00bc30654cd5d898");
158+
expect(drafts[0].threadId).not.toBeUndefined();
159+
expect(drafts[0].id).toBe("draft00bc30654cd5d898");
160+
expect(drafts[0].subject).toBe("Test Draft");
161+
expect(drafts[0].source).toBe("native");
162+
});
163+
164+
it("should handle thread items with no id gracefully", () => {
165+
// Ensure undefined threadId is captured (rather than crashing)
166+
const mockThreadList = [
167+
{
168+
// id is missing at top level
169+
thread: {
170+
historyId: 12345,
171+
messages: {
172+
"draft001": {
173+
draft: {
174+
id: "draft001",
175+
subject: "No Thread ID",
176+
to: [],
177+
from: "test@example.com",
178+
snippet: "",
179+
date: "2026-02-26T10:00:00Z",
180+
},
181+
},
182+
},
183+
},
184+
},
185+
];
186+
187+
const provider = new SuperhumanDraftProvider({
188+
superhumanToken: { token: "fake" },
189+
} as any);
190+
191+
const drafts = (provider as any).parseThreadList(mockThreadList);
192+
193+
expect(drafts).toHaveLength(1);
194+
expect(drafts[0].threadId).toBeUndefined();
195+
});
196+
});

src/providers/superhuman-draft-provider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ interface SuperhumanMessage {
3232
}
3333

3434
interface SuperhumanThread {
35+
id: string; // threadId is at the top level, NOT inside thread
3536
thread: {
36-
id: string;
37+
historyId?: number;
3738
messages: Record<string, SuperhumanMessage>;
3839
};
3940
}
@@ -188,7 +189,7 @@ export class SuperhumanDraftProvider implements IDraftProvider {
188189
const drafts: Draft[] = [];
189190

190191
for (const threadItem of threadList) {
191-
const threadId = threadItem.thread?.id;
192+
const threadId = threadItem.id;
192193
const messages = threadItem.thread?.messages || {};
193194

194195
for (const [messageId, message] of Object.entries(messages)) {

0 commit comments

Comments
 (0)