@hiroleague/taskmanager 0.0.3 → 0.0.4
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/assets/index-BpzHnKdP.css +1 -0
- package/dist/assets/index-DmNErTAP.js +273 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/hiro-task-manager-cli/SKILL.md +6 -4
- package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +1 -0
- package/skills/hiro-task-manager-cli/reference/releases.md +14 -0
- package/src/cli/commands/query.ts +56 -56
- package/src/cli/commands/releases.ts +22 -0
- package/src/cli/handlers/boards.test.ts +669 -669
- package/src/cli/handlers/cli-wiring.test.ts +38 -1
- package/src/cli/handlers/releases.ts +15 -0
- package/src/cli/handlers/search.test.ts +374 -374
- package/src/cli/handlers/search.ts +17 -17
- package/src/cli/lib/cli-http-errors.test.ts +85 -85
- package/src/cli/lib/write/releases.ts +64 -1
- package/src/cli/lib/write-result.test.ts +3 -0
- package/src/cli/lib/write-result.ts +3 -0
- package/src/cli/lib/writeCommands.breadth.test.ts +143 -0
- package/src/cli/lib/writeCommands.ts +1 -0
- package/src/cli/subprocess.real-stack.test.ts +625 -611
- package/src/cli/subprocess.smoke.test.ts +954 -954
- package/src/client/api/useBoardChangeStream.ts +421 -168
- package/src/client/api/useBoardIndexStream.ts +35 -0
- package/src/client/components/board/BoardStatsChips.tsx +233 -233
- package/src/client/components/board/BoardStatsContext.tsx +41 -41
- package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
- package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
- package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
- package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
- package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
- package/src/client/components/layout/AppShell.tsx +5 -2
- package/src/client/components/layout/NotificationToasts.tsx +38 -1
- package/src/client/components/multi-select.tsx +1206 -1206
- package/src/client/components/routing/BoardPage.tsx +20 -20
- package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
- package/src/client/components/task/TaskCard.tsx +643 -643
- package/src/client/components/ui/badge.tsx +49 -49
- package/src/client/components/ui/button.tsx +65 -65
- package/src/client/components/ui/command.tsx +193 -193
- package/src/client/components/ui/dialog.tsx +163 -163
- package/src/client/components/ui/input-group.tsx +155 -155
- package/src/client/components/ui/input.tsx +19 -19
- package/src/client/components/ui/popover.tsx +87 -87
- package/src/client/components/ui/separator.tsx +28 -28
- package/src/client/components/ui/textarea.tsx +18 -18
- package/src/client/index.css +248 -248
- package/src/client/lib/appNavigate.ts +16 -16
- package/src/client/lib/taskCardDate.ts +111 -111
- package/src/client/lib/utils.ts +6 -6
- package/src/client/store/notificationUi.ts +14 -0
- package/src/server/auth.ts +351 -351
- package/src/server/events.ts +31 -4
- package/src/server/migrations/registry.ts +43 -43
- package/src/server/notificationEvents.ts +8 -1
- package/src/server/routes/boards.ts +15 -1
- package/src/server/routes/trash.ts +6 -1
- package/src/shared/boardEvents.ts +6 -0
- package/src/shared/runtimeConfig.ts +256 -256
- package/dist/assets/index-hMFTu7sr.css +0 -1
- package/dist/assets/index-oKG1C41_.js +0 -273
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { runSearch } from "../lib/read/search";
|
|
2
|
-
import type { CliContext } from "./context";
|
|
3
|
-
|
|
4
|
-
export async function handleSearch(
|
|
5
|
-
ctx: CliContext,
|
|
6
|
-
queryParts: string[],
|
|
7
|
-
options: {
|
|
8
|
-
board?: string;
|
|
9
|
-
limit?: string;
|
|
10
|
-
offset?: string;
|
|
11
|
-
noPrefix?: boolean;
|
|
12
|
-
pageAll?: boolean;
|
|
13
|
-
fields?: string;
|
|
14
|
-
},
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
await runSearch(ctx, queryParts, options);
|
|
17
|
-
}
|
|
1
|
+
import { runSearch } from "../lib/read/search";
|
|
2
|
+
import type { CliContext } from "./context";
|
|
3
|
+
|
|
4
|
+
export async function handleSearch(
|
|
5
|
+
ctx: CliContext,
|
|
6
|
+
queryParts: string[],
|
|
7
|
+
options: {
|
|
8
|
+
board?: string;
|
|
9
|
+
limit?: string;
|
|
10
|
+
offset?: string;
|
|
11
|
+
noPrefix?: boolean;
|
|
12
|
+
pageAll?: boolean;
|
|
13
|
+
fields?: string;
|
|
14
|
+
},
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
await runSearch(ctx, queryParts, options);
|
|
17
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { CLI_ERR } from "../types/errors";
|
|
3
|
-
import { enrichNotFoundError, mapHttpStatusToCliFailure } from "./cli-http-errors";
|
|
4
|
-
import { CliError } from "./output";
|
|
5
|
-
|
|
6
|
-
describe("mapHttpStatusToCliFailure", () => {
|
|
7
|
-
test.each([
|
|
8
|
-
[400, CLI_ERR.badRequest, 9, undefined],
|
|
9
|
-
[401, CLI_ERR.unauthenticated, 10, undefined],
|
|
10
|
-
[403, CLI_ERR.forbidden, 4, undefined],
|
|
11
|
-
[404, CLI_ERR.notFound, 3, undefined],
|
|
12
|
-
[408, CLI_ERR.requestTimeout, 7, true],
|
|
13
|
-
[409, CLI_ERR.conflict, 5, undefined],
|
|
14
|
-
[422, CLI_ERR.badRequest, 9, undefined],
|
|
15
|
-
[426, CLI_ERR.versionMismatch, 8, undefined],
|
|
16
|
-
[429, CLI_ERR.rateLimited, 1, true],
|
|
17
|
-
[502, CLI_ERR.internalError, 1, true],
|
|
18
|
-
[418, CLI_ERR.httpError, 1, undefined],
|
|
19
|
-
] as const)(
|
|
20
|
-
"status %i → code %s exit %i retryable %j",
|
|
21
|
-
(status, code, exit, retryable) => {
|
|
22
|
-
const { exitCode, details } = mapHttpStatusToCliFailure(status, {
|
|
23
|
-
status,
|
|
24
|
-
url: "http://127.0.0.1:1/api/x",
|
|
25
|
-
});
|
|
26
|
-
expect(exitCode).toBe(exit);
|
|
27
|
-
expect(details.code).toBe(code);
|
|
28
|
-
if (retryable === undefined) {
|
|
29
|
-
expect(details.retryable).toBeUndefined();
|
|
30
|
-
} else {
|
|
31
|
-
expect(details.retryable).toBe(retryable);
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
test("599 maps to internal_error (5xx branch)", () => {
|
|
37
|
-
const { exitCode, details } = mapHttpStatusToCliFailure(599, { status: 599 });
|
|
38
|
-
expect(exitCode).toBe(1);
|
|
39
|
-
expect(details.code).toBe(CLI_ERR.internalError);
|
|
40
|
-
expect(details.retryable).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("preserves API code as serverCode, not duplicate top-level code", () => {
|
|
44
|
-
const { details } = mapHttpStatusToCliFailure(400, {
|
|
45
|
-
code: "CUSTOM",
|
|
46
|
-
status: 400,
|
|
47
|
-
});
|
|
48
|
-
expect(details.code).toBe(CLI_ERR.badRequest);
|
|
49
|
-
expect(details.serverCode).toBe("CUSTOM");
|
|
50
|
-
expect((details as Record<string, unknown>).code).toBe(CLI_ERR.badRequest);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("non-string code in body is ignored for serverCode", () => {
|
|
54
|
-
const { details } = mapHttpStatusToCliFailure(404, {
|
|
55
|
-
code: 123,
|
|
56
|
-
status: 404,
|
|
57
|
-
} as Record<string, unknown>);
|
|
58
|
-
expect(details.serverCode).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("enrichNotFoundError", () => {
|
|
63
|
-
test("merges context when details.code is not_found", () => {
|
|
64
|
-
const err = new CliError("Whatever", 3, { code: CLI_ERR.notFound });
|
|
65
|
-
try {
|
|
66
|
-
enrichNotFoundError(err, { board: "my-board", taskId: 9 });
|
|
67
|
-
expect.unreachable();
|
|
68
|
-
} catch (e) {
|
|
69
|
-
expect(e).toBeInstanceOf(CliError);
|
|
70
|
-
const c = e as CliError;
|
|
71
|
-
expect(c.message).toBe("Whatever");
|
|
72
|
-
expect(c.exitCode).toBe(3);
|
|
73
|
-
expect(c.details).toMatchObject({
|
|
74
|
-
code: CLI_ERR.notFound,
|
|
75
|
-
board: "my-board",
|
|
76
|
-
taskId: 9,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("rethrows unchanged when code is not not_found", () => {
|
|
82
|
-
const err = new CliError("Nope", 4, { code: CLI_ERR.forbidden });
|
|
83
|
-
expect(() => enrichNotFoundError(err, { board: "x" })).toThrow(err);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CLI_ERR } from "../types/errors";
|
|
3
|
+
import { enrichNotFoundError, mapHttpStatusToCliFailure } from "./cli-http-errors";
|
|
4
|
+
import { CliError } from "./output";
|
|
5
|
+
|
|
6
|
+
describe("mapHttpStatusToCliFailure", () => {
|
|
7
|
+
test.each([
|
|
8
|
+
[400, CLI_ERR.badRequest, 9, undefined],
|
|
9
|
+
[401, CLI_ERR.unauthenticated, 10, undefined],
|
|
10
|
+
[403, CLI_ERR.forbidden, 4, undefined],
|
|
11
|
+
[404, CLI_ERR.notFound, 3, undefined],
|
|
12
|
+
[408, CLI_ERR.requestTimeout, 7, true],
|
|
13
|
+
[409, CLI_ERR.conflict, 5, undefined],
|
|
14
|
+
[422, CLI_ERR.badRequest, 9, undefined],
|
|
15
|
+
[426, CLI_ERR.versionMismatch, 8, undefined],
|
|
16
|
+
[429, CLI_ERR.rateLimited, 1, true],
|
|
17
|
+
[502, CLI_ERR.internalError, 1, true],
|
|
18
|
+
[418, CLI_ERR.httpError, 1, undefined],
|
|
19
|
+
] as const)(
|
|
20
|
+
"status %i → code %s exit %i retryable %j",
|
|
21
|
+
(status, code, exit, retryable) => {
|
|
22
|
+
const { exitCode, details } = mapHttpStatusToCliFailure(status, {
|
|
23
|
+
status,
|
|
24
|
+
url: "http://127.0.0.1:1/api/x",
|
|
25
|
+
});
|
|
26
|
+
expect(exitCode).toBe(exit);
|
|
27
|
+
expect(details.code).toBe(code);
|
|
28
|
+
if (retryable === undefined) {
|
|
29
|
+
expect(details.retryable).toBeUndefined();
|
|
30
|
+
} else {
|
|
31
|
+
expect(details.retryable).toBe(retryable);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
test("599 maps to internal_error (5xx branch)", () => {
|
|
37
|
+
const { exitCode, details } = mapHttpStatusToCliFailure(599, { status: 599 });
|
|
38
|
+
expect(exitCode).toBe(1);
|
|
39
|
+
expect(details.code).toBe(CLI_ERR.internalError);
|
|
40
|
+
expect(details.retryable).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("preserves API code as serverCode, not duplicate top-level code", () => {
|
|
44
|
+
const { details } = mapHttpStatusToCliFailure(400, {
|
|
45
|
+
code: "CUSTOM",
|
|
46
|
+
status: 400,
|
|
47
|
+
});
|
|
48
|
+
expect(details.code).toBe(CLI_ERR.badRequest);
|
|
49
|
+
expect(details.serverCode).toBe("CUSTOM");
|
|
50
|
+
expect((details as Record<string, unknown>).code).toBe(CLI_ERR.badRequest);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("non-string code in body is ignored for serverCode", () => {
|
|
54
|
+
const { details } = mapHttpStatusToCliFailure(404, {
|
|
55
|
+
code: 123,
|
|
56
|
+
status: 404,
|
|
57
|
+
} as Record<string, unknown>);
|
|
58
|
+
expect(details.serverCode).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("enrichNotFoundError", () => {
|
|
63
|
+
test("merges context when details.code is not_found", () => {
|
|
64
|
+
const err = new CliError("Whatever", 3, { code: CLI_ERR.notFound });
|
|
65
|
+
try {
|
|
66
|
+
enrichNotFoundError(err, { board: "my-board", taskId: 9 });
|
|
67
|
+
expect.unreachable();
|
|
68
|
+
} catch (e) {
|
|
69
|
+
expect(e).toBeInstanceOf(CliError);
|
|
70
|
+
const c = e as CliError;
|
|
71
|
+
expect(c.message).toBe("Whatever");
|
|
72
|
+
expect(c.exitCode).toBe(3);
|
|
73
|
+
expect(c.details).toMatchObject({
|
|
74
|
+
code: CLI_ERR.notFound,
|
|
75
|
+
board: "my-board",
|
|
76
|
+
taskId: 9,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("rethrows unchanged when code is not not_found", () => {
|
|
82
|
+
const err = new CliError("Nope", 4, { code: CLI_ERR.forbidden });
|
|
83
|
+
expect(() => enrichNotFoundError(err, { board: "x" })).toThrow(err);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ReleaseMutationResult,
|
|
4
4
|
} from "../../../shared/mutationResults";
|
|
5
5
|
import type { PaginatedListBody } from "../../../shared/pagination";
|
|
6
|
-
import type { ReleaseDefinition } from "../../../shared/models";
|
|
6
|
+
import type { Board, ReleaseDefinition } from "../../../shared/models";
|
|
7
7
|
import type { CliContext } from "../../types/context";
|
|
8
8
|
import { CLI_ERR } from "../../types/errors";
|
|
9
9
|
import { CLI_DEFAULTS } from "../constants";
|
|
@@ -19,6 +19,7 @@ import { fetchAllPages } from "../paginatedFetch";
|
|
|
19
19
|
import { CliError } from "../output";
|
|
20
20
|
import { assertMutuallyExclusive } from "../validation";
|
|
21
21
|
import {
|
|
22
|
+
compactBoardEntity,
|
|
22
23
|
compactReleaseEntity,
|
|
23
24
|
writeReleaseDelete,
|
|
24
25
|
writeSuccess,
|
|
@@ -306,3 +307,65 @@ export async function runReleasesDelete(
|
|
|
306
307
|
enrichNotFoundError(e, { board: boardId, releaseId: rid });
|
|
307
308
|
}
|
|
308
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Set or clear the board default release (PATCH board `defaultReleaseId`).
|
|
313
|
+
* Validates the release exists on the board before PATCH so errors are clearer than a generic 404.
|
|
314
|
+
*/
|
|
315
|
+
export async function runReleasesSetDefault(
|
|
316
|
+
ctx: CliContext,
|
|
317
|
+
opts: {
|
|
318
|
+
port?: number;
|
|
319
|
+
board: string | undefined;
|
|
320
|
+
releaseId: string | undefined;
|
|
321
|
+
clear: boolean;
|
|
322
|
+
},
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
const boardId = opts.board?.trim();
|
|
325
|
+
if (!boardId) {
|
|
326
|
+
throw new CliError("Missing required option: --board", 2, {
|
|
327
|
+
code: CLI_ERR.missingRequired,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
assertMutuallyExclusive([
|
|
331
|
+
["<release-id>", opts.releaseId, "--clear", opts.clear],
|
|
332
|
+
]);
|
|
333
|
+
const ridRaw = opts.releaseId?.trim();
|
|
334
|
+
if (!opts.clear && (ridRaw === undefined || ridRaw === "")) {
|
|
335
|
+
throw new CliError("Provide <release-id> or use --clear", 2, {
|
|
336
|
+
code: CLI_ERR.missingRequired,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!opts.clear) {
|
|
340
|
+
const rid = parsePositiveInt("releaseId", ridRaw);
|
|
341
|
+
if (rid === undefined) {
|
|
342
|
+
throw new CliError("Invalid release id", 2, {
|
|
343
|
+
code: CLI_ERR.invalidValue,
|
|
344
|
+
releaseId: ridRaw,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const rows = await fetchAllBoardReleases(ctx, opts.port, boardId);
|
|
348
|
+
if (!rows.some((r) => r.releaseId === rid)) {
|
|
349
|
+
throw new CliError("Release not found", 3, {
|
|
350
|
+
code: CLI_ERR.notFound,
|
|
351
|
+
board: boardId,
|
|
352
|
+
releaseId: rid,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const patch: Record<string, unknown> = {
|
|
358
|
+
defaultReleaseId: opts.clear ? null : Number(ridRaw),
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const board = await ctx.fetchApiMutate<Board>(
|
|
363
|
+
`/boards/${encodeURIComponent(boardId)}`,
|
|
364
|
+
{ method: "PATCH", body: patch },
|
|
365
|
+
{ port: opts.port },
|
|
366
|
+
);
|
|
367
|
+
ctx.printJson(writeSuccess(board, compactBoardEntity(board)));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
enrichNotFoundError(e, { board: boardId });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -29,6 +29,7 @@ describe("compact*Entity", () => {
|
|
|
29
29
|
emoji: null,
|
|
30
30
|
createdAt: "c",
|
|
31
31
|
updatedAt: "u",
|
|
32
|
+
defaultReleaseId: null,
|
|
32
33
|
});
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -103,6 +104,7 @@ describe("writeSuccess / trashedEntity / writeTrashMove", () => {
|
|
|
103
104
|
emoji: null,
|
|
104
105
|
createdAt: "c",
|
|
105
106
|
updatedAt: "u",
|
|
107
|
+
defaultReleaseId: null,
|
|
106
108
|
},
|
|
107
109
|
);
|
|
108
110
|
expect(envelope).toEqual({
|
|
@@ -118,6 +120,7 @@ describe("writeSuccess / trashedEntity / writeTrashMove", () => {
|
|
|
118
120
|
emoji: null,
|
|
119
121
|
createdAt: "c",
|
|
120
122
|
updatedAt: "u",
|
|
123
|
+
defaultReleaseId: null,
|
|
121
124
|
},
|
|
122
125
|
});
|
|
123
126
|
});
|
|
@@ -9,6 +9,8 @@ export type WriteBoardEntity = {
|
|
|
9
9
|
emoji: string | null;
|
|
10
10
|
createdAt: string;
|
|
11
11
|
updatedAt: string;
|
|
12
|
+
/** Board default release for new tasks (when set). */
|
|
13
|
+
defaultReleaseId?: number | null;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export type WriteListEntity = {
|
|
@@ -91,6 +93,7 @@ export function compactBoardEntity(board: Board): WriteBoardEntity {
|
|
|
91
93
|
emoji: board.emoji ?? null,
|
|
92
94
|
createdAt: board.createdAt,
|
|
93
95
|
updatedAt: board.updatedAt,
|
|
96
|
+
defaultReleaseId: board.defaultReleaseId ?? null,
|
|
94
97
|
};
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
runListsUpdate,
|
|
26
26
|
runReleasesAdd,
|
|
27
27
|
runReleasesDelete,
|
|
28
|
+
runReleasesSetDefault,
|
|
28
29
|
runReleasesUpdate,
|
|
29
30
|
runTasksAdd,
|
|
30
31
|
runTasksDelete,
|
|
@@ -118,6 +119,48 @@ describe("writeCommands breadth — validation", () => {
|
|
|
118
119
|
});
|
|
119
120
|
});
|
|
120
121
|
|
|
122
|
+
test("runReleasesSetDefault throws without board", async () => {
|
|
123
|
+
await expect(
|
|
124
|
+
runReleasesSetDefault(ctx, {
|
|
125
|
+
port: 1,
|
|
126
|
+
board: undefined,
|
|
127
|
+
releaseId: "1",
|
|
128
|
+
clear: false,
|
|
129
|
+
}),
|
|
130
|
+
).rejects.toMatchObject({
|
|
131
|
+
exitCode: 2,
|
|
132
|
+
details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("runReleasesSetDefault throws without release id and without --clear", async () => {
|
|
137
|
+
await expect(
|
|
138
|
+
runReleasesSetDefault(ctx, {
|
|
139
|
+
port: 1,
|
|
140
|
+
board: "b",
|
|
141
|
+
releaseId: undefined,
|
|
142
|
+
clear: false,
|
|
143
|
+
}),
|
|
144
|
+
).rejects.toMatchObject({
|
|
145
|
+
exitCode: 2,
|
|
146
|
+
details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("runReleasesSetDefault throws when release id and --clear together", async () => {
|
|
151
|
+
await expect(
|
|
152
|
+
runReleasesSetDefault(ctx, {
|
|
153
|
+
port: 1,
|
|
154
|
+
board: "b",
|
|
155
|
+
releaseId: "2",
|
|
156
|
+
clear: true,
|
|
157
|
+
}),
|
|
158
|
+
).rejects.toMatchObject({
|
|
159
|
+
exitCode: 2,
|
|
160
|
+
details: expect.objectContaining({ code: CLI_ERR.mutuallyExclusiveOptions }),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
121
164
|
test("runListsMove throws on multiple placement flags", async () => {
|
|
122
165
|
await expect(
|
|
123
166
|
runListsMove(ctx, {
|
|
@@ -687,6 +730,106 @@ describe("writeCommands breadth — mock fetch happy paths", () => {
|
|
|
687
730
|
});
|
|
688
731
|
});
|
|
689
732
|
|
|
733
|
+
test("runReleasesSetDefault GETs releases then PATCHes board defaultReleaseId", async () => {
|
|
734
|
+
setMockFetch(async (input, init) => {
|
|
735
|
+
const u = reqUrl(input);
|
|
736
|
+
if (
|
|
737
|
+
u.includes("/boards/b/releases") &&
|
|
738
|
+
(init?.method === "GET" || init?.method === undefined)
|
|
739
|
+
) {
|
|
740
|
+
return new Response(
|
|
741
|
+
JSON.stringify({
|
|
742
|
+
items: [
|
|
743
|
+
{
|
|
744
|
+
releaseId: 2,
|
|
745
|
+
name: "v1",
|
|
746
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
total: 1,
|
|
750
|
+
limit: 500,
|
|
751
|
+
offset: 0,
|
|
752
|
+
}),
|
|
753
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (u.includes("/api/boards/b") && !u.includes("/releases") && init?.method === "PATCH") {
|
|
757
|
+
expect(JSON.parse(String(init?.body))).toEqual({ defaultReleaseId: 2 });
|
|
758
|
+
return new Response(
|
|
759
|
+
JSON.stringify({
|
|
760
|
+
boardId: 1,
|
|
761
|
+
slug: "b",
|
|
762
|
+
name: "B",
|
|
763
|
+
emoji: null,
|
|
764
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
765
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
766
|
+
defaultReleaseId: 2,
|
|
767
|
+
releases: [],
|
|
768
|
+
lists: [],
|
|
769
|
+
taskGroups: [],
|
|
770
|
+
taskPriorities: [],
|
|
771
|
+
defaultTaskGroupId: 1,
|
|
772
|
+
autoAssignReleaseOnCreateUi: false,
|
|
773
|
+
autoAssignReleaseOnCreateCli: false,
|
|
774
|
+
} as unknown as Board),
|
|
775
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
throw new Error(`unexpected fetch: ${u} ${init?.method}`);
|
|
779
|
+
});
|
|
780
|
+
const out = await captureStdout(() =>
|
|
781
|
+
runReleasesSetDefault(ctx, {
|
|
782
|
+
port: 22025,
|
|
783
|
+
board: "b",
|
|
784
|
+
releaseId: "2",
|
|
785
|
+
clear: false,
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
expect(JSON.parse(out.trim())).toMatchObject({
|
|
789
|
+
ok: true,
|
|
790
|
+
entity: { type: "board", defaultReleaseId: 2 },
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("runReleasesSetDefault --clear PATCHes defaultReleaseId null", async () => {
|
|
795
|
+
setMockFetch(async (input, init) => {
|
|
796
|
+
expect(reqUrl(input)).toContain("/api/boards/b");
|
|
797
|
+
expect(init?.method).toBe("PATCH");
|
|
798
|
+
expect(JSON.parse(String(init?.body))).toEqual({ defaultReleaseId: null });
|
|
799
|
+
return new Response(
|
|
800
|
+
JSON.stringify({
|
|
801
|
+
boardId: 1,
|
|
802
|
+
slug: "b",
|
|
803
|
+
name: "B",
|
|
804
|
+
emoji: null,
|
|
805
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
806
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
807
|
+
defaultReleaseId: null,
|
|
808
|
+
releases: [],
|
|
809
|
+
lists: [],
|
|
810
|
+
taskGroups: [],
|
|
811
|
+
taskPriorities: [],
|
|
812
|
+
defaultTaskGroupId: 1,
|
|
813
|
+
autoAssignReleaseOnCreateUi: false,
|
|
814
|
+
autoAssignReleaseOnCreateCli: false,
|
|
815
|
+
} as unknown as Board),
|
|
816
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
const out = await captureStdout(() =>
|
|
820
|
+
runReleasesSetDefault(ctx, {
|
|
821
|
+
port: 22026,
|
|
822
|
+
board: "b",
|
|
823
|
+
releaseId: undefined,
|
|
824
|
+
clear: true,
|
|
825
|
+
}),
|
|
826
|
+
);
|
|
827
|
+
expect(JSON.parse(out.trim())).toMatchObject({
|
|
828
|
+
ok: true,
|
|
829
|
+
entity: { type: "board", defaultReleaseId: null },
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
690
833
|
test("mutating fetch sends runtime client name header", async () => {
|
|
691
834
|
setRuntimeCliClientName("Cursor Agent");
|
|
692
835
|
setMockFetch(async (_input, init) => {
|