Files
Peter Schilling 3a00cc7604 Accept team names and IDs, and state names, wherever the CLI takes them
Every command that takes a team now accepts its key, name, or UUID through
one shared resolver (findTeam / resolveTeam / resolveTeams), replacing the
key-only getTeamIdByKey and the ad-hoc uppercasing spread across commands.
One aliased ResolveTeam operation looks up key, name, and (for UUID-shaped
input) id in a single round trip; precedence is key, then id, then name,
applied client-side so a reference that equals one team's key and another
team's name always means the key. Keys stay the canonical downstream form:
filters that matched on team.key still do, with the server's uppercase key,
and callers that need a UUID take it from the same resolved object. An
unknown team now errors with the list of valid keys instead of an empty
result or a raw "Entity not found" from the API.

Only explicit input goes through the resolver. The configured default team
is already a normalized key, and resolving it would add a round trip to
every default-team invocation of the most-used commands for no gain. In
issue create, the interactive substring picker survives only for that
default; an explicit --team that matches nothing errors like everywhere
else.

issue query --state and issue mine --state take a workflow state name or
ID as well as the six type tokens. Names and IDs are resolved within the
queried scope (the team, the teams, or the whole workspace under
--all-teams, where a name matches every team's same-named state), so a
state from another team errors instead of silently matching nothing, and
the error lists the scope's states. Type-only input still sends the same
{ type: { in } } filter with no extra request; a mix of types and names
becomes an or-filter.

The MCP server already describes these parameters as "key, name, or ID"
and "type, name, or ID"; this brings the CLI to parity so an agent does
not need a preliminary team list to translate a name into a key.

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-05 07:19:38 -07:00

683 lines
17 KiB
TypeScript

import { snapshotTest } from "@cliffy/testing"
import { createCommand } from "../../../src/commands/document/document-create.ts"
import { MockLinearServer } from "../../utils/mock_linear_server.ts"
import { commonDenoArgs } from "../../utils/test-helpers.ts"
const PROJECT_UUID = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
// Test help output
await snapshotTest({
name: "Document Create Command - Help Text",
meta: import.meta,
colors: false,
args: ["--help"],
denoArgs: commonDenoArgs,
async fn() {
await createCommand.parse()
},
})
// Test creating a document with inline content (project UUID passes through
// without a resolution request)
await snapshotTest({
name: "Document Create Command - With Inline Content",
meta: import.meta,
colors: false,
args: [
"--title",
"Test Document",
"--content",
"# Hello\n\nWorld",
"--project",
PROJECT_UUID,
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "CreateDocument",
variables: {
input: {
title: "Test Document",
projectId: PROJECT_UUID,
content: "# Hello\n\nWorld",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-new",
slugId: "newd0c12345",
title: "Test Document",
url:
"https://linear.app/test/document/test-document-newd0c12345",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to a project
await snapshotTest({
name: "Document Create Command - Attached To Project",
meta: import.meta,
colors: false,
args: [
"--title",
"Project Spec",
"--project",
"tinycloud-sdk",
"--content",
"# Spec",
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
// Shared project resolver tries name first, then slugId
{
queryName: "GetProjectIdByName",
response: { data: { projects: { nodes: [] } } },
},
{
queryName: "GetProjectIdBySlugId",
variables: { slugId: "tinycloud-sdk" },
response: {
data: {
projects: { nodes: [{ id: "project-uuid-123" }] },
},
},
},
// Mock document create mutation
{
queryName: "CreateDocument",
variables: {
input: {
title: "Project Spec",
projectId: "project-uuid-123",
content: "# Spec",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-proj",
slugId: "projd0c456",
title: "Project Spec",
url: "https://linear.app/test/document/project-spec-projd0c456",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to an issue
await snapshotTest({
name: "Document Create Command - Attached To Issue",
meta: import.meta,
colors: false,
args: [
"--title",
"Investigation",
"--issue",
"tc-123",
"--content",
"# Notes",
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
// Mock issue resolution query (identifier gets uppercased)
{
queryName: "GetIssueForDocumentTarget",
variables: { id: "TC-123" },
response: {
data: {
issue: {
id: "issue-uuid-456",
},
},
},
},
// Mock document create mutation
{
queryName: "CreateDocument",
variables: {
input: {
title: "Investigation",
issueId: "issue-uuid-456",
content: "# Notes",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-issue",
slugId: "issued0c789",
title: "Investigation",
url:
"https://linear.app/test/document/investigation-issued0c789",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to a team by key
await snapshotTest({
name: "Document Create Command - Attached To Team",
meta: import.meta,
colors: false,
args: ["--title", "Team Handbook", "--team", "eng", "--content", "# Rules"],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
// The resolver matches keys case-insensitively server-side
{
queryName: "ResolveTeam",
variables: { reference: "eng" },
response: {
data: {
teams: {
nodes: [{ id: "team-eng-id", key: "ENG", name: "Engineering" }],
},
},
},
},
{
queryName: "CreateDocument",
variables: {
input: {
title: "Team Handbook",
teamId: "team-eng-id",
content: "# Rules",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-team",
slugId: "teamd0c111",
title: "Team Handbook",
url:
"https://linear.app/test/document/team-handbook-teamd0c111",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to a cycle: --team scopes the cycle
// lookup and is one target together with --cycle, not two
await snapshotTest({
name: "Document Create Command - Attached To Cycle With Team Scope",
meta: import.meta,
colors: false,
args: [
"--title",
"Sprint Notes",
"--team",
"ENG",
"--cycle",
"8",
"--content",
"# Sprint",
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "ResolveTeam",
variables: { reference: "ENG" },
response: {
data: {
teams: {
nodes: [{ id: "team-eng-id", key: "ENG", name: "Engineering" }],
},
},
},
},
{
queryName: "GetTeamCyclesForLookup",
variables: { teamId: "team-eng-id" },
response: {
data: {
team: {
key: "ENG",
cyclesEnabled: true,
cycles: {
nodes: [
{
id: "cycle-8-id",
number: 8,
startsAt: "2026-07-27T07:00:00.000Z",
name: "Sprint 8",
},
],
},
activeCycle: null,
},
},
},
},
{
queryName: "CreateDocument",
variables: {
input: {
title: "Sprint Notes",
cycleId: "cycle-8-id",
content: "# Sprint",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-cycle",
slugId: "cycled0c222",
title: "Sprint Notes",
url:
"https://linear.app/test/document/sprint-notes-cycled0c222",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to an initiative by slug
await snapshotTest({
name: "Document Create Command - Attached To Initiative",
meta: import.meta,
colors: false,
args: [
"--title",
"Initiative Brief",
"--initiative",
"dev-experience",
"--content",
"# Brief",
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "ResolveInitiativeBySlug",
variables: { slugId: "dev-experience" },
response: {
data: { initiatives: { nodes: [{ id: "initiative-uuid-1" }] } },
},
},
{
queryName: "CreateDocument",
variables: {
input: {
title: "Initiative Brief",
initiativeId: "initiative-uuid-1",
content: "# Brief",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-init",
slugId: "initd0c333",
title: "Initiative Brief",
url:
"https://linear.app/test/document/initiative-brief-initd0c333",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document attached to a release by name
await snapshotTest({
name: "Document Create Command - Attached To Release",
meta: import.meta,
colors: false,
args: [
"--title",
"Release Notes",
"--release",
"Summer Launch",
"--content",
"# Notes",
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "ResolveReleases",
variables: { input: "Summer Launch", after: null },
response: {
data: {
releases: {
nodes: [
{
id: "release-uuid-1",
name: "Summer Launch",
version: "2026.8",
},
],
pageInfo: { hasNextPage: false, endCursor: null },
},
},
},
},
{
queryName: "CreateDocument",
variables: {
input: {
title: "Release Notes",
releaseId: "release-uuid-1",
content: "# Notes",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-release",
slugId: "reld0c444",
title: "Release Notes",
url: "https://linear.app/test/document/release-notes-reld0c444",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Ambiguous release names must error instead of picking one silently. The
// resolver paginates, so candidates split across pages still all count.
await snapshotTest({
name: "Document Create Command - Ambiguous Release Error",
meta: import.meta,
colors: false,
canFail: true,
args: ["--title", "Release Notes", "--release", "Launch"],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "ResolveReleases",
variables: { input: "Launch", after: null },
response: {
data: {
releases: {
nodes: [
{ id: "release-uuid-1", name: "Launch", version: "1.0" },
],
pageInfo: { hasNextPage: true, endCursor: "cursor-1" },
},
},
},
},
{
queryName: "ResolveReleases",
variables: { input: "Launch", after: "cursor-1" },
response: {
data: {
releases: {
nodes: [
{ id: "release-uuid-2", name: "Launch", version: "2.0" },
],
pageInfo: { hasNextPage: false, endCursor: null },
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test creating a document with icon
await snapshotTest({
name: "Document Create Command - With Icon",
meta: import.meta,
colors: false,
args: [
"--title",
"Design Doc",
"--icon",
"📐",
"--content",
"# Design",
"--project",
PROJECT_UUID,
],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "CreateDocument",
variables: {
input: {
title: "Design Doc",
projectId: PROJECT_UUID,
content: "# Design",
icon: "📐",
},
},
response: {
data: {
documentCreate: {
success: true,
document: {
id: "doc-icon",
slugId: "icond0c000",
title: "Design Doc",
url: "https://linear.app/test/document/design-doc-icond0c000",
},
},
},
},
},
])
try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")
await createCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Test missing title error
await snapshotTest({
name: "Document Create Command - Missing Title Error",
meta: import.meta,
colors: false,
canFail: true,
args: ["--content", "# Content without title"],
denoArgs: commonDenoArgs,
async fn() {
// Set dummy API key so validation logic is reached (not "api_key not set" error)
Deno.env.set("LINEAR_API_KEY", "dummy-key-for-validation-test")
try {
await createCommand.parse()
} finally {
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// The API requires exactly one attachment target; a targetless create must
// fail locally with the flag list instead of relaying a GraphQL error
await snapshotTest({
name: "Document Create Command - Missing Target Error",
meta: import.meta,
colors: false,
canFail: true,
args: ["--title", "No Target", "--content", "# Content"],
denoArgs: commonDenoArgs,
async fn() {
Deno.env.set("LINEAR_API_KEY", "dummy-key-for-validation-test")
try {
await createCommand.parse()
} finally {
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// Two distinct targets must fail before any network work
await snapshotTest({
name: "Document Create Command - Multiple Targets Error",
meta: import.meta,
colors: false,
canFail: true,
args: [
"--title",
"Two Targets",
"--project",
"roadmap",
"--team",
"ENG",
],
denoArgs: commonDenoArgs,
async fn() {
Deno.env.set("LINEAR_API_KEY", "dummy-key-for-validation-test")
try {
await createCommand.parse()
} finally {
Deno.env.delete("LINEAR_API_KEY")
}
},
})
// NOTE: "API Error" test removed - stack traces contain machine-specific paths