@askverdict/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/client.test.js +472 -0
- package/dist/__tests__/client.test.js.map +1 -0
- package/dist/client.d.ts +167 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +503 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +227 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AskVerdict AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @askverdict/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the [AskVerdict AI](https://askverdict.ai) debate engine API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @askverdict/sdk
|
|
9
|
+
# or
|
|
10
|
+
npm install @askverdict/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { AskVerdictClient } from '@askverdict/sdk';
|
|
17
|
+
|
|
18
|
+
const client = new AskVerdictClient({ apiKey: process.env.ASKVERDICT_API_KEY });
|
|
19
|
+
|
|
20
|
+
// Create a verdict
|
|
21
|
+
const { verdict } = await client.createVerdict({
|
|
22
|
+
question: 'Should I use PostgreSQL or MongoDB for my SaaS?',
|
|
23
|
+
mode: 'balanced',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(verdict.verdict.recommendation);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Streaming
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const { verdict } = await client.createVerdict({
|
|
33
|
+
question: 'React vs Vue for a new project?',
|
|
34
|
+
mode: 'thorough',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
for await (const event of client.streamVerdict(verdict.id)) {
|
|
38
|
+
switch (event.type) {
|
|
39
|
+
case 'agent_thinking':
|
|
40
|
+
console.log(`${event.agentName} is thinking...`);
|
|
41
|
+
break;
|
|
42
|
+
case 'agent_argument':
|
|
43
|
+
console.log(`${event.agentName}: ${event.content}`);
|
|
44
|
+
break;
|
|
45
|
+
case 'verdict_complete':
|
|
46
|
+
console.log('Verdict:', event.verdict.recommendation);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
Three ways to authenticate:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// 1. API key (server-side)
|
|
58
|
+
const client = new AskVerdictClient({ apiKey: 'ask_...' });
|
|
59
|
+
|
|
60
|
+
// 2. Auth token (browser, from Better Auth session)
|
|
61
|
+
const client = new AskVerdictClient({ authToken: 'session-token' });
|
|
62
|
+
|
|
63
|
+
// 3. Custom base URL (self-hosted or dev)
|
|
64
|
+
const client = new AskVerdictClient({
|
|
65
|
+
apiKey: 'ask_...',
|
|
66
|
+
baseUrl: 'http://localhost:9100',
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### Verdicts
|
|
73
|
+
|
|
74
|
+
| Method | Description |
|
|
75
|
+
|--------|-------------|
|
|
76
|
+
| `createVerdict(params)` | Start a new AI debate |
|
|
77
|
+
| `getVerdict(id)` | Get a debate by ID |
|
|
78
|
+
| `listVerdicts(params?)` | List your debates (paginated) |
|
|
79
|
+
| `deleteVerdict(id)` | Delete a debate |
|
|
80
|
+
| `streamVerdict(id)` | Stream live debate events (SSE) |
|
|
81
|
+
|
|
82
|
+
### Voting
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `getVotes(debateId)` | Get vote tallies for all claims |
|
|
87
|
+
| `castVote(debateId, claimId, vote)` | Vote on a claim (agree/disagree/neutral) |
|
|
88
|
+
| `removeVote(debateId, claimId)` | Remove your vote |
|
|
89
|
+
|
|
90
|
+
### Polls
|
|
91
|
+
|
|
92
|
+
| Method | Description |
|
|
93
|
+
|--------|-------------|
|
|
94
|
+
| `getPolls(debateId)` | Get polls for a debate |
|
|
95
|
+
| `createPoll(debateId, question, options)` | Create a poll |
|
|
96
|
+
| `votePoll(debateId, pollId, optionId)` | Vote on a poll |
|
|
97
|
+
| `closePoll(debateId, pollId)` | Close a poll |
|
|
98
|
+
| `deletePoll(debateId, pollId)` | Delete a poll |
|
|
99
|
+
|
|
100
|
+
### Billing
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `getBalance()` | Get credit balance and plan info |
|
|
105
|
+
| `createSubscriptionCheckout(plan, interval)` | Create Stripe checkout |
|
|
106
|
+
| `createCreditCheckout(pack)` | Buy credit pack |
|
|
107
|
+
| `createPortalSession()` | Open Stripe portal |
|
|
108
|
+
|
|
109
|
+
### Search and Stats
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| `search(query, opts?)` | Search debates |
|
|
114
|
+
| `getDashboard(workspaceId?)` | Get dashboard statistics |
|
|
115
|
+
| `getScore()` | Get decision accuracy score |
|
|
116
|
+
| `getStreak()` | Get debate streak info |
|
|
117
|
+
|
|
118
|
+
### Outcomes
|
|
119
|
+
|
|
120
|
+
| Method | Description |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `getOutcome(debateId)` | Get recorded outcome |
|
|
123
|
+
| `submitOutcome(debateId, params)` | Record a decision outcome |
|
|
124
|
+
| `getPendingOutcomes()` | List debates awaiting outcomes |
|
|
125
|
+
| `getOutcomeHistory(opts?)` | Get outcome history |
|
|
126
|
+
|
|
127
|
+
### Health
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
|--------|-------------|
|
|
131
|
+
| `health()` | Check API health (no auth required) |
|
|
132
|
+
|
|
133
|
+
## Error Handling
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { AskVerdictClient, AskVerdictError } from '@askverdict/sdk';
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const result = await client.createVerdict({ question: '...' });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof AskVerdictError) {
|
|
142
|
+
console.error(`[${error.status}] ${error.code}: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/client.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { AskVerdictClient } from "../client.js";
|
|
3
|
+
import { AskVerdictError } from "../types.js";
|
|
4
|
+
// ── Fetch mock setup ───────────────────────────────────────────────────────────
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
global.fetch = mockFetch;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
mockFetch.mockReset();
|
|
10
|
+
});
|
|
11
|
+
function mockResponse(body, status = 200) {
|
|
12
|
+
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify(body), {
|
|
13
|
+
status,
|
|
14
|
+
headers: { "Content-Type": "application/json" },
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function mockNetworkError(message = "Failed to fetch") {
|
|
18
|
+
mockFetch.mockRejectedValueOnce(new Error(message));
|
|
19
|
+
}
|
|
20
|
+
// ── Constructor ────────────────────────────────────────────────────────────────
|
|
21
|
+
describe("AskVerdictClient — constructor", () => {
|
|
22
|
+
it("uses default base URL when none provided", async () => {
|
|
23
|
+
const client = new AskVerdictClient();
|
|
24
|
+
mockResponse({ status: "ok", service: "askverdict", timestamp: "", version: "1.0.0" });
|
|
25
|
+
await client.health();
|
|
26
|
+
const [url] = mockFetch.mock.calls[0];
|
|
27
|
+
expect(url).toBe("https://api.askverdict.ai/v1/health");
|
|
28
|
+
});
|
|
29
|
+
it("uses provided baseUrl and strips trailing slash", async () => {
|
|
30
|
+
const client = new AskVerdictClient({ baseUrl: "https://api.example.com/" });
|
|
31
|
+
mockResponse({ status: "ok", service: "askverdict", timestamp: "", version: "1.0.0" });
|
|
32
|
+
await client.health();
|
|
33
|
+
const [url] = mockFetch.mock.calls[0];
|
|
34
|
+
expect(url).toBe("https://api.example.com/v1/health");
|
|
35
|
+
});
|
|
36
|
+
it("sets X-Api-Key header when apiKey is provided", async () => {
|
|
37
|
+
const client = new AskVerdictClient({ apiKey: "my-api-key" });
|
|
38
|
+
mockResponse({ status: "ok", service: "askverdict", timestamp: "", version: "1.0.0" });
|
|
39
|
+
await client.health();
|
|
40
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
41
|
+
expect(init.headers["X-Api-Key"]).toBe("my-api-key");
|
|
42
|
+
});
|
|
43
|
+
it("sets Authorization Bearer header when authToken is provided", async () => {
|
|
44
|
+
const client = new AskVerdictClient({ authToken: "tok_abc123" });
|
|
45
|
+
mockResponse({ status: "ok", service: "askverdict", timestamp: "", version: "1.0.0" });
|
|
46
|
+
await client.health();
|
|
47
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
48
|
+
expect(init.headers["Authorization"]).toBe("Bearer tok_abc123");
|
|
49
|
+
});
|
|
50
|
+
it("sets no auth headers when neither apiKey nor authToken provided", async () => {
|
|
51
|
+
const client = new AskVerdictClient();
|
|
52
|
+
mockResponse({ status: "ok", service: "askverdict", timestamp: "", version: "1.0.0" });
|
|
53
|
+
await client.health();
|
|
54
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
55
|
+
const headers = init.headers;
|
|
56
|
+
expect(headers["X-Api-Key"]).toBeUndefined();
|
|
57
|
+
expect(headers["Authorization"]).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// ── health() ──────────────────────────────────────────────────────────────────
|
|
61
|
+
describe("AskVerdictClient.health()", () => {
|
|
62
|
+
it("returns a health response on 200", async () => {
|
|
63
|
+
const client = new AskVerdictClient();
|
|
64
|
+
const body = {
|
|
65
|
+
status: "ok",
|
|
66
|
+
service: "askverdict-api",
|
|
67
|
+
timestamp: "2026-02-18T00:00:00Z",
|
|
68
|
+
version: "1.2.3",
|
|
69
|
+
};
|
|
70
|
+
mockResponse(body);
|
|
71
|
+
const result = await client.health();
|
|
72
|
+
expect(result).toEqual(body);
|
|
73
|
+
});
|
|
74
|
+
it("sends GET to /health", async () => {
|
|
75
|
+
const client = new AskVerdictClient();
|
|
76
|
+
mockResponse({ status: "ok", service: "s", timestamp: "", version: "1" });
|
|
77
|
+
await client.health();
|
|
78
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
79
|
+
expect(init.method).toBe("GET");
|
|
80
|
+
});
|
|
81
|
+
it("throws AskVerdictError on non-200 response", async () => {
|
|
82
|
+
const client = new AskVerdictClient();
|
|
83
|
+
mockResponse({ error: { code: "INTERNAL_ERROR", message: "Something broke" } }, 500);
|
|
84
|
+
await expect(client.health()).rejects.toThrow(AskVerdictError);
|
|
85
|
+
});
|
|
86
|
+
it("carries status code on AskVerdictError for non-200 responses", async () => {
|
|
87
|
+
const client = new AskVerdictClient();
|
|
88
|
+
mockResponse({ error: { code: "INTERNAL_ERROR", message: "Something broke" } }, 500);
|
|
89
|
+
let caught;
|
|
90
|
+
try {
|
|
91
|
+
await client.health();
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (e instanceof AskVerdictError)
|
|
95
|
+
caught = e;
|
|
96
|
+
}
|
|
97
|
+
expect(caught?.status).toBe(500);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
// ── createDebate() ────────────────────────────────────────────────────────────
|
|
101
|
+
describe("AskVerdictClient.createDebate()", () => {
|
|
102
|
+
it("sends POST to /v1/verdicts with correct body", async () => {
|
|
103
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
104
|
+
const returnBody = { id: "d1", status: "pending", streamUrl: "/v1/verdicts/d1/stream" };
|
|
105
|
+
mockResponse(returnBody, 201);
|
|
106
|
+
await client.createDebate({ question: "Is TypeScript worth it?" });
|
|
107
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
108
|
+
expect(url).toContain("/v1/verdicts");
|
|
109
|
+
expect(init.method).toBe("POST");
|
|
110
|
+
const sentBody = JSON.parse(init.body);
|
|
111
|
+
expect(sentBody["question"]).toBe("Is TypeScript worth it?");
|
|
112
|
+
});
|
|
113
|
+
it("sends all optional params in the body", async () => {
|
|
114
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
115
|
+
mockResponse({ id: "d2", status: "pending", streamUrl: "/v1/verdicts/d2/stream" }, 201);
|
|
116
|
+
await client.createDebate({
|
|
117
|
+
question: "Tabs or spaces?",
|
|
118
|
+
mode: "thorough",
|
|
119
|
+
agentCount: 4,
|
|
120
|
+
maxRounds: 3,
|
|
121
|
+
enableSearch: true,
|
|
122
|
+
context: "Engineering context",
|
|
123
|
+
});
|
|
124
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
125
|
+
const sentBody = JSON.parse(init.body);
|
|
126
|
+
expect(sentBody["mode"]).toBe("thorough");
|
|
127
|
+
expect(sentBody["agentCount"]).toBe(4);
|
|
128
|
+
expect(sentBody["maxRounds"]).toBe(3);
|
|
129
|
+
expect(sentBody["enableSearch"]).toBe(true);
|
|
130
|
+
expect(sentBody["context"]).toBe("Engineering context");
|
|
131
|
+
});
|
|
132
|
+
it("returns CreateDebateResult with id, debateId alias, and streamUrl", async () => {
|
|
133
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
134
|
+
const returnBody = { id: "d3", status: "pending", streamUrl: "/v1/verdicts/d3/stream" };
|
|
135
|
+
mockResponse(returnBody, 201);
|
|
136
|
+
const result = await client.createDebate({ question: "Test?" });
|
|
137
|
+
expect(result.id).toBe("d3");
|
|
138
|
+
expect(result.debateId).toBe("d3");
|
|
139
|
+
expect(result.streamUrl).toBe("/v1/verdicts/d3/stream");
|
|
140
|
+
});
|
|
141
|
+
it("throws AskVerdictError on 422 validation error", async () => {
|
|
142
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
143
|
+
mockResponse({ error: { code: "VALIDATION_ERROR", message: "question is required" } }, 422);
|
|
144
|
+
await expect(client.createDebate({ question: "" })).rejects.toThrow(AskVerdictError);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ── getDebate() ───────────────────────────────────────────────────────────────
|
|
148
|
+
describe("AskVerdictClient.getDebate()", () => {
|
|
149
|
+
it("sends GET to the correct verdict path", async () => {
|
|
150
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
151
|
+
mockResponse({ verdict: { id: "abc-123", question: "?", status: "completed" } });
|
|
152
|
+
await client.getDebate("abc-123");
|
|
153
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
154
|
+
expect(url).toContain("/v1/verdicts/abc-123");
|
|
155
|
+
expect(init.method).toBe("GET");
|
|
156
|
+
});
|
|
157
|
+
it("URL-encodes the debate ID", async () => {
|
|
158
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
159
|
+
mockResponse({ verdict: { id: "id with spaces", question: "?", status: "pending" } });
|
|
160
|
+
await client.getDebate("id with spaces");
|
|
161
|
+
const [url] = mockFetch.mock.calls[0];
|
|
162
|
+
expect(url).toContain("id%20with%20spaces");
|
|
163
|
+
});
|
|
164
|
+
it("throws AskVerdictError with status 404 when debate not found", async () => {
|
|
165
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
166
|
+
mockResponse({ error: { code: "NOT_FOUND", message: "Debate not found" } }, 404);
|
|
167
|
+
let caught;
|
|
168
|
+
try {
|
|
169
|
+
await client.getDebate("nonexistent");
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
if (e instanceof AskVerdictError)
|
|
173
|
+
caught = e;
|
|
174
|
+
}
|
|
175
|
+
expect(caught).toBeInstanceOf(AskVerdictError);
|
|
176
|
+
expect(caught?.status).toBe(404);
|
|
177
|
+
expect(caught?.code).toBe("NOT_FOUND");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ── listDebates() ─────────────────────────────────────────────────────────────
|
|
181
|
+
describe("AskVerdictClient.listDebates()", () => {
|
|
182
|
+
it("sends GET to /v1/verdicts with no query string when called with no params", async () => {
|
|
183
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
184
|
+
mockResponse({ verdicts: [], total: 0, page: 1, pageSize: 20, hasMore: false });
|
|
185
|
+
await client.listDebates();
|
|
186
|
+
const [url] = mockFetch.mock.calls[0];
|
|
187
|
+
expect(url).toBe("https://api.askverdict.ai/v1/verdicts");
|
|
188
|
+
});
|
|
189
|
+
it("sends limit in query string", async () => {
|
|
190
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
191
|
+
mockResponse({ verdicts: [], total: 0, page: 1, pageSize: 10, hasMore: false });
|
|
192
|
+
await client.listDebates({ limit: 10 });
|
|
193
|
+
const [url] = mockFetch.mock.calls[0];
|
|
194
|
+
expect(url).toContain("limit=10");
|
|
195
|
+
});
|
|
196
|
+
it("sends status filter in query string", async () => {
|
|
197
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
198
|
+
mockResponse({ verdicts: [], total: 0, page: 1, pageSize: 20, hasMore: false });
|
|
199
|
+
await client.listDebates({ status: "completed" });
|
|
200
|
+
const [url] = mockFetch.mock.calls[0];
|
|
201
|
+
expect(url).toContain("status=completed");
|
|
202
|
+
});
|
|
203
|
+
it("normalizes verdicts key to debates in response", async () => {
|
|
204
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
205
|
+
const returnBody = {
|
|
206
|
+
verdicts: [{ id: "d1", question: "Q?", status: "completed" }],
|
|
207
|
+
total: 1,
|
|
208
|
+
page: 1,
|
|
209
|
+
pageSize: 20,
|
|
210
|
+
hasMore: false,
|
|
211
|
+
};
|
|
212
|
+
mockResponse(returnBody);
|
|
213
|
+
const result = await client.listDebates();
|
|
214
|
+
expect(result.total).toBe(1);
|
|
215
|
+
expect(result.debates).toHaveLength(1);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// ── deleteDebate() ────────────────────────────────────────────────────────────
|
|
219
|
+
describe("AskVerdictClient.deleteDebate()", () => {
|
|
220
|
+
it("sends DELETE to the correct verdict path", async () => {
|
|
221
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
222
|
+
mockResponse({ deleted: true });
|
|
223
|
+
await client.deleteDebate("debate-99");
|
|
224
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
225
|
+
expect(url).toContain("/v1/verdicts/debate-99");
|
|
226
|
+
expect(init.method).toBe("DELETE");
|
|
227
|
+
});
|
|
228
|
+
it("resolves without a return value on success", async () => {
|
|
229
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
230
|
+
mockResponse({ deleted: true });
|
|
231
|
+
const result = await client.deleteDebate("d1");
|
|
232
|
+
expect(result).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
// ── castVote() ────────────────────────────────────────────────────────────────
|
|
236
|
+
describe("AskVerdictClient.castVote()", () => {
|
|
237
|
+
it("sends POST to the votes endpoint with claimId and vote", async () => {
|
|
238
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
239
|
+
mockResponse({ success: true });
|
|
240
|
+
await client.castVote("debate-1", "claim-A", "agree");
|
|
241
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
242
|
+
expect(url).toContain("/api/debates/debate-1/votes");
|
|
243
|
+
expect(init.method).toBe("POST");
|
|
244
|
+
const body = JSON.parse(init.body);
|
|
245
|
+
expect(body["claimId"]).toBe("claim-A");
|
|
246
|
+
expect(body["vote"]).toBe("agree");
|
|
247
|
+
});
|
|
248
|
+
it("sends neutral vote to remove existing vote", async () => {
|
|
249
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
250
|
+
mockResponse({ success: true });
|
|
251
|
+
await client.castVote("debate-1", "claim-B", "neutral");
|
|
252
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
253
|
+
const body = JSON.parse(init.body);
|
|
254
|
+
expect(body["vote"]).toBe("neutral");
|
|
255
|
+
});
|
|
256
|
+
it("sends disagree vote correctly", async () => {
|
|
257
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
258
|
+
mockResponse({ success: true });
|
|
259
|
+
await client.castVote("debate-2", "claim-C", "disagree");
|
|
260
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
261
|
+
const body = JSON.parse(init.body);
|
|
262
|
+
expect(body["vote"]).toBe("disagree");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
// ── removeVote() ──────────────────────────────────────────────────────────────
|
|
266
|
+
describe("AskVerdictClient.removeVote()", () => {
|
|
267
|
+
it("sends DELETE to the correct votes path", async () => {
|
|
268
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
269
|
+
mockResponse({ success: true });
|
|
270
|
+
await client.removeVote("debate-1", "claim-A");
|
|
271
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
272
|
+
expect(url).toContain("/api/debates/debate-1/votes/claim-A");
|
|
273
|
+
expect(init.method).toBe("DELETE");
|
|
274
|
+
});
|
|
275
|
+
it("URL-encodes both debateId and claimId", async () => {
|
|
276
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
277
|
+
mockResponse({ success: true });
|
|
278
|
+
await client.removeVote("debate/1", "claim/A");
|
|
279
|
+
const [url] = mockFetch.mock.calls[0];
|
|
280
|
+
expect(url).toContain("debate%2F1");
|
|
281
|
+
expect(url).toContain("claim%2FA");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
// ── getVotes() ────────────────────────────────────────────────────────────────
|
|
285
|
+
describe("AskVerdictClient.getVotes()", () => {
|
|
286
|
+
it("sends GET to the votes endpoint and returns the votes map", async () => {
|
|
287
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
288
|
+
const votesPayload = {
|
|
289
|
+
votes: {
|
|
290
|
+
"claim-1": { agree: 5, disagree: 2, userVote: "agree" },
|
|
291
|
+
"claim-2": { agree: 0, disagree: 3 },
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
mockResponse(votesPayload);
|
|
295
|
+
const result = await client.getVotes("debate-1");
|
|
296
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
297
|
+
expect(url).toContain("/api/debates/debate-1/votes");
|
|
298
|
+
expect(init.method).toBe("GET");
|
|
299
|
+
expect(result["claim-1"]?.agree).toBe(5);
|
|
300
|
+
expect(result["claim-2"]?.disagree).toBe(3);
|
|
301
|
+
});
|
|
302
|
+
it("unwraps the votes property from the API response", async () => {
|
|
303
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
304
|
+
mockResponse({ votes: { "c1": { agree: 1, disagree: 0 } } });
|
|
305
|
+
const result = await client.getVotes("debate-1");
|
|
306
|
+
expect(result).not.toHaveProperty("votes");
|
|
307
|
+
expect(result).toHaveProperty("c1");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
// ── getPolls() ────────────────────────────────────────────────────────────────
|
|
311
|
+
describe("AskVerdictClient.getPolls()", () => {
|
|
312
|
+
it("sends GET to the polls endpoint and returns polls array", async () => {
|
|
313
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
314
|
+
const poll = {
|
|
315
|
+
id: "poll-1",
|
|
316
|
+
debateId: "debate-1",
|
|
317
|
+
question: "Do you agree?",
|
|
318
|
+
options: [{ id: "opt-1", label: "Yes" }, { id: "opt-2", label: "No" }],
|
|
319
|
+
status: "open",
|
|
320
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
321
|
+
closedAt: null,
|
|
322
|
+
tallies: {},
|
|
323
|
+
totalVotes: 0,
|
|
324
|
+
userVote: null,
|
|
325
|
+
};
|
|
326
|
+
mockResponse({ polls: [poll] });
|
|
327
|
+
const result = await client.getPolls("debate-1");
|
|
328
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
329
|
+
expect(url).toContain("/api/debates/debate-1/polls");
|
|
330
|
+
expect(init.method).toBe("GET");
|
|
331
|
+
expect(result).toHaveLength(1);
|
|
332
|
+
expect(result[0]?.id).toBe("poll-1");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// ── createPoll() ──────────────────────────────────────────────────────────────
|
|
336
|
+
describe("AskVerdictClient.createPoll()", () => {
|
|
337
|
+
it("sends POST with question and options, returns Poll", async () => {
|
|
338
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
339
|
+
const createdPoll = {
|
|
340
|
+
id: "poll-new",
|
|
341
|
+
debateId: "debate-1",
|
|
342
|
+
question: "Which is better?",
|
|
343
|
+
options: [{ id: "o1", label: "A" }, { id: "o2", label: "B" }],
|
|
344
|
+
status: "open",
|
|
345
|
+
createdAt: "2026-02-18T00:00:00Z",
|
|
346
|
+
closedAt: null,
|
|
347
|
+
tallies: {},
|
|
348
|
+
totalVotes: 0,
|
|
349
|
+
userVote: null,
|
|
350
|
+
};
|
|
351
|
+
mockResponse({ poll: createdPoll });
|
|
352
|
+
const result = await client.createPoll("debate-1", "Which is better?", ["A", "B"]);
|
|
353
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
354
|
+
expect(url).toContain("/api/debates/debate-1/polls");
|
|
355
|
+
expect(init.method).toBe("POST");
|
|
356
|
+
const body = JSON.parse(init.body);
|
|
357
|
+
expect(body["question"]).toBe("Which is better?");
|
|
358
|
+
expect(body["options"]).toEqual(["A", "B"]);
|
|
359
|
+
expect(result.id).toBe("poll-new");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
// ── votePoll() ────────────────────────────────────────────────────────────────
|
|
363
|
+
describe("AskVerdictClient.votePoll()", () => {
|
|
364
|
+
it("sends POST to the correct vote URL with optionId in body", async () => {
|
|
365
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
366
|
+
mockResponse({ success: true });
|
|
367
|
+
await client.votePoll("debate-1", "poll-1", "opt-1");
|
|
368
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
369
|
+
expect(url).toContain("/api/debates/debate-1/polls/poll-1/vote");
|
|
370
|
+
expect(init.method).toBe("POST");
|
|
371
|
+
const body = JSON.parse(init.body);
|
|
372
|
+
expect(body["optionId"]).toBe("opt-1");
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// ── Error handling ────────────────────────────────────────────────────────────
|
|
376
|
+
describe("AskVerdictClient — error handling", () => {
|
|
377
|
+
it("throws AskVerdictError with code VALIDATION_ERROR on 400", async () => {
|
|
378
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
379
|
+
mockResponse({ error: { code: "VALIDATION_ERROR", message: "Invalid request body" } }, 400);
|
|
380
|
+
let caught;
|
|
381
|
+
try {
|
|
382
|
+
await client.createDebate({ question: "" });
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
if (e instanceof AskVerdictError)
|
|
386
|
+
caught = e;
|
|
387
|
+
}
|
|
388
|
+
expect(caught).toBeInstanceOf(AskVerdictError);
|
|
389
|
+
expect(caught?.code).toBe("VALIDATION_ERROR");
|
|
390
|
+
expect(caught?.status).toBe(400);
|
|
391
|
+
});
|
|
392
|
+
it("throws AskVerdictError with code UNAUTHORIZED on 401", async () => {
|
|
393
|
+
const client = new AskVerdictClient();
|
|
394
|
+
mockResponse({ error: { code: "UNAUTHORIZED", message: "Authentication required" } }, 401);
|
|
395
|
+
let caught;
|
|
396
|
+
try {
|
|
397
|
+
await client.listDebates();
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
if (e instanceof AskVerdictError)
|
|
401
|
+
caught = e;
|
|
402
|
+
}
|
|
403
|
+
expect(caught?.code).toBe("UNAUTHORIZED");
|
|
404
|
+
expect(caught?.status).toBe(401);
|
|
405
|
+
});
|
|
406
|
+
it("throws AskVerdictError with code HTTP_ERROR on 500 with non-JSON body", async () => {
|
|
407
|
+
const client = new AskVerdictClient();
|
|
408
|
+
mockFetch.mockResolvedValueOnce(new Response("Internal Server Error", {
|
|
409
|
+
status: 500,
|
|
410
|
+
headers: { "Content-Type": "text/plain" },
|
|
411
|
+
}));
|
|
412
|
+
let caught;
|
|
413
|
+
try {
|
|
414
|
+
await client.health();
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
if (e instanceof AskVerdictError)
|
|
418
|
+
caught = e;
|
|
419
|
+
}
|
|
420
|
+
expect(caught).toBeInstanceOf(AskVerdictError);
|
|
421
|
+
expect(caught?.code).toBe("HTTP_ERROR");
|
|
422
|
+
expect(caught?.status).toBe(500);
|
|
423
|
+
});
|
|
424
|
+
it("throws AskVerdictError with code NETWORK_ERROR on fetch rejection", async () => {
|
|
425
|
+
const client = new AskVerdictClient();
|
|
426
|
+
mockNetworkError("ECONNREFUSED");
|
|
427
|
+
let caught;
|
|
428
|
+
try {
|
|
429
|
+
await client.health();
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
if (e instanceof AskVerdictError)
|
|
433
|
+
caught = e;
|
|
434
|
+
}
|
|
435
|
+
expect(caught).toBeInstanceOf(AskVerdictError);
|
|
436
|
+
expect(caught?.code).toBe("NETWORK_ERROR");
|
|
437
|
+
expect(caught?.message).toContain("ECONNREFUSED");
|
|
438
|
+
});
|
|
439
|
+
it("throws AskVerdictError with details field when API provides them", async () => {
|
|
440
|
+
const client = new AskVerdictClient({ authToken: "tok" });
|
|
441
|
+
mockResponse({
|
|
442
|
+
error: {
|
|
443
|
+
code: "VALIDATION_ERROR",
|
|
444
|
+
message: "Invalid fields",
|
|
445
|
+
details: { field: "question", reason: "too_short" },
|
|
446
|
+
},
|
|
447
|
+
}, 422);
|
|
448
|
+
let caught;
|
|
449
|
+
try {
|
|
450
|
+
await client.createDebate({ question: "?" });
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
if (e instanceof AskVerdictError)
|
|
454
|
+
caught = e;
|
|
455
|
+
}
|
|
456
|
+
expect(caught?.details).toEqual({ field: "question", reason: "too_short" });
|
|
457
|
+
});
|
|
458
|
+
it("falls back to API_ERROR code when error body has no code field", async () => {
|
|
459
|
+
const client = new AskVerdictClient();
|
|
460
|
+
mockResponse({ error: { message: "Something went wrong" } }, 500);
|
|
461
|
+
let caught;
|
|
462
|
+
try {
|
|
463
|
+
await client.health();
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
if (e instanceof AskVerdictError)
|
|
467
|
+
caught = e;
|
|
468
|
+
}
|
|
469
|
+
expect(caught?.code).toBe("API_ERROR");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
//# sourceMappingURL=client.test.js.map
|