Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ ade auth status
ade doctor
ade lanes list --text
ade lanes create "fix-checkout-flow" --parent main
ade lanes create "lin-123" --linear-issue-json '{"id":"...","identifier":"LIN-123","title":"...","projectId":"...","projectSlug":"...","teamId":"...","teamKey":"...","stateId":"...","stateName":"Todo","stateType":"unstarted","priority":2,"priorityLabel":"high","labels":[],"assigneeId":null,"assigneeName":null,"createdAt":"...","updatedAt":"..."}'
ade --role cto linear quick-view --text
ade --role cto linear search-issues --query "auth" --state-type started,unstarted --first 50
ade git commit --lane lane-id
ade git push --lane lane-id
ade git branches --lane lane-id --text
ade git user-identity --lane lane-id --text
ade diff patch --lane lane-id --path src/file.ts --text
ade prs create --lane lane-id --base main --title "Fix checkout flow"
ade prs create --lane lane-id --base main --close-linear-issue-on-merge
ade prs list-open --text
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
ade prs path-to-merge --pr pr-id --model gpt-5.5 --conflict-strategy auto --force-finalize conditional
Expand Down
186 changes: 186 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,52 @@ function createRuntime() {
resolveRunAction: vi.fn(async (runId: string, action: string) => ({ id: runId, status: action })),
cancelRun: vi.fn(async () => {}),
} as any,
linearCredentialService: {
getStatus: vi.fn(() => ({
tokenStored: true,
authMode: "manual",
tokenExpiresAt: null,
refreshTokenStored: false,
oauthConfigured: true,
})),
} as any,
linearIssueTracker: {
getConnectionStatus: vi.fn(async () => ({
connected: true,
viewerId: "user-1",
viewerName: "Arul",
message: null,
})),
getQuickView: vi.fn(async (connection: unknown) => ({
connection,
organization: {
id: "org-1",
name: "ADE",
urlKey: "ade",
logoUrl: null,
gitBranchFormat: null,
createdIssueCount: 12,
roadmapEnabled: true,
customersEnabled: false,
releasesEnabled: false,
},
viewer: {
id: "user-1",
name: "Arul",
displayName: "Arul",
email: "arul@example.com",
avatarUrl: null,
admin: true,
guest: false,
url: null,
},
projects: [],
teams: [],
assignedIssues: [],
recentIssues: [],
fetchedAt: "2026-03-17T19:11:00.000Z",
sdk: { packageName: "@linear/sdk", surfaces: ["viewer", "organization"] },
})),
fetchIssueById: vi.fn(async (issueId: string) => ({
id: issueId,
identifier: "LIN-1",
Expand All @@ -711,6 +756,13 @@ function createRuntime() {
createComment: vi.fn(async () => ({ id: "comment-1" })),
fetchWorkflowStates: vi.fn(async () => [{ id: "state-done", name: "Done" }]),
updateIssueState: vi.fn(async () => {}),
listProjects: vi.fn(async () => [{ id: "proj-1", name: "ADE", slug: "ade", teamName: "ADE", teamKey: "ADE" }]),
listUsers: vi.fn(async () => [{ id: "user-1", name: "Arul", displayName: "Arul" }]),
listWorkflowStates: vi.fn(async () => [{ id: "state-1", name: "Todo", type: "unstarted", teamId: "team-1", teamKey: "ADE" }]),
searchIssues: vi.fn(async (query: any) => ({
issues: [{ id: "issue-1", identifier: "ADE-123", title: "Test", _query: query }],
pageInfo: { hasNextPage: false, endCursor: null },
})),
} as any,
linearSyncService: {
getDashboard: vi.fn(() => ({ enabled: true, running: false, ingressMode: "webhook-first", reconciliationIntervalSec: 60, lastPollAt: null, lastSuccessAt: null, lastError: null, queue: { queued: 1, blocked: 0, failed: 0 }, workflowRuns: { active: 1, waiting: 0 }, recentIssues: [] })),
Expand Down Expand Up @@ -1465,6 +1517,7 @@ describe("adeRpcServer", () => {
"pr_rerun_failed_checks",
"pr_reply_to_review_thread",
"pr_resolve_review_thread",
"getLinearQuickView",
"listLinearWorkflows",
"getLinearRunStatus",
"getLinearSyncDashboard",
Expand Down Expand Up @@ -1523,6 +1576,78 @@ describe("adeRpcServer", () => {
);
});

it("returns the Linear quick view for cto callers", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });

await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "getLinearQuickView", {});

expect((runtime.linearIssueTracker as any).getConnectionStatus).toHaveBeenCalled();
expect((runtime.linearIssueTracker as any).getQuickView).toHaveBeenCalledWith(
expect.objectContaining({
connected: true,
tokenStored: true,
viewerId: "user-1",
}),
);
expect(result.structuredContent).toEqual(
expect.objectContaining({
organization: expect.objectContaining({ name: "ADE" }),
sdk: expect.objectContaining({ packageName: "@linear/sdk" }),
}),
);
});

it("returns the Linear issue picker data for cto callers", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });

await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "getLinearIssuePickerData", {});

expect((runtime.linearIssueTracker as any).listProjects).toHaveBeenCalled();
expect((runtime.linearIssueTracker as any).listUsers).toHaveBeenCalled();
expect((runtime.linearIssueTracker as any).listWorkflowStates).toHaveBeenCalled();
expect(result.structuredContent).toEqual(
expect.objectContaining({
projects: expect.any(Array),
users: expect.any(Array),
states: expect.any(Array),
}),
);
});

it("forwards search filters when calling searchLinearIssues", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });

await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "searchLinearIssues", {
projectId: "proj-1",
stateTypes: ["started", "unstarted"],
query: "auth",
first: 25,
includeArchived: true,
});

expect((runtime.linearIssueTracker as any).searchIssues).toHaveBeenCalledWith(
expect.objectContaining({
projectId: "proj-1",
stateTypes: ["started", "unstarted"],
query: "auth",
first: 25,
includeArchived: true,
}),
);
expect(result.structuredContent).toEqual(
expect.objectContaining({
issues: expect.any(Array),
pageInfo: expect.objectContaining({ hasNextPage: false }),
}),
);
});

it("forwards employeeOverride and laneId when resuming a Linear sync queue item", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
Expand Down Expand Up @@ -3865,6 +3990,7 @@ describe("adeRpcServer", () => {
title: "My PR",
body: "Body text",
draft: true,
closeLinearIssueOnMerge: true,
});
expect(created?.isError).toBeUndefined();
expect(fixture.runtime.prService.createFromLane).toHaveBeenCalledWith({
Expand All @@ -3873,6 +3999,7 @@ describe("adeRpcServer", () => {
title: "My PR",
body: "Body text",
draft: true,
closeLinearIssueOnMerge: true,
});

const drafted = await callTool(handler, "create_pr_from_lane", {
Expand Down Expand Up @@ -4259,6 +4386,65 @@ describe("adeRpcServer", () => {
);
});

it("passes branch and Linear issue data through create_lane", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
const linearIssue = {
id: "issue-1",
identifier: "ADE-123",
title: "Create linked lane",
description: null,
url: "https://linear.app/ade/issue/ADE-123/create-linked-lane",
projectId: "project-1",
projectSlug: "ade",
projectName: "ADE",
teamId: "team-1",
teamKey: "ADE",
teamName: "ADE",
stateId: "state-1",
stateName: "Todo",
stateType: "unstarted",
priority: 2,
priorityLabel: "high",
labels: ["desktop"],
assigneeId: null,
assigneeName: null,
creatorId: null,
creatorName: null,
dueDate: null,
estimate: null,
branchName: "ade-123-create-linked-lane",
createdAt: "2026-05-08T00:00:00.000Z",
updatedAt: "2026-05-08T00:00:00.000Z",
secretToken: "do-not-forward",
};

await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "create_lane", {
name: "new-feature",
baseBranch: "main",
branchName: "ade-123-create-linked-lane",
linearIssue,
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.laneService.create).toHaveBeenCalledWith(
expect.objectContaining({
name: "new-feature",
baseBranch: "main",
branchName: "ade-123-create-linked-lane",
linearIssue: expect.objectContaining({
id: "issue-1",
identifier: "ADE-123",
title: "Create linked lane",
projectId: "project-1",
priorityLabel: "high",
}),
})
);
expect((fixture.runtime.laneService.create as any).mock.calls[0][0].linearIssue).not.toHaveProperty("secretToken");
});

it("routes simulate_integration as a read-only dry-merge", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
Expand Down
Loading
Loading