diff --git a/skills/linear-cli/SKILL.md b/skills/linear-cli/SKILL.md index 9de18737..ab33907e 100644 --- a/skills/linear-cli/SKILL.md +++ b/skills/linear-cli/SKILL.md @@ -168,6 +168,7 @@ linear team delete linear team id linear team list linear team members +linear team states ``` ## Reference Documentation diff --git a/skills/linear-cli/references/team.md b/skills/linear-cli/references/team.md index 10117f70..d78eea54 100644 --- a/skills/linear-cli/references/team.md +++ b/skills/linear-cli/references/team.md @@ -23,7 +23,8 @@ Commands: list - List teams id - Print the configured team id autolinks - Configure GitHub repository autolinks for Linear issues with this team prefix - members [teamKey] - List team members + members [teamKey] - List team members + states [teamKey] - List workflow states for a team ``` ## Subcommands @@ -139,3 +140,21 @@ Options: --workspace - Target workspace (uses credentials) -a, --all - Include inactive members ``` + +### states + +> List workflow states for a team + +``` +Usage: linear team states [teamKey] + +Description: + + List workflow states for a team + +Options: + + -h, --help - Show this help. + --workspace - Target workspace (uses credentials) + -j, --json - Output as JSON +``` diff --git a/src/commands/issue/issue-create.ts b/src/commands/issue/issue-create.ts index ec0bcec0..74292383 100644 --- a/src/commands/issue/issue-create.ts +++ b/src/commands/issue/issue-create.ts @@ -19,14 +19,15 @@ import { getProjectsForTeam, getTeamIdByKey, getTeamKey, - getWorkflowStateByNameOrType, getWorkflowStates, isLinearUuid, lookupUserId, resolveMilestoneId, + resolveWorkflowState, searchTeamsByKeySubstring, selectOption, type WorkflowState, + workflowStateNotFoundError, } from "../../utils/linear.ts" import { startWorkOnIssue } from "../../utils/actions.ts" import { @@ -822,16 +823,12 @@ export const createCommand = new Command() ) } let stateId: string | undefined - if (state) { - const workflowState = await getWorkflowStateByNameOrType( - team, - state, - ) + if (state != null) { + const states = await getWorkflowStates(team) + const workflowState = resolveWorkflowState(states, state) if (!workflowState) { - throw new NotFoundError( - "Workflow state", - `'${state}' for team ${team}`, - ) + spinner?.stop() + throw workflowStateNotFoundError(team, state, states) } stateId = workflowState.id } diff --git a/src/commands/issue/issue-update.ts b/src/commands/issue/issue-update.ts index 86a60199..7c4120cf 100644 --- a/src/commands/issue/issue-update.ts +++ b/src/commands/issue/issue-update.ts @@ -10,10 +10,12 @@ import { getIssueProjectId, getProjectIdByName, getTeamIdByKey, - getWorkflowStateByNameOrType, + getWorkflowStates, isLinearUuid, lookupUserId, resolveMilestoneId, + resolveWorkflowState, + workflowStateNotFoundError, } from "../../utils/linear.ts" import { CliError, @@ -160,16 +162,12 @@ export const updateCommand = new Command() } let stateId: string | undefined - if (state) { - const workflowState = await getWorkflowStateByNameOrType( - teamKey, - state, - ) + if (state != null) { + const states = await getWorkflowStates(teamKey) + const workflowState = resolveWorkflowState(states, state) if (!workflowState) { - throw new NotFoundError( - "Workflow state", - `'${state}' for team ${teamKey}`, - ) + spinner?.stop() + throw workflowStateNotFoundError(teamKey, state, states) } stateId = workflowState.id } diff --git a/src/commands/team/team-states.ts b/src/commands/team/team-states.ts new file mode 100644 index 00000000..36afdd61 --- /dev/null +++ b/src/commands/team/team-states.ts @@ -0,0 +1,77 @@ +import { Command } from "@cliffy/command" +import { unicodeWidth } from "@std/cli" +import { getTeamKey, getWorkflowStates } from "../../utils/linear.ts" +import { padDisplay } from "../../utils/display.ts" +import { shouldShowSpinner } from "../../utils/hyperlink.ts" +import { handleError, ValidationError } from "../../utils/errors.ts" + +export const statesCommand = new Command() + .name("states") + .description("List workflow states for a team") + .arguments("[teamKey:string]") + .option("-j, --json", "Output as JSON") + .action(async ({ json }, teamKey?: string) => { + const showSpinner = !json && shouldShowSpinner() + let spinner: { start: () => void; stop: () => void } | null = null + + try { + const resolvedTeamKey = teamKey || getTeamKey() + if (!resolvedTeamKey) { + throw new ValidationError( + "Could not determine team key from directory name", + { suggestion: "Please specify a team key as an argument." }, + ) + } + + if (showSpinner) { + const { Spinner } = await import("@std/cli/unstable-spinner") + spinner = new Spinner() + spinner.start() + } + + const states = await getWorkflowStates(resolvedTeamKey) + + spinner?.stop() + + if (json) { + console.log(JSON.stringify({ nodes: states }, null, 2)) + return + } + + if (states.length === 0) { + console.log("No workflow states found for this team.") + return + } + + // States arrive sorted by position; keep that order (it is meaningful). + const NAME_WIDTH = Math.max( + unicodeWidth("NAME"), + ...states.map((s) => unicodeWidth(s.name)), + ) + const TYPE_WIDTH = Math.max( + unicodeWidth("TYPE"), + ...states.map((s) => unicodeWidth(s.type)), + ) + + console.log( + `%c${padDisplay("NAME", NAME_WIDTH)}%c %c${ + padDisplay("TYPE", TYPE_WIDTH) + }%c`, + "text-decoration: underline", + "text-decoration: none", + "text-decoration: underline", + "text-decoration: none", + ) + + for (const state of states) { + console.log( + `${padDisplay(state.name, NAME_WIDTH)} ${ + padDisplay(state.type, TYPE_WIDTH) + }`, + ) + } + } catch (error) { + spinner?.stop() + handleError(error, "Failed to fetch workflow states") + } + }) diff --git a/src/commands/team/team.ts b/src/commands/team/team.ts index 33ef4f0a..7822d57a 100644 --- a/src/commands/team/team.ts +++ b/src/commands/team/team.ts @@ -4,6 +4,7 @@ import { idCommand } from "./team-id.ts" import { autolinksCommand } from "./team-autolinks.ts" import { membersCommand } from "./team-members.ts" import { listCommand } from "./team-list.ts" +import { statesCommand } from "./team-states.ts" import { createCommand } from "./team-create.ts" import { deleteCommand } from "./team-delete.ts" @@ -18,3 +19,4 @@ export const teamCommand = new Command() .command("id", idCommand) .command("autolinks", autolinksCommand) .command("members", membersCommand) + .command("states", statesCommand) diff --git a/src/utils/linear.ts b/src/utils/linear.ts index e69f73a4..f089d70f 100644 --- a/src/utils/linear.ts +++ b/src/utils/linear.ts @@ -171,25 +171,47 @@ export async function getStartedState( return { id: startedStates[0].id, name: startedStates[0].name } } -export async function getWorkflowStateByNameOrType( - teamKey: string, +/** + * Resolve a workflow state from an already-fetched list by name + * (case-insensitive) or by type. Duplicate types resolve to the first matching + * state in the input order — callers pass the position-sorted list from + * `getWorkflowStates`, so that is the lowest-position state of that type. + */ +export function resolveWorkflowState( + states: readonly WorkflowState[], nameOrType: string, -): Promise<{ id: string; name: string } | undefined> { - const states = await getWorkflowStates(teamKey) - +): WorkflowState | undefined { const nameMatch = states.find( (s) => s.name.toLowerCase() === nameOrType.toLowerCase(), ) if (nameMatch) { - return { id: nameMatch.id, name: nameMatch.name } + return nameMatch } - const typeMatch = states.find((s) => s.type === nameOrType.toLowerCase()) - if (typeMatch) { - return { id: typeMatch.id, name: typeMatch.name } - } + return states.find((s) => s.type === nameOrType.toLowerCase()) +} - return undefined +/** + * Build the error thrown when a requested workflow state can't be resolved for + * a team. Shared by `issue create` and `issue update` so both surface the same + * message and the same list of valid states. + */ +export function workflowStateNotFoundError( + teamKey: string, + requested: string, + states: readonly WorkflowState[], +): NotFoundError { + const suggestion = states.length > 0 + ? `Valid states: ${ + states.map((s) => `${JSON.stringify(s.name)} (${s.type})`).join(", ") + }. Run \`linear team states ${teamKey}\` to list them.` + : `Team ${teamKey} has no workflow states. Run \`linear team states ${teamKey}\`.` + + return new NotFoundError( + "Workflow state", + `'${requested}' for team ${teamKey}`, + { suggestion }, + ) } export async function updateIssueState( diff --git a/test/commands/issue/__snapshots__/issue-create.test.ts.snap b/test/commands/issue/__snapshots__/issue-create.test.ts.snap index 121308d5..d37eceee 100644 --- a/test/commands/issue/__snapshots__/issue-create.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-create.test.ts.snap @@ -84,3 +84,12 @@ https://linear.app/test-team/issue/ENG-890/test-cycle-feature stderr: "" `; + +snapshot[`Issue Create Command - Unknown State Lists Valid States 1`] = ` +stdout: +"" +stderr: +"✗ Failed to create issue: Workflow state not found: 'Nope' for team ENG + Valid states: \\"Todo\\" (unstarted), \\"In Progress\\" (started), \\"Done\\" (completed). Run \`linear team states ENG\` to list them. +" +`; diff --git a/test/commands/issue/__snapshots__/issue-update.test.ts.snap b/test/commands/issue/__snapshots__/issue-update.test.ts.snap index cc88b8b9..c26f2ef1 100644 --- a/test/commands/issue/__snapshots__/issue-update.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-update.test.ts.snap @@ -120,3 +120,12 @@ https://linear.app/test-team/issue/ENG-123/test-issue stderr: "" `; + +snapshot[`Issue Update Command - Unknown State Lists Valid States 1`] = ` +stdout: +"" +stderr: +"✗ Failed to update issue: Workflow state not found: 'Nope' for team ENG + Valid states: \\"Todo\\" (unstarted), \\"In Progress\\" (started), \\"Done\\" (completed). Run \`linear team states ENG\` to list them. +" +`; diff --git a/test/commands/issue/issue-create.test.ts b/test/commands/issue/issue-create.test.ts index fb1bba11..6a7d4654 100644 --- a/test/commands/issue/issue-create.test.ts +++ b/test/commands/issue/issue-create.test.ts @@ -1521,3 +1521,68 @@ Deno.test("Issue Create Command - Interactive Assignee Can Override Config Self await cleanup() } }) + +// Regression test for #210: an unknown --state must surface the valid options +// and point at `linear team states`, not just "not found". +await snapshotTest({ + name: "Issue Create Command - Unknown State Lists Valid States", + meta: import.meta, + colors: false, + args: [ + "--title", + "Fix authentication bug", + "--team", + "ENG", + "--state", + "Nope", + "--no-interactive", + ], + denoArgs: commonDenoArgs, + canFail: true, + async fn() { + const { cleanup } = await setupMockLinearServer([ + { + queryName: "GetTeamIdByKey", + variables: { team: "ENG" }, + response: { data: { teams: { nodes: [{ id: "team-eng-id" }] } } }, + }, + { + queryName: "GetWorkflowStates", + response: { + data: { + team: { + states: { + nodes: [ + { + id: "s-todo", + name: "Todo", + type: "unstarted", + position: 1, + }, + { + id: "s-progress", + name: "In Progress", + type: "started", + position: 2, + }, + { + id: "s-done", + name: "Done", + type: "completed", + position: 3, + }, + ], + }, + }, + }, + }, + }, + ], { LINEAR_TEAM_ID: "ENG" }) + + try { + await createCommand.parse() + } finally { + await cleanup() + } + }, +}) diff --git a/test/commands/issue/issue-update.test.ts b/test/commands/issue/issue-update.test.ts index e1e87bcd..5e29fc3f 100644 --- a/test/commands/issue/issue-update.test.ts +++ b/test/commands/issue/issue-update.test.ts @@ -510,3 +510,60 @@ await snapshotTest({ } }, }) + +// Regression test for #210: an unknown --state must surface the valid options +// and point at `linear team states`, not just "not found". +await snapshotTest({ + name: "Issue Update Command - Unknown State Lists Valid States", + meta: import.meta, + colors: false, + args: ["ENG-123", "--state", "Nope"], + denoArgs: commonDenoArgs, + canFail: true, + async fn() { + const { cleanup } = await setupMockLinearServer([ + { + queryName: "GetTeamIdByKey", + variables: { team: "ENG" }, + response: { data: { teams: { nodes: [{ id: "team-eng-id" }] } } }, + }, + { + queryName: "GetWorkflowStates", + response: { + data: { + team: { + states: { + nodes: [ + { + id: "s-todo", + name: "Todo", + type: "unstarted", + position: 1, + }, + { + id: "s-progress", + name: "In Progress", + type: "started", + position: 2, + }, + { + id: "s-done", + name: "Done", + type: "completed", + position: 3, + }, + ], + }, + }, + }, + }, + }, + ], { LINEAR_TEAM_ID: "ENG" }) + + try { + await updateCommand.parse() + } finally { + await cleanup() + } + }, +}) diff --git a/test/commands/team/__snapshots__/team-states.test.ts.snap b/test/commands/team/__snapshots__/team-states.test.ts.snap new file mode 100644 index 00000000..8c6cfeb8 --- /dev/null +++ b/test/commands/team/__snapshots__/team-states.test.ts.snap @@ -0,0 +1,106 @@ +export const snapshot = {}; + +snapshot[`Team States Command - Help Text 1`] = ` +stdout: +" +Usage: states [teamKey] + +Description: + + List workflow states for a team + +Options: + + -h, --help - Show this help. + -j, --json - Output as JSON + +" +stderr: +"" +`; + +snapshot[`Team States Command - Table 1`] = ` +stdout: +"NAME TYPE +Backlog backlog +Todo unstarted +In Progress started +Done completed +" +stderr: +"" +`; + +snapshot[`Team States Command - JSON 1`] = ` +stdout: +'{ + "nodes": [ + { + "id": "s-backlog", + "name": "Backlog", + "type": "backlog", + "position": 0 + }, + { + "id": "s-todo", + "name": "Todo", + "type": "unstarted", + "position": 1 + }, + { + "id": "s-progress", + "name": "In Progress", + "type": "started", + "position": 2 + }, + { + "id": "s-done", + "name": "Done", + "type": "completed", + "position": 3 + } + ] +} +' +stderr: +"" +`; + +snapshot[`Team States Command - Configured Team Fallback 1`] = ` +stdout: +"NAME TYPE +Backlog backlog +Todo unstarted +In Progress started +Done completed +" +stderr: +"" +`; + +snapshot[`Team States Command - Empty 1`] = ` +stdout: +"No workflow states found for this team. +" +stderr: +"" +`; + +snapshot[`Team States Command - Empty JSON 1`] = ` +stdout: +'{ + "nodes": [] +} +' +stderr: +"" +`; + +snapshot[`Team States Command - No Team Configured 1`] = ` +stdout: +"" +stderr: +"✗ Failed to fetch workflow states: Could not determine team key from directory name + Please specify a team key as an argument. +" +`; diff --git a/test/commands/team/team-states.test.ts b/test/commands/team/team-states.test.ts new file mode 100644 index 00000000..6cd98602 --- /dev/null +++ b/test/commands/team/team-states.test.ts @@ -0,0 +1,201 @@ +import { snapshotTest as cliffySnapshotTest } from "@cliffy/testing" +import { assertEquals } from "@std/assert" +import { statesCommand } from "../../../src/commands/team/team-states.ts" +import { teamCommand } from "../../../src/commands/team/team.ts" +import { MockLinearServer } from "../../utils/mock_linear_server.ts" + +// Common Deno args for permissions +const denoArgs = ["--allow-all", "--quiet"] + +// Deliberately out of position order to prove the command sorts by position. +const UNSORTED_STATES = { + data: { + team: { + states: { + nodes: [ + { id: "s-done", name: "Done", type: "completed", position: 3 }, + { id: "s-backlog", name: "Backlog", type: "backlog", position: 0 }, + { + id: "s-progress", + name: "In Progress", + type: "started", + position: 2, + }, + { id: "s-todo", name: "Todo", type: "unstarted", position: 1 }, + ], + }, + }, + }, +} + +// The states command is registered under `team` — a direct wiring guard so the +// snapshot tests (which drive statesCommand directly) can't mask a missing +// registration. +Deno.test("team states - is registered on the team command", () => { + assertEquals(teamCommand.getCommand("states"), statesCommand) +}) + +// Help text +await cliffySnapshotTest({ + name: "Team States Command - Help Text", + meta: import.meta, + colors: false, + args: ["--help"], + denoArgs, + async fn() { + await statesCommand.parse() + }, +}) + +// Table output for an explicit team key, sorted by position +await cliffySnapshotTest({ + name: "Team States Command - Table", + meta: import.meta, + colors: false, + args: ["ENG"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { queryName: "GetWorkflowStates", response: UNSORTED_STATES }, + ]) + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + await statesCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// JSON output preserves GraphQL field names under the connection's `nodes` +await cliffySnapshotTest({ + name: "Team States Command - JSON", + meta: import.meta, + colors: false, + args: ["ENG", "--json"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { queryName: "GetWorkflowStates", response: UNSORTED_STATES }, + ]) + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + await statesCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// Falls back to the configured team key when the argument is omitted; the mock +// only matches when the resolved key reaches the query. +await cliffySnapshotTest({ + name: "Team States Command - Configured Team Fallback", + meta: import.meta, + colors: false, + args: [], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetWorkflowStates", + variables: { teamKey: "FALLBACK" }, + response: UNSORTED_STATES, + }, + ]) + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + Deno.env.set("LINEAR_TEAM_ID", "FALLBACK") + await statesCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + Deno.env.delete("LINEAR_TEAM_ID") + } + }, +}) + +// Empty workflow: human message +await cliffySnapshotTest({ + name: "Team States Command - Empty", + meta: import.meta, + colors: false, + args: ["ENG"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetWorkflowStates", + response: { data: { team: { states: { nodes: [] } } } }, + }, + ]) + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + await statesCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// Empty workflow: JSON still emits the connection shape +await cliffySnapshotTest({ + name: "Team States Command - Empty JSON", + meta: import.meta, + colors: false, + args: ["ENG", "--json"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetWorkflowStates", + response: { data: { team: { states: { nodes: [] } } } }, + }, + ]) + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + await statesCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// No team key argument and no configured team → actionable validation error. +await cliffySnapshotTest({ + name: "Team States Command - No Team Configured", + meta: import.meta, + colors: false, + args: [], + denoArgs, + canFail: true, + async fn() { + // Empty team id is falsy, so getTeamKey() resolves to undefined even though + // the repo's .linear.toml sets one. + Deno.env.set("LINEAR_TEAM_ID", "") + try { + await statesCommand.parse() + } finally { + Deno.env.delete("LINEAR_TEAM_ID") + } + }, +}) diff --git a/test/utils/linear.test.ts b/test/utils/linear.test.ts index 09932c00..d320ca48 100644 --- a/test/utils/linear.test.ts +++ b/test/utils/linear.test.ts @@ -4,7 +4,10 @@ import { isLinearUuid, resolveMilestoneId, resolveProjectId, + resolveWorkflowState, searchIssuesByTerm, + type WorkflowState, + workflowStateNotFoundError, } from "../../src/utils/linear.ts" import { NotFoundError, ValidationError } from "../../src/utils/errors.ts" import { setupMockLinearServer } from "../utils/test-helpers.ts" @@ -296,3 +299,73 @@ Deno.test("resolveMilestoneId - errors when a name is passed without a project", await cleanup() } }) + +// States are passed to resolveWorkflowState already sorted by position, mirroring +// getWorkflowStates. Duplicate "started" states are ordered so the lower position +// comes first. +const WORKFLOW_STATES: WorkflowState[] = [ + { id: "s-backlog", name: "Backlog", type: "backlog", position: 0 }, + { id: "s-todo", name: "Todo", type: "unstarted", position: 1 }, + { id: "s-progress", name: "In Progress", type: "started", position: 2 }, + { id: "s-review", name: "In Review", type: "started", position: 3 }, + { id: "s-done", name: "Done", type: "completed", position: 4 }, +] + +Deno.test("resolveWorkflowState - matches by exact name, case-insensitively", () => { + assertEquals( + resolveWorkflowState(WORKFLOW_STATES, "in progress")?.id, + "s-progress", + ) +}) + +Deno.test("resolveWorkflowState - name match wins over type match", () => { + // "Done" is a name and "completed" is its type; the name should resolve first. + assertEquals(resolveWorkflowState(WORKFLOW_STATES, "Done")?.id, "s-done") +}) + +Deno.test("resolveWorkflowState - matches by type when no name matches", () => { + assertEquals( + resolveWorkflowState(WORKFLOW_STATES, "COMPLETED")?.id, + "s-done", + ) +}) + +Deno.test("resolveWorkflowState - duplicate types resolve to the first by position", () => { + assertEquals( + resolveWorkflowState(WORKFLOW_STATES, "started")?.id, + "s-progress", + ) +}) + +Deno.test("resolveWorkflowState - returns undefined when nothing matches", () => { + assertEquals(resolveWorkflowState(WORKFLOW_STATES, "nope"), undefined) +}) + +Deno.test("workflowStateNotFoundError - lists valid states and the discovery command", () => { + const error = workflowStateNotFoundError("ENG", "nope", [ + { id: "s-backlog", name: "Backlog", type: "backlog", position: 0 }, + { id: "s-todo", name: "Todo", type: "unstarted", position: 1 }, + ]) + assertEquals(error instanceof NotFoundError, true) + assertEquals(error.message, "Workflow state not found: 'nope' for team ENG") + assertEquals( + error.suggestion, + 'Valid states: "Backlog" (backlog), "Todo" (unstarted). ' + + "Run `linear team states ENG` to list them.", + ) +}) + +Deno.test("workflowStateNotFoundError - escapes quotes in state names", () => { + const error = workflowStateNotFoundError("ENG", "nope", [ + { id: "s-weird", name: 'Needs "review"', type: "started", position: 0 }, + ]) + assertStringIncludes(error.suggestion ?? "", '"Needs \\"review\\"" (started)') +}) + +Deno.test("workflowStateNotFoundError - handles a team with no states", () => { + const error = workflowStateNotFoundError("ENG", "nope", []) + assertEquals( + error.suggestion, + "Team ENG has no workflow states. Run `linear team states ENG`.", + ) +})