Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,8 @@ Stateful Linear GraphQL API emulation with seeded organizations, users, teams, w
- Queries: `viewer`, `organization`, `users`, `user`, `teams`, `team`, `workflowStates`, `workflowState`, `issues`, `issue`, `comments`, `comment`, `issueLabels`, `issueLabel`, `projects`, `project`, `cycles`, `cycle`, `webhooks`, `webhook`, `agentSessions`, `agentSession`
- Mutations: `issueCreate`, `issueUpdate`, `issueDelete`, `issueArchive`, `issueUnarchive`, `commentCreate`, `commentUpdate`, `commentDelete`, `issueLabelCreate`, `issueLabelUpdate`, `issueLabelDelete`, `issueAddLabel`, `issueRemoveLabel`, `webhookCreate`, `webhookDelete`, `agentSessionCreateOnIssue`, `agentSessionCreateOnComment`, `agentSessionUpdate`, `agentActivityCreate`

Issue selections expose both numeric `priority` and Linear's derived `priorityLabel` values: `No priority`, `Urgent`, `High`, `Medium`, and `Low`.

### OAuth

- `GET /oauth/authorize` - authorization endpoint with local user picker
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/docs/linear/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Supported mutations:

Connections use Relay-style cursors with `nodes`, `edges`, and `pageInfo`.

Issue selections expose both numeric `priority` and Linear's derived `priorityLabel` values: `No priority`, `Urgent`, `High`, `Medium`, and `Low`.

## Auth

GraphQL accepts `Authorization: Bearer <token>` or a bare personal API key value. The default seeded token is `lin_test_admin`.
Expand Down
1 change: 1 addition & 0 deletions packages/@emulators/linear/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ npx emulate --service linear

- `POST /graphql` for a focused Linear GraphQL subset.
- Queries for viewer, organization, users, teams, workflow states, issues, comments, labels, projects, cycles, webhooks, and agent sessions.
- Issue selections include numeric `priority` and the derived `priorityLabel`.
- Mutations for issues, comments, labels, webhooks, and basic agent sessions and activities.
- OAuth authorize, token, refresh, revoke, PKCE, client credentials, and app actor tokens.
- Personal API key and OAuth bearer token auth.
Expand Down
68 changes: 67 additions & 1 deletion packages/@emulators/linear/src/__tests__/linear.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { LinearClient } from "@linear/sdk";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Hono, Store, WebhookDispatcher, authMiddleware, type TokenMap } from "@emulators/core";
import { getLinearStore, linearPlugin, seedFromConfig } from "../index.js";
import { getLinearStore, linearPlugin, seedFromConfig, type LinearIssue } from "../index.js";

const base = "http://localhost:4300";

Expand Down Expand Up @@ -62,6 +62,72 @@ describe("Linear emulator", () => {
expect(body.data.issues.nodes[0].comments.nodes[0].body).toContain("seeded");
});

it("returns priority labels for issue queries and mutations", async () => {
seedFromConfig(store, base, {
issues: [
{ team: "ENG", title: "No priority issue", priority: 0 },
{ team: "ENG", title: "Urgent issue", priority: 1 },
{ team: "ENG", title: "High priority issue", priority: 2 },
{ team: "ENG", title: "Medium priority issue", priority: 3 },
{ team: "ENG", title: "Low priority issue", priority: 4 },
],
});

const queryRes = await gql(app, `query { issues { nodes { title priority priorityLabel } } }`);
expect(queryRes.status).toBe(200);
const queryBody = (await queryRes.json()) as any;
expect(queryBody.errors).toBeUndefined();
expect(
Object.fromEntries(
queryBody.data.issues.nodes
.filter((issue: { title: string }) => issue.title.endsWith("issue"))
.map((issue: { title: string; priorityLabel: string }) => [issue.title, issue.priorityLabel]),
),
).toEqual({
"No priority issue": "No priority",
"Urgent issue": "Urgent",
"High priority issue": "High",
"Medium priority issue": "Medium",
"Low priority issue": "Low",
});

const team = getLinearStore(store).teams.findOneBy("key", "ENG")!;
const mutationRes = await gql(
app,
`mutation CreateIssue($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { priority priorityLabel }
}
}`,
{ input: { teamId: team.linear_id, title: "Created without priority" } },
);
expect(mutationRes.status).toBe(200);
const mutationBody = (await mutationRes.json()) as any;
expect(mutationBody.errors).toBeUndefined();
expect(mutationBody.data.issueCreate.issue).toEqual({ priority: 0, priorityLabel: "No priority" });

const linearStore = getLinearStore(store);
const fractionalIssue = linearStore.issues.findOneBy("title", "High priority issue")!;
const outOfRangeIssue = linearStore.issues.findOneBy("title", "Low priority issue")!;
linearStore.issues.update(fractionalIssue.id, { priority: 2.5 as LinearIssue["priority"] });
linearStore.issues.update(outOfRangeIssue.id, { priority: 10 as LinearIssue["priority"] });

const irregularRes = await gql(
app,
`query {
fractional: issue(id: "${fractionalIssue.linear_id}") { priorityLabel }
outOfRange: issue(id: "${outOfRangeIssue.linear_id}") { priorityLabel }
}`,
);
const irregularBody = (await irregularRes.json()) as any;
expect(irregularBody.errors).toBeUndefined();
expect(irregularBody.data).toEqual({
fractional: { priorityLabel: "Medium" },
outOfRange: { priorityLabel: "No priority" },
});
});

it("creates issues and comments that can be read back", async () => {
const team = getLinearStore(store).teams.findOneBy("key", "ENG")!;
const state = getLinearStore(store).workflowStates.findOneBy("name", "Todo")!;
Expand Down
8 changes: 8 additions & 0 deletions packages/@emulators/linear/src/routes/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import type {
} from "../entities.js";
import { dispatchLinearWebhook } from "../webhooks.js";

const PRIORITY_LABELS = ["No priority", "Urgent", "High", "Medium", "Low"] as const;

const schema = buildSchema(`
scalar TeamFilter
scalar PaginationOrderBy
Expand Down Expand Up @@ -243,6 +245,7 @@ const schema = buildSchema(`
title: String!
description: String
priority: Int!
priorityLabel: String!
url: String!
createdAt: String!
updatedAt: String!
Expand Down Expand Up @@ -1354,6 +1357,7 @@ function formatIssue(context: LinearGraphQLContext, issue: LinearIssue) {
title: issue.title,
description: issue.description,
priority: issue.priority,
priorityLabel: priorityLabelFor(issue.priority),
url: issue.url,
createdAt: issue.created_at,
updatedAt: issue.updated_at,
Expand Down Expand Up @@ -1995,6 +1999,10 @@ function normalizePriority(value: unknown): LinearIssuePriority {
return value as LinearIssuePriority;
}

function priorityLabelFor(priority: number): string {
return PRIORITY_LABELS[Math.round(priority)] ?? PRIORITY_LABELS[0];
}

function normalizeSessionState(value: string | undefined | null): LinearAgentSession["state"] | undefined {
if (
value === "pending" ||
Expand Down
3 changes: 3 additions & 0 deletions packages/emulate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Framework adapters:
GitHub API coverage:
Includes repository contents, raw downloads, commit history, commit details, and ref comparisons.

Linear API coverage:
Issue queries and mutations include numeric priority and derived priorityLabel fields.

Webhook signatures:
Stripe webhook secrets produce a Stripe-Signature header for raw-body verification.
`,
Expand Down
2 changes: 2 additions & 0 deletions skills/linear/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ Supported mutations:

Connections use Relay-style cursors with `nodes`, `edges`, and `pageInfo`.

Issue selections expose both numeric `priority` and Linear's derived `priorityLabel` values: `No priority`, `Urgent`, `High`, `Medium`, and `Low`.

## OAuth

- `GET /oauth/authorize` - authorization endpoint with local user picker
Expand Down
Loading