@thehammer/danx-dashboard-mcp 0.1.22 → 0.1.24
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/README.md +1 -1
- package/dist/handlers.js +13 -5
- package/dist/index.js +23 -6
- package/dist/priority.js +70 -0
- package/package.json +1 -1
- package/dist/handlers.test.js +0 -51
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `m
|
|
|
24
24
|
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
25
25
|
| `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
|
|
26
26
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
27
|
-
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose
|
|
27
|
+
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
28
|
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
|
|
29
29
|
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
|
|
30
30
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
package/dist/handlers.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { readFile } from "node:fs/promises";
|
|
25
25
|
import { basename, extname, isAbsolute } from "node:path";
|
|
26
|
+
import { resolvePriority } from "./priority.js";
|
|
26
27
|
export async function issueList(client, args) {
|
|
27
28
|
// status_derived is display-only on the server but the route accepts
|
|
28
29
|
// it as a projection filter — passthrough verbatim. parent_id=null is
|
|
@@ -88,8 +89,8 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
88
89
|
body.effort_level = args.effort_level;
|
|
89
90
|
if (args.list_id !== undefined)
|
|
90
91
|
body.list_id = args.list_id;
|
|
91
|
-
if (args.
|
|
92
|
-
body.
|
|
92
|
+
if (args.gate_decisions !== undefined)
|
|
93
|
+
body.gate_decisions = args.gate_decisions;
|
|
93
94
|
if (args.phase_children !== undefined)
|
|
94
95
|
body.phase_children = args.phase_children;
|
|
95
96
|
return client.request({ method: "POST", path: "", body, board });
|
|
@@ -97,8 +98,9 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
97
98
|
export async function issueEdit(client, args) {
|
|
98
99
|
// `board` is a transport concern (query), NOT a patch field — pull it
|
|
99
100
|
// out of `rest` so it never leaks into the `/edit` body (the server
|
|
100
|
-
// 400s on non-allowlisted keys).
|
|
101
|
-
|
|
101
|
+
// 400s on non-allowlisted keys). `priority` is pulled out separately so it
|
|
102
|
+
// can be resolved (tier word → numeric) before it reaches the body.
|
|
103
|
+
const { id, board, priority, ...rest } = args;
|
|
102
104
|
// Strip undefined so we don't send `{title: undefined}` — JSON.stringify
|
|
103
105
|
// drops them anyway but explicit is clearer.
|
|
104
106
|
const body = {};
|
|
@@ -106,6 +108,12 @@ export async function issueEdit(client, args) {
|
|
|
106
108
|
if (v !== undefined)
|
|
107
109
|
body[k] = v;
|
|
108
110
|
}
|
|
111
|
+
// DX-1532 — resolve a tier WORD to its numeric midpoint (inverse of
|
|
112
|
+
// priorityTier); a raw number passes through for the route to range-validate.
|
|
113
|
+
// Omitted (undefined) → never sent, so a prose-only edit is unchanged.
|
|
114
|
+
if (priority !== undefined) {
|
|
115
|
+
body.priority = resolvePriority(priority);
|
|
116
|
+
}
|
|
109
117
|
return client.request({
|
|
110
118
|
method: "PATCH",
|
|
111
119
|
path: `/${encodeURIComponent(id)}/edit`,
|
|
@@ -338,7 +346,7 @@ export async function issueRequiresHuman(client, args) {
|
|
|
338
346
|
* POST /api/issues/:id/quality-gates/:gate {required} — the same write the
|
|
339
347
|
* dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY
|
|
340
348
|
* post-create path to mark a gate required/not — `issue_create` carries
|
|
341
|
-
* `
|
|
349
|
+
* `gate_decisions` at birth, and `issue_edit` rejects gate keys; without
|
|
342
350
|
* this tool an agent that created a card cannot turn a gate on afterward.
|
|
343
351
|
*
|
|
344
352
|
* `gate` is a registry name (`plan-dependency` | `plan-architecture` |
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
53
53
|
import { z } from "zod";
|
|
54
54
|
import { DashboardHttpClient } from "./http-client.js";
|
|
55
55
|
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueRequiresHuman, issueRetro, issueTransition, issueTriage, } from "./handlers.js";
|
|
56
|
+
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
56
57
|
function readEnvOrDie(name) {
|
|
57
58
|
const v = process.env[name];
|
|
58
59
|
if (typeof v !== "string" || v === "") {
|
|
@@ -164,7 +165,7 @@ server.tool("issue_get", 'Fetch a single hydrated issue via GET /api/issues/:id.
|
|
|
164
165
|
...boardField,
|
|
165
166
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
166
167
|
// ---------------- issue_create ----------------
|
|
167
|
-
server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-scoped; defaults to the dispatch\'s board. Pass `board` (a qualified id `<repo>:<slug>`) to create the card on another board (forwarded into body.board + ?board=; unknown board → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic\'s id as parent_id. Optional list_id PLACES the card directly into a column in ONE call. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, emoji-tolerant — e.g. a queue name like "⚙️ Fulfillment Queue" or just "Fulfillment Queue") — the server resolves a name to its id.** The card lands DIRECTLY in that column with the matching lifecycle stamped automatically — a `ready`-type queue → ToDo, a `completed` list → Done, etc. **You do NOT need a separate issue_transition(ready) + issue_edit(list_id) afterward — just pass the queue name here and the card is created already in that column.** Omit list_id for the default (Review). NOT valid on type=Epic (Epic status derives from children) → 400. Unknown name/id → 400.
|
|
168
|
+
server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-scoped; defaults to the dispatch\'s board. Pass `board` (a qualified id `<repo>:<slug>`) to create the card on another board (forwarded into body.board + ?board=; unknown board → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic\'s id as parent_id. Optional list_id PLACES the card directly into a column in ONE call. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, emoji-tolerant — e.g. a queue name like "⚙️ Fulfillment Queue" or just "Fulfillment Queue") — the server resolves a name to its id.** The card lands DIRECTLY in that column with the matching lifecycle stamped automatically — a `ready`-type queue → ToDo, a `completed` list → Done, etc. **You do NOT need a separate issue_transition(ready) + issue_edit(list_id) afterward — just pass the queue name here and the card is created already in that column.** Omit list_id for the default (Review). NOT valid on type=Epic (Epic status derives from children) → 400. Unknown name/id → 400. **gate_decisions is REQUIRED whenever the board has any OPTIONAL quality gate for the card\'s type** (DX-1594): supply one `{gate, enabled, note}` per board-optional gate. The create FAILS CLOSED — a missing decision returns 400 `{error, required_gate_decisions:[...]}` enumerating exactly which gates to answer, so just retry with a decision for each listed gate. `required`/`disabled` board gates take no decision; a board with no optional gates needs no gate_decisions at all.', {
|
|
168
169
|
type: z.enum(ISSUE_TYPES),
|
|
169
170
|
title: z.string().min(1),
|
|
170
171
|
description: z.string(),
|
|
@@ -172,10 +173,14 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
172
173
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
173
174
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
174
175
|
list_id: z.string().min(1).nullable().optional(),
|
|
175
|
-
|
|
176
|
-
.array(z.
|
|
176
|
+
gate_decisions: z
|
|
177
|
+
.array(z.object({
|
|
178
|
+
gate: z.string().min(1),
|
|
179
|
+
enabled: z.boolean(),
|
|
180
|
+
note: z.string(),
|
|
181
|
+
}))
|
|
177
182
|
.optional()
|
|
178
|
-
.describe('
|
|
183
|
+
.describe('REQUIRED fail-closed quality-gate decisions (DX-1594 — replaces required_gates). One {gate, enabled, note} per board-OPTIONAL gate of the card\'s type: `enabled` answers whether the gate runs on this card, `note` records the rationale (persisted as the decision rationale, distinct from the reviewer verdict). The board requirement is TRI-STATE per gate (`board_quality_gate_settings.default_state`): `required` runs always (NO decision — auto-on); `optional` REQUIRES a decision here (unanswered → the create 400s); `disabled` never runs (NO decision). Omit this only on a board with no optional gates; otherwise the 400 body\'s `required_gate_decisions` lists exactly which gates to answer — retry with {enabled, note} for each. A decision naming a non-optional gate is rejected 400.'),
|
|
179
184
|
phase_children: z
|
|
180
185
|
.array(z.object({
|
|
181
186
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -183,12 +188,20 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
183
188
|
description: z.string(),
|
|
184
189
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
185
190
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
191
|
+
gate_decisions: z
|
|
192
|
+
.array(z.object({
|
|
193
|
+
gate: z.string().min(1),
|
|
194
|
+
enabled: z.boolean(),
|
|
195
|
+
note: z.string(),
|
|
196
|
+
}))
|
|
197
|
+
.optional()
|
|
198
|
+
.describe("Per-child fail-closed gate decisions — same shape + rule as the root gate_decisions, resolved against THIS child's own type. Required when the child's type has board-optional gates."),
|
|
186
199
|
}))
|
|
187
200
|
.optional(),
|
|
188
201
|
...boardField,
|
|
189
202
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
190
203
|
// ---------------- issue_edit ----------------
|
|
191
|
-
server.tool("issue_edit", 'Patch prose fields
|
|
204
|
+
server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, checklists, effort_level, parent_id, priority, list_id. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. PRIORITY (DX-1532): set card priority via the `priority` key — a tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical", resolved to the tier midpoint) OR a raw number in [0,6). This is the ONLY way to change priority: the numeric `issues.priority` column is what the Trello priority label AND the dashboard badge read — editing a "Priority: <x>" line in the DESCRIPTION changes nothing downstream (a silent false-positive). To honor a "set priority" request, write `priority` here, do NOT edit description prose. CHECKLISTS (DX-1290): a card carries 0..N named checklists, each item ONE 4-state status `incomplete|failing|passing|cancelled` (terminal = passing|cancelled). `ac` is the 2-state CONVENIENCE onto the default "Acceptance Criteria" checklist (checked:true ↔ passing, false ↔ incomplete) — wholesale soft-delete + reinsert of that checklist\'s items. `checklists` is the GENERIC wholesale write path: it REPLACES every named checklist on the card with full 4-state control (each `{name, items:[{label, detail?, status}]}`) — use it to author named checklists like "Feature Tests". Send EITHER `ac` OR `checklists`, NOT both (400). list_id (DX-1192 / DX-1200) PINS the card to a specific board list. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, e.g. a queue name like "⚙️ Fulfillment Queue") — the server resolves a name to its id.** Its type MUST match the card\'s CURRENT derived-status list-type (so to route a ToDo card into a `ready`-type queue, ready it first; mismatch / unknown name or id → 400); pass null to clear the pin (back to default-for-type).', {
|
|
192
205
|
id: z.string().min(1),
|
|
193
206
|
title: z.string().min(1).optional(),
|
|
194
207
|
description: z.string().optional(),
|
|
@@ -210,6 +223,10 @@ server.tool("issue_edit", 'Patch prose fields only via PATCH /api/issues/:id/edi
|
|
|
210
223
|
.optional(),
|
|
211
224
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
212
225
|
parent_id: z.string().nullable().optional(),
|
|
226
|
+
priority: z
|
|
227
|
+
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
228
|
+
.optional()
|
|
229
|
+
.describe('Card priority (DX-1532). A tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical") resolved to the tier midpoint, OR a raw number in [0,6). Writes the numeric `issues.priority` column the Trello label + dashboard badge read — set priority HERE, never via description prose (which no system reads for priority).'),
|
|
213
230
|
list_id: z.string().min(1).nullable().optional(),
|
|
214
231
|
...boardField,
|
|
215
232
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
@@ -290,7 +307,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
|
|
|
290
307
|
...boardField,
|
|
291
308
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
292
309
|
// ---------------- issue_quality_gate ----------------
|
|
293
|
-
server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the SAME write the dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY post-create way to mark a gate required/not-required: `issue_create` carries `
|
|
310
|
+
server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the SAME write the dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY post-create way to mark a gate required/not-required: `issue_create` carries `gate_decisions` at birth, and `issue_edit` REJECTS gate keys (400 offending_keys) — without this tool a card created without a gate can never have it turned on by an agent. `gate` is a registry name: `plan-dependency` | `plan-architecture` | `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality` (the PRE/plan- gates run before the work dispatch; the POST/code- gates block issue_transition complete). Unknown gate → 400 (never a silent no-op); a card with no seeded row for a registered gate → 500 (canonical corruption). NOTE board requirement is TRI-STATE per gate (`board_quality_gate_settings.default_state`, the Agents-tab surface), NOT a binary on/off: `required` = gate always runs (this flag irrelevant); `optional` = gate runs WHEN this per-card flag is true (per-card opt-in — `optional` is ENABLED, NOT off); `disabled` = never runs (this flag inert). So flipping `required:true` here LAUNCHES the gate when the board state is `required` OR `optional`; it is inert ONLY when the board state is `disabled`. Do not read `optional` as off. (Source of truth: `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.) Returns the hydrated issue. Board-scoped; pass `board` (`<repo>:<slug>`) to target another board.", {
|
|
294
311
|
id: z.string().min(1),
|
|
295
312
|
gate: z.enum([
|
|
296
313
|
"plan-dependency",
|
package/dist/priority.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-word → numeric priority resolution for the MCP `issue_edit` tool
|
|
3
|
+
* (DX-1532).
|
|
4
|
+
*
|
|
5
|
+
* Priority is a STRUCTURED numeric column `issues.priority` (float in `[0,6)`).
|
|
6
|
+
* Both the Trello "priority" label and the dashboard priority badge read it via
|
|
7
|
+
* `priorityTier(issue.priority)` — description PROSE ("Priority: Low") is read
|
|
8
|
+
* by neither. So an agent that "sets priority" by editing description text
|
|
9
|
+
* produces a silent false-positive: nothing downstream changes. The fix is to
|
|
10
|
+
* let the agent write the numeric column through `issue_edit`.
|
|
11
|
+
*
|
|
12
|
+
* Agents (and operators) speak in tier WORDS ("low" / "high"), so the tool
|
|
13
|
+
* accepts a tier word and resolves it to the tier's MIDPOINT here — the inverse
|
|
14
|
+
* of `src/issue-tracker/priority-tier.ts#priorityTier`. A raw numeric is passed
|
|
15
|
+
* through unchanged for the server route to range-validate (it 400s on
|
|
16
|
+
* out-of-`[0,6)`), so the MCP boundary never has to duplicate that bound.
|
|
17
|
+
*
|
|
18
|
+
* This package is a standalone published artifact and cannot import the
|
|
19
|
+
* backend's `priority-tier` module, so the midpoint table is duplicated here.
|
|
20
|
+
* Each value IS the `[min,max)` bucket midpoint by construction and equals the
|
|
21
|
+
* backend's `PRIORITY_TIERS.defaultValue`; `src/issue-tracker/priority-tier.test.ts`
|
|
22
|
+
* asserts `priorityTier(defaultValue) === key` on the canonical side, pinning
|
|
23
|
+
* the round-trip these midpoints rely on.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The six priority tier words, low → high. A const tuple so it doubles as the
|
|
27
|
+
* `z.enum` source in `index.ts`.
|
|
28
|
+
*/
|
|
29
|
+
export const PRIORITY_TIER_WORDS = [
|
|
30
|
+
"lowest",
|
|
31
|
+
"low",
|
|
32
|
+
"medium",
|
|
33
|
+
"high",
|
|
34
|
+
"very_high",
|
|
35
|
+
"critical",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Tier word → bucket midpoint (inverse of `priorityTier`). Lands the numeric
|
|
39
|
+
* column squarely inside the requested tier so a later `priorityTier` read
|
|
40
|
+
* returns the same word.
|
|
41
|
+
*/
|
|
42
|
+
export const PRIORITY_TIER_MIDPOINTS = {
|
|
43
|
+
lowest: 0.5,
|
|
44
|
+
low: 1.5,
|
|
45
|
+
medium: 2.5,
|
|
46
|
+
high: 3.5,
|
|
47
|
+
very_high: 4.5,
|
|
48
|
+
critical: 5.5,
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a priority input — a tier WORD or a raw numeric — to the numeric
|
|
52
|
+
* value the `PATCH /api/issues/:id/edit` route stores in `issues.priority`.
|
|
53
|
+
*
|
|
54
|
+
* - tier word → its bucket midpoint (`PRIORITY_TIER_MIDPOINTS`).
|
|
55
|
+
* - number → passed through unchanged; the server route range-validates it to
|
|
56
|
+
* `[0,6)` and 400s otherwise (the MCP boundary deliberately does not
|
|
57
|
+
* re-validate the bound — single source of truth on the route).
|
|
58
|
+
* - unknown string → throws here at the MCP boundary (no round-trip). The Zod
|
|
59
|
+
* enum on the tool schema already rejects this for tool callers; the explicit
|
|
60
|
+
* throw keeps the helper safe for any direct caller.
|
|
61
|
+
*/
|
|
62
|
+
export function resolvePriority(input) {
|
|
63
|
+
if (typeof input === "number")
|
|
64
|
+
return input;
|
|
65
|
+
const midpoint = PRIORITY_TIER_MIDPOINTS[input];
|
|
66
|
+
if (midpoint === undefined) {
|
|
67
|
+
throw new Error(`issue_edit: unknown priority tier word "${input}" (expected one of ${PRIORITY_TIER_WORDS.join(", ")} or a number in [0,6))`);
|
|
68
|
+
}
|
|
69
|
+
return midpoint;
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/handlers.test.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { DashboardHttpClient } from "./http-client.js";
|
|
3
|
-
import { issueList, issueTransition } from "./handlers.js";
|
|
4
|
-
/**
|
|
5
|
-
* `issue_list` query-param forwarding. A fake `fetch` captures the built
|
|
6
|
-
* URL so each filter is asserted at the wire, through the real client's
|
|
7
|
-
* URL builder (`?repo=` always stamped, extra query merged after).
|
|
8
|
-
*/
|
|
9
|
-
function clientCapturing() {
|
|
10
|
-
const urls = [];
|
|
11
|
-
const fetchImpl = (async (url) => {
|
|
12
|
-
urls.push(url);
|
|
13
|
-
return new Response(JSON.stringify({ issues: [] }), { status: 200 });
|
|
14
|
-
});
|
|
15
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
16
|
-
return { client, urls };
|
|
17
|
-
}
|
|
18
|
-
describe("issueList — query forwarding", () => {
|
|
19
|
-
it("forwards q as the server-side search needle", async () => {
|
|
20
|
-
const { client, urls } = clientCapturing();
|
|
21
|
-
await issueList(client, { q: "retire" });
|
|
22
|
-
expect(urls[0]).toContain("q=retire");
|
|
23
|
-
});
|
|
24
|
-
it("omits q when not provided", async () => {
|
|
25
|
-
const { client, urls } = clientCapturing();
|
|
26
|
-
await issueList(client, { include_closed: true });
|
|
27
|
-
expect(urls[0]).not.toContain("q=");
|
|
28
|
-
expect(urls[0]).toContain("include_closed=true");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe("issueTransition — body forwarding", () => {
|
|
32
|
-
function clientCapturingBody() {
|
|
33
|
-
const bodies = [];
|
|
34
|
-
const fetchImpl = (async (_url, init) => {
|
|
35
|
-
bodies.push(JSON.parse(String(init?.body ?? "{}")));
|
|
36
|
-
return new Response(JSON.stringify({ issue: {} }), { status: 200 });
|
|
37
|
-
});
|
|
38
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
39
|
-
return { client, bodies };
|
|
40
|
-
}
|
|
41
|
-
it("forwards manual: true on pickup (DX-946 operator self-pickup)", async () => {
|
|
42
|
-
const { client, bodies } = clientCapturingBody();
|
|
43
|
-
await issueTransition(client, { id: "DX-1", action: "pickup", manual: true });
|
|
44
|
-
expect(bodies[0]).toEqual({ action: "pickup", manual: true });
|
|
45
|
-
});
|
|
46
|
-
it("omits manual when not provided", async () => {
|
|
47
|
-
const { client, bodies } = clientCapturingBody();
|
|
48
|
-
await issueTransition(client, { id: "DX-1", action: "pickup" });
|
|
49
|
-
expect(bodies[0]).toEqual({ action: "pickup" });
|
|
50
|
-
});
|
|
51
|
-
});
|