feat: add label commands (task 0015) #15
@@ -20,6 +20,7 @@ The label command group: `label list`, `label create`, `label edit`, `label dele
|
|||||||
- [x] `label edit`/`label delete` on an unknown name yield `VALIDATION_ERROR` (exit 2)
|
- [x] `label edit`/`label delete` on an unknown name yield `VALIDATION_ERROR` (exit 2)
|
||||||
- [x] `label delete <name>` outputs `delete: ok` + `label: <name>`
|
- [x] `label delete <name>` outputs `delete: ok` + `label: <name>`
|
||||||
- [x] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals
|
- [x] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals
|
||||||
|
- [x] End-to-end tests exercise the live-Gitea semantics the fixture server cannot attest to: the `#`-prefixed color round-trip on create, idempotency against the live listing, and name→id resolution behind edit/delete (plus the not-idempotent delete refusal)
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
@@ -38,3 +39,14 @@ Added `renderObject(item, help)` to `src/render.ts` because the label create/edi
|
|||||||
- **Shared lookup + positional helpers (cleanups from review).**
|
- **Shared lookup + positional helpers (cleanups from review).**
|
||||||
Extracted `findLabel`/`resolveLabel` and a shared `labelNotFound` message into `src/lookup.ts`, reused by the existing `resolveLabelIds`; and extracted `parseSinglePositional` in `src/flags.ts`, now shared by `parsePositionalNumber` and the label name positionals, removing the duplicated count-check/error scaffolding.
|
Extracted `findLabel`/`resolveLabel` and a shared `labelNotFound` message into `src/lookup.ts`, reused by the existing `resolveLabelIds`; and extracted `parseSinglePositional` in `src/flags.ts`, now shared by `parsePositionalNumber` and the label name positionals, removing the duplicated count-check/error scaffolding.
|
||||||
- **`label list` uses a single-page fetch with `--limit` (default 500)**, reading `X-Total-Count` for the count line, rather than the exhaustive pagination `resolveLabel`/`resolveLabelIds` use; the spec asks only for `--limit`, and a repo with >500 labels is signalled by the `count: N of T total` line.
|
- **`label list` uses a single-page fetch with `--limit` (default 500)**, reading `X-Total-Count` for the count line, rather than the exhaustive pagination `resolveLabel`/`resolveLabelIds` use; the spec asks only for `--limit`, and a repo with >500 labels is signalled by the `count: N of T total` line.
|
||||||
|
|
||||||
|
### Follow-ups added after review
|
||||||
|
|
||||||
|
- **End-to-end tier extended.**
|
||||||
|
The task originally scoped its tests to the fixture-server tier only, matching the precedent of the preceding PR-command tasks (0011–0014), none of which added e2e cases.
|
||||||
|
On reflection the label commands sit squarely inside the e2e tier's charter — "behavior the fixture server cannot attest to" — because a fixture server never enforces that Gitea's `CreateLabelOption.color` requires the leading `#`, nor that edit/delete really key on the numeric label id.
|
||||||
|
Added a `test/e2e/mutations.test.ts` label-lifecycle case (create → idempotent re-create → edit → delete, verified against live state via a new `fetchLabels` provisioner helper) plus the not-idempotent delete refusal.
|
||||||
|
These run only in CI (gated on `GITEA_AXI_E2E_URL`).
|
||||||
|
- **Unit-tier coverage backfill.**
|
||||||
|
The initial fixture tests covered only the happy paths and unknown-name refusals, leaving the help output, validation errors, and API-error propagation untested — enough to drop the repo below its global branch-coverage gate.
|
||||||
|
Added confirming fixture tests for those behaviors, bringing `src/commands/label.ts` to ~96% line / ~86% branch and the suite back over its thresholds.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { runCliTest } from "../harness.js";
|
|||||||
import {
|
import {
|
||||||
fetchComments,
|
fetchComments,
|
||||||
fetchIssue,
|
fetchIssue,
|
||||||
|
fetchLabels,
|
||||||
fetchOpenPulls,
|
fetchOpenPulls,
|
||||||
provisionInstance,
|
provisionInstance,
|
||||||
seedBranch,
|
seedBranch,
|
||||||
@@ -216,3 +217,88 @@ describe.skipIf(!E2E_URL)("end-to-end: pull request mutations", () => {
|
|||||||
expect(comments[0]!.body).toBe("A PR comment from the e2e tier.");
|
expect(comments[0]!.body).toBe("A PR comment from the e2e tier.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!E2E_URL)("end-to-end: label mutations", () => {
|
||||||
|
let instance: E2EInstance;
|
||||||
|
// Unique to this block so it never collides with the seeded instance.labelName.
|
||||||
|
const NAME = "E2E-Lifecycle";
|
||||||
|
|
||||||
|
function env(): Record<string, string> {
|
||||||
|
return envFor(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
instance = await instanceOnce();
|
||||||
|
}, 150_000);
|
||||||
|
|
||||||
|
/** The repo's labels named `name`, matched case-insensitively. */
|
||||||
|
async function labelsNamed(name: string): Promise<Record<string, unknown>[]> {
|
||||||
|
const labels = await fetchLabels(instance);
|
||||||
|
return labels.filter(
|
||||||
|
(label) => String(label.name).toLowerCase() === name.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A color as Gitea returned it, with any leading `#` stripped for comparison. */
|
||||||
|
function normalizedColor(label: Record<string, unknown>): string {
|
||||||
|
return String(label.color).replace(/^#/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
it("creates, re-creates idempotently, edits, and deletes a label against live Gitea", async () => {
|
||||||
|
// 1. Create: the CLI prepends `#` to the color, which live Gitea requires.
|
||||||
|
const created = await runCliTest(
|
||||||
|
["label", "create", "--name", NAME, "--color", "ff0000"],
|
||||||
|
{ env: env() },
|
||||||
|
);
|
||||||
|
expect(created.exitCode).toBe(0);
|
||||||
|
expect(created.stdout).toContain("created: ok");
|
||||||
|
expect(created.stdout).toContain(`label: ${NAME}`);
|
||||||
|
|
||||||
|
const afterCreate = await labelsNamed(NAME);
|
||||||
|
expect(afterCreate).toHaveLength(1);
|
||||||
|
expect(normalizedColor(afterCreate[0]!)).toBe("ff0000");
|
||||||
|
|
||||||
|
// 2. Re-create with different casing: the live listing is checked
|
||||||
|
// case-insensitively, so no second label is made and nothing changes.
|
||||||
|
const recreated = await runCliTest(
|
||||||
|
["label", "create", "--name", NAME.toUpperCase(), "--color", "00ff00"],
|
||||||
|
{ env: env() },
|
||||||
|
);
|
||||||
|
expect(recreated.exitCode).toBe(0);
|
||||||
|
expect(recreated.stdout).toContain("create: already_exists");
|
||||||
|
expect(recreated.stdout).toContain(`label: ${NAME}`);
|
||||||
|
|
||||||
|
const afterRecreate = await labelsNamed(NAME);
|
||||||
|
expect(afterRecreate).toHaveLength(1);
|
||||||
|
expect(normalizedColor(afterRecreate[0]!)).toBe("ff0000");
|
||||||
|
|
||||||
|
// 3. Edit: name→id resolution plus PATCH-by-id, verified live.
|
||||||
|
const edited = await runCliTest(
|
||||||
|
["label", "edit", NAME, "--color", "00ff00"],
|
||||||
|
{ env: env() },
|
||||||
|
);
|
||||||
|
expect(edited.exitCode).toBe(0);
|
||||||
|
expect(edited.stdout).toContain("edit: ok");
|
||||||
|
|
||||||
|
const afterEdit = await labelsNamed(NAME);
|
||||||
|
expect(afterEdit).toHaveLength(1);
|
||||||
|
expect(normalizedColor(afterEdit[0]!)).toBe("00ff00");
|
||||||
|
|
||||||
|
// 4. Delete: name→id resolution plus DELETE-by-id, verified live.
|
||||||
|
const deleted = await runCliTest(["label", "delete", NAME], { env: env() });
|
||||||
|
expect(deleted.exitCode).toBe(0);
|
||||||
|
expect(deleted.stdout).toContain("delete: ok");
|
||||||
|
|
||||||
|
expect(await labelsNamed(NAME)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses to delete a label that does not exist", async () => {
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "delete", "no-such-label-xyz"],
|
||||||
|
{ env: env() },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -250,6 +250,18 @@ export async function fetchOpenPulls(
|
|||||||
return (await res.json()) as Record<string, unknown>[];
|
return (await res.json()) as Record<string, unknown>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch the repository's labels as Gitea returns them, for verifying what a
|
||||||
|
* label mutation wrote against live state rather than the CLI's own echo. */
|
||||||
|
export async function fetchLabels(instance: E2EInstance): Promise<Record<string, unknown>[]> {
|
||||||
|
const res = await apiRequest(
|
||||||
|
instance.baseUrl,
|
||||||
|
"GET",
|
||||||
|
`/repos/${instance.owner}/${instance.repo}/labels`,
|
||||||
|
instance.token,
|
||||||
|
);
|
||||||
|
return (await res.json()) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
/** Fetch a single issue as Gitea returns it, for verifying what a mutation wrote. */
|
/** Fetch a single issue as Gitea returns it, for verifying what a mutation wrote. */
|
||||||
export async function fetchIssue(
|
export async function fetchIssue(
|
||||||
instance: E2EInstance,
|
instance: E2EInstance,
|
||||||
|
|||||||
@@ -52,6 +52,41 @@ describe("label list", () => {
|
|||||||
expect(stdout).toContain("count: 0 of 0 total");
|
expect(stdout).toContain("count: 0 of 0 total");
|
||||||
expect(stdout).toContain("labels[0]: (none)");
|
expect(stdout).toContain("labels[0]: (none)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts a numeric --limit value", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: LABELS_PATH,
|
||||||
|
headers: { "X-Total-Count": "1" },
|
||||||
|
body: [{ id: 11, name: "bug" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "list", "--limit", "5"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("count: 1 of 1 total");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates a 403 from the labels API as a FORBIDDEN error", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: LABELS_PATH,
|
||||||
|
status: 403,
|
||||||
|
body: { message: "forbidden" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "list"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toContain("code: FORBIDDEN");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("label create", () => {
|
describe("label create", () => {
|
||||||
@@ -90,6 +125,48 @@ describe("label create", () => {
|
|||||||
expect(stdout).toContain("label: bug");
|
expect(stdout).toContain("label: bug");
|
||||||
expect(server.requests.some((r) => r.method === "POST")).toBe(false);
|
expect(server.requests.some((r) => r.method === "POST")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes --description through in the POST body", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{ method: "GET", path: LABELS_PATH, body: [] },
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: LABELS_PATH,
|
||||||
|
status: 201,
|
||||||
|
body: { id: 5, name: "bug" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { exitCode } = await runCliTest(
|
||||||
|
["label", "create", "--name", "bug", "--color", "ff0000", "--description", "A bug"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(postedBody(server, LABELS_PATH)).toEqual({
|
||||||
|
name: "bug",
|
||||||
|
color: "#ff0000",
|
||||||
|
description: "A bug",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates a 403 on the create call as a FORBIDDEN error", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{ method: "GET", path: LABELS_PATH, body: [] },
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: LABELS_PATH,
|
||||||
|
status: 403,
|
||||||
|
body: { message: "forbidden" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "create", "--name", "bug", "--color", "ff0000"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toContain("code: FORBIDDEN");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("label edit", () => {
|
describe("label edit", () => {
|
||||||
@@ -145,6 +222,30 @@ describe("label edit", () => {
|
|||||||
server.requests.some((r) => r.method === "PATCH" || r.method === "DELETE"),
|
server.requests.some((r) => r.method === "PATCH" || r.method === "DELETE"),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("PATCHes only the provided field when just --color is given", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{ method: "GET", path: LABELS_PATH, body: [{ id: 11, name: "bug" }] },
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
path: `${LABELS_PATH}/11`,
|
||||||
|
status: 200,
|
||||||
|
body: { id: 11, name: "bug", color: "#00ff00" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "edit", "bug", "--color", "00ff00"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const patch = server.requests.find(
|
||||||
|
(r) => r.method === "PATCH" && r.path === `${LABELS_PATH}/11`,
|
||||||
|
);
|
||||||
|
expect(patch?.body).toEqual({ color: "#00ff00" });
|
||||||
|
expect(stdout).toContain("edit: ok");
|
||||||
|
expect(stdout).toContain("label: bug");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("label delete", () => {
|
describe("label delete", () => {
|
||||||
@@ -181,4 +282,164 @@ describe("label delete", () => {
|
|||||||
server.requests.some((r) => r.method === "PATCH" || r.method === "DELETE"),
|
server.requests.some((r) => r.method === "PATCH" || r.method === "DELETE"),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("propagates a 403 on the delete call as a FORBIDDEN error", async () => {
|
||||||
|
server = await startFixtureServer([
|
||||||
|
{ method: "GET", path: LABELS_PATH, body: [{ id: 11, name: "bug" }] },
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: `${LABELS_PATH}/11`,
|
||||||
|
status: 403,
|
||||||
|
body: { message: "forbidden" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "delete", "bug"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toContain("code: FORBIDDEN");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("label help and dispatch", () => {
|
||||||
|
it("prints group usage when no subcommand is given", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label <command>");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints group usage for label --help", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "--help"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label <command>");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints subcommand usage for label list --help", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "list", "--help"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label list");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints subcommand usage for label create --help", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "create", "--help"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label create");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints subcommand usage for label edit --help", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "edit", "--help"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label edit");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints subcommand usage for label delete --help", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "delete", "--help"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("usage: gitea-axi label delete");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an unknown label subcommand", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "bogus"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("Unknown label command: bogus");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an unexpected positional on label list", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "list", "extra"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("Unexpected argument: extra");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a non-numeric --limit on label list", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "list", "--limit", "abc"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("Invalid --limit value");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires --name on label create", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "create"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("label create requires --name");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires --color on label create", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(
|
||||||
|
["label", "create", "--name", "bug"],
|
||||||
|
{ env: testModeEnv(server.url) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("label create requires --color");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires at least one change flag on label edit", async () => {
|
||||||
|
server = await startFixtureServer([]);
|
||||||
|
const { stdout, exitCode } = await runCliTest(["label", "edit", "bug"], {
|
||||||
|
env: testModeEnv(server.url),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(2);
|
||||||
|
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||||
|
expect(stdout).toContain("label edit requires at least one change");
|
||||||
|
expect(server.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user