@supaku/agentfactory-linear 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent-client-project-repo.test.d.ts +2 -0
- package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
- package/dist/src/agent-client-project-repo.test.js +149 -0
- package/dist/src/agent-client.d.ts +11 -0
- package/dist/src/agent-client.d.ts.map +1 -1
- package/dist/src/agent-client.js +30 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/platform-adapter.d.ts +119 -0
- package/dist/src/platform-adapter.d.ts.map +1 -0
- package/dist/src/platform-adapter.js +229 -0
- package/dist/src/platform-adapter.test.d.ts +2 -0
- package/dist/src/platform-adapter.test.d.ts.map +1 -0
- package/dist/src/platform-adapter.test.js +435 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-client-project-repo.test.d.ts","sourceRoot":"","sources":["../../src/agent-client-project-repo.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { LinearAgentClient } from './agent-client.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Create a mock project with configurable external links and description.
|
|
8
|
+
*/
|
|
9
|
+
function mockProject(overrides = {}) {
|
|
10
|
+
const { id = 'project-1', description = null, externalLinks = [], } = overrides;
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
description,
|
|
14
|
+
externalLinks: () => Promise.resolve({ nodes: externalLinks }),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a LinearAgentClient with a mocked LinearClient.
|
|
19
|
+
*/
|
|
20
|
+
function createClientWithProject(project) {
|
|
21
|
+
const mockLinearClient = {
|
|
22
|
+
project: vi.fn().mockResolvedValue(project),
|
|
23
|
+
};
|
|
24
|
+
// Construct client bypassing the constructor by setting private fields directly
|
|
25
|
+
const client = Object.create(LinearAgentClient.prototype);
|
|
26
|
+
Object.defineProperty(client, 'client', { value: mockLinearClient, writable: false });
|
|
27
|
+
Object.defineProperty(client, 'retryConfig', {
|
|
28
|
+
value: { maxRetries: 0, baseDelay: 0, maxDelay: 0 },
|
|
29
|
+
writable: false,
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(client, 'statusCache', { value: new Map(), writable: false });
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tests
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
describe('LinearAgentClient.getProjectRepositoryUrl', () => {
|
|
38
|
+
it('returns URL from project external link with "Repository" label', async () => {
|
|
39
|
+
const project = mockProject({
|
|
40
|
+
externalLinks: [
|
|
41
|
+
{ label: 'Repository', url: 'https://github.com/org/repo' },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
const client = createClientWithProject(project);
|
|
45
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
46
|
+
expect(result).toBe('https://github.com/org/repo');
|
|
47
|
+
});
|
|
48
|
+
it('returns URL from project external link with "GitHub" label (case-insensitive)', async () => {
|
|
49
|
+
const project = mockProject({
|
|
50
|
+
externalLinks: [
|
|
51
|
+
{ label: 'github', url: 'https://github.com/org/another-repo' },
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
const client = createClientWithProject(project);
|
|
55
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
56
|
+
expect(result).toBe('https://github.com/org/another-repo');
|
|
57
|
+
});
|
|
58
|
+
it('returns URL from project external link with "REPOSITORY" label (uppercase)', async () => {
|
|
59
|
+
const project = mockProject({
|
|
60
|
+
externalLinks: [
|
|
61
|
+
{ label: 'REPOSITORY', url: 'https://github.com/org/upper-repo' },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const client = createClientWithProject(project);
|
|
65
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
66
|
+
expect(result).toBe('https://github.com/org/upper-repo');
|
|
67
|
+
});
|
|
68
|
+
it('returns URL from project description fallback', async () => {
|
|
69
|
+
const project = mockProject({
|
|
70
|
+
description: 'This is a project.\nRepository: https://github.com/org/desc-repo\nMore info.',
|
|
71
|
+
externalLinks: [],
|
|
72
|
+
});
|
|
73
|
+
const client = createClientWithProject(project);
|
|
74
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
75
|
+
expect(result).toBe('https://github.com/org/desc-repo');
|
|
76
|
+
});
|
|
77
|
+
it('returns URL from project description with case-insensitive matching', async () => {
|
|
78
|
+
const project = mockProject({
|
|
79
|
+
description: 'repository: github.com/org/lower-desc-repo',
|
|
80
|
+
externalLinks: [],
|
|
81
|
+
});
|
|
82
|
+
const client = createClientWithProject(project);
|
|
83
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
84
|
+
expect(result).toBe('github.com/org/lower-desc-repo');
|
|
85
|
+
});
|
|
86
|
+
it('prefers external link over description when both are present', async () => {
|
|
87
|
+
const project = mockProject({
|
|
88
|
+
description: 'Repository: https://github.com/org/desc-repo',
|
|
89
|
+
externalLinks: [
|
|
90
|
+
{ label: 'Repository', url: 'https://github.com/org/link-repo' },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
const client = createClientWithProject(project);
|
|
94
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
95
|
+
expect(result).toBe('https://github.com/org/link-repo');
|
|
96
|
+
});
|
|
97
|
+
it('returns null when no link or description match', async () => {
|
|
98
|
+
const project = mockProject({
|
|
99
|
+
description: 'This project has no repo URL.',
|
|
100
|
+
externalLinks: [
|
|
101
|
+
{ label: 'Documentation', url: 'https://docs.example.com' },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
const client = createClientWithProject(project);
|
|
105
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it('returns null when project has no links and no description', async () => {
|
|
109
|
+
const project = mockProject({
|
|
110
|
+
description: null,
|
|
111
|
+
externalLinks: [],
|
|
112
|
+
});
|
|
113
|
+
const client = createClientWithProject(project);
|
|
114
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
115
|
+
expect(result).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
it('handles projects with empty external links gracefully', async () => {
|
|
118
|
+
const project = mockProject({
|
|
119
|
+
description: null,
|
|
120
|
+
externalLinks: [],
|
|
121
|
+
});
|
|
122
|
+
const client = createClientWithProject(project);
|
|
123
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
124
|
+
expect(result).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
it('skips links without a label', async () => {
|
|
127
|
+
const project = mockProject({
|
|
128
|
+
externalLinks: [
|
|
129
|
+
{ url: 'https://github.com/org/no-label-repo' },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const client = createClientWithProject(project);
|
|
133
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
134
|
+
expect(result).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
it('finds the matching link among multiple external links', async () => {
|
|
137
|
+
const project = mockProject({
|
|
138
|
+
externalLinks: [
|
|
139
|
+
{ label: 'Documentation', url: 'https://docs.example.com' },
|
|
140
|
+
{ label: 'Figma', url: 'https://figma.com/project' },
|
|
141
|
+
{ label: 'GitHub', url: 'https://github.com/org/found-repo' },
|
|
142
|
+
{ label: 'Slack', url: 'https://slack.com/channel' },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
const client = createClientWithProject(project);
|
|
146
|
+
const result = await client.getProjectRepositoryUrl('project-1');
|
|
147
|
+
expect(result).toBe('https://github.com/org/found-repo');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -176,6 +176,17 @@ export declare class LinearAgentClient {
|
|
|
176
176
|
* @returns Array of sub-issue statuses
|
|
177
177
|
*/
|
|
178
178
|
getSubIssueStatuses(issueIdOrIdentifier: string): Promise<SubIssueStatus[]>;
|
|
179
|
+
/**
|
|
180
|
+
* Get the repository URL associated with a project via its links or description
|
|
181
|
+
*
|
|
182
|
+
* Checks project links for a link with label matching 'Repository' or 'GitHub'
|
|
183
|
+
* (case-insensitive). Falls back to parsing the project description for a
|
|
184
|
+
* "Repository: <url>" pattern.
|
|
185
|
+
*
|
|
186
|
+
* @param projectId - The project ID
|
|
187
|
+
* @returns The repository URL if found, null otherwise
|
|
188
|
+
*/
|
|
189
|
+
getProjectRepositoryUrl(projectId: string): Promise<string | null>;
|
|
179
190
|
/**
|
|
180
191
|
* Get sub-issues with their blocking relations for dependency graph building
|
|
181
192
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent-client.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent-client.ts"],"names":[],"mappings":"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"}
|
package/dist/src/agent-client.js
CHANGED
|
@@ -467,6 +467,36 @@ export class LinearAgentClient {
|
|
|
467
467
|
return results;
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Get the repository URL associated with a project via its links or description
|
|
472
|
+
*
|
|
473
|
+
* Checks project links for a link with label matching 'Repository' or 'GitHub'
|
|
474
|
+
* (case-insensitive). Falls back to parsing the project description for a
|
|
475
|
+
* "Repository: <url>" pattern.
|
|
476
|
+
*
|
|
477
|
+
* @param projectId - The project ID
|
|
478
|
+
* @returns The repository URL if found, null otherwise
|
|
479
|
+
*/
|
|
480
|
+
async getProjectRepositoryUrl(projectId) {
|
|
481
|
+
return this.withRetry(async () => {
|
|
482
|
+
const project = await this.client.project(projectId);
|
|
483
|
+
// Check project external links for a Repository/GitHub link
|
|
484
|
+
const links = await project.externalLinks();
|
|
485
|
+
for (const link of links.nodes) {
|
|
486
|
+
if (link.label && /^(repository|github)$/i.test(link.label)) {
|
|
487
|
+
return link.url;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Fallback: check project description for Repository: pattern
|
|
491
|
+
if (project.description) {
|
|
492
|
+
const match = project.description.match(/Repository:\s*([\S]+)/i);
|
|
493
|
+
if (match) {
|
|
494
|
+
return match[1];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
470
500
|
/**
|
|
471
501
|
* Get sub-issues with their blocking relations for dependency graph building
|
|
472
502
|
*
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ export * from './webhook-types.js';
|
|
|
16
16
|
export { LinearFrontendAdapter } from './frontend-adapter.js';
|
|
17
17
|
export type { AbstractStatus as LinearAbstractStatus, AbstractIssue as LinearAbstractIssue, AbstractComment as LinearAbstractComment, ExternalUrl as LinearExternalUrl, CreateIssueInput as LinearCreateIssueInput, UpdateSessionInput as LinearUpdateSessionInput, } from './frontend-adapter.js';
|
|
18
18
|
export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, defaultDetectWorkTypeFromPrompt, defaultGetPriority, defaultParseAutoTriggerConfig, type DefaultAutoTriggerConfig, type WorkflowContext, } from './defaults/index.js';
|
|
19
|
+
export { LinearPlatformAdapter } from './platform-adapter.js';
|
|
20
|
+
export type { GovernorIssue as LinearGovernorIssue } from './platform-adapter.js';
|
|
19
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.js
CHANGED
|
@@ -20,3 +20,5 @@ export * from './webhook-types.js';
|
|
|
20
20
|
export { LinearFrontendAdapter } from './frontend-adapter.js';
|
|
21
21
|
// Default implementations (prompt templates, work type detection, priority, auto-trigger)
|
|
22
22
|
export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, defaultDetectWorkTypeFromPrompt, defaultGetPriority, defaultParseAutoTriggerConfig, } from './defaults/index.js';
|
|
23
|
+
// Platform adapter (Governor event integration)
|
|
24
|
+
export { LinearPlatformAdapter } from './platform-adapter.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinearPlatformAdapter
|
|
3
|
+
*
|
|
4
|
+
* Extends LinearFrontendAdapter with Governor event integration methods.
|
|
5
|
+
* Structurally satisfies the PlatformAdapter interface from @supaku/agentfactory
|
|
6
|
+
* without an explicit `implements` clause, avoiding a circular package dependency
|
|
7
|
+
* (core depends on linear, so linear cannot import core).
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Normalize Linear webhook payloads into GovernorEvents
|
|
11
|
+
* - Scan Linear projects for non-terminal issues
|
|
12
|
+
* - Convert Linear SDK Issue objects to GovernorIssue
|
|
13
|
+
*/
|
|
14
|
+
import { LinearFrontendAdapter } from './frontend-adapter.js';
|
|
15
|
+
import type { LinearAgentClient } from './agent-client.js';
|
|
16
|
+
/**
|
|
17
|
+
* Minimal issue representation used by the Governor.
|
|
18
|
+
* Structurally identical to GovernorIssue in @supaku/agentfactory.
|
|
19
|
+
*/
|
|
20
|
+
export interface GovernorIssue {
|
|
21
|
+
id: string;
|
|
22
|
+
identifier: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
status: string;
|
|
26
|
+
labels: string[];
|
|
27
|
+
createdAt: number;
|
|
28
|
+
parentId?: string;
|
|
29
|
+
project?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Where an event originated. */
|
|
32
|
+
type EventSource = 'webhook' | 'poll' | 'manual';
|
|
33
|
+
/**
|
|
34
|
+
* Fired when an issue's workflow status changes.
|
|
35
|
+
* Structurally identical to IssueStatusChangedEvent in @supaku/agentfactory.
|
|
36
|
+
*/
|
|
37
|
+
interface IssueStatusChangedEvent {
|
|
38
|
+
type: 'issue-status-changed';
|
|
39
|
+
issueId: string;
|
|
40
|
+
issue: GovernorIssue;
|
|
41
|
+
previousStatus?: string;
|
|
42
|
+
newStatus: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
source: EventSource;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fired when a comment is added to an issue.
|
|
48
|
+
* Structurally identical to CommentAddedEvent in @supaku/agentfactory.
|
|
49
|
+
*/
|
|
50
|
+
interface CommentAddedEvent {
|
|
51
|
+
type: 'comment-added';
|
|
52
|
+
issueId: string;
|
|
53
|
+
issue: GovernorIssue;
|
|
54
|
+
commentId: string;
|
|
55
|
+
commentBody: string;
|
|
56
|
+
userId?: string;
|
|
57
|
+
userName?: string;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
source: EventSource;
|
|
60
|
+
}
|
|
61
|
+
/** Union of events this adapter can produce. */
|
|
62
|
+
type GovernorEvent = IssueStatusChangedEvent | CommentAddedEvent;
|
|
63
|
+
/**
|
|
64
|
+
* Linear platform adapter for the EventDrivenGovernor.
|
|
65
|
+
*
|
|
66
|
+
* Extends LinearFrontendAdapter to inherit all frontend operations
|
|
67
|
+
* (status mapping, issue read/write, agent sessions) and adds
|
|
68
|
+
* Governor-specific methods for webhook normalization, project scanning,
|
|
69
|
+
* and issue conversion.
|
|
70
|
+
*
|
|
71
|
+
* Structurally satisfies PlatformAdapter from @supaku/agentfactory.
|
|
72
|
+
*/
|
|
73
|
+
export declare class LinearPlatformAdapter extends LinearFrontendAdapter {
|
|
74
|
+
/**
|
|
75
|
+
* The underlying Linear client, exposed to subclass methods.
|
|
76
|
+
* Re-declared here because the parent's `client` field is private.
|
|
77
|
+
*/
|
|
78
|
+
private readonly linearAgentClient;
|
|
79
|
+
constructor(client: LinearAgentClient);
|
|
80
|
+
/**
|
|
81
|
+
* Normalize a raw Linear webhook payload into GovernorEvents.
|
|
82
|
+
*
|
|
83
|
+
* Handles two payload types:
|
|
84
|
+
* - Issue updates with state changes -> IssueStatusChangedEvent
|
|
85
|
+
* - Comment creations -> CommentAddedEvent
|
|
86
|
+
*
|
|
87
|
+
* Returns `null` for unrecognized payloads (e.g., label changes,
|
|
88
|
+
* AgentSession events, or other resource types).
|
|
89
|
+
*
|
|
90
|
+
* @param payload - Raw Linear webhook payload
|
|
91
|
+
* @returns Array of GovernorEvents, or null if not relevant
|
|
92
|
+
*/
|
|
93
|
+
normalizeWebhookEvent(payload: unknown): GovernorEvent[] | null;
|
|
94
|
+
/**
|
|
95
|
+
* Scan a Linear project for all non-terminal issues.
|
|
96
|
+
*
|
|
97
|
+
* Queries the Linear API with a filter that excludes terminal statuses
|
|
98
|
+
* (Accepted, Canceled, Duplicate). Each issue is converted to a
|
|
99
|
+
* GovernorIssue for evaluation by the Governor.
|
|
100
|
+
*
|
|
101
|
+
* @param project - Linear project name to scan
|
|
102
|
+
* @returns Array of GovernorIssue for all active issues
|
|
103
|
+
*/
|
|
104
|
+
scanProjectIssues(project: string): Promise<GovernorIssue[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Convert a Linear SDK Issue object to a GovernorIssue.
|
|
107
|
+
*
|
|
108
|
+
* The `native` parameter is typed as `unknown` to satisfy the
|
|
109
|
+
* PlatformAdapter interface. Internally it is cast to the Linear SDK
|
|
110
|
+
* `Issue` type. Callers must ensure they pass a valid Linear Issue.
|
|
111
|
+
*
|
|
112
|
+
* @param native - Linear SDK Issue object
|
|
113
|
+
* @returns GovernorIssue representation
|
|
114
|
+
* @throws Error if the native object is not a valid Linear Issue
|
|
115
|
+
*/
|
|
116
|
+
toGovernorIssue(native: unknown): Promise<GovernorIssue>;
|
|
117
|
+
}
|
|
118
|
+
export {};
|
|
119
|
+
//# sourceMappingURL=platform-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-adapter.d.ts","sourceRoot":"","sources":["../../src/platform-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAY1D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,iCAAiC;AACjC,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEhD;;;GAGG;AACH,UAAU,uBAAuB;IAC/B,IAAI,EAAE,sBAAsB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED;;;GAGG;AACH,UAAU,iBAAiB;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED,gDAAgD;AAChD,KAAK,aAAa,GAAG,uBAAuB,GAAG,iBAAiB,CAAA;AAmJhE;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,SAAQ,qBAAqB;IAC9D;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAmB;gBAEzC,MAAM,EAAE,iBAAiB;IAOrC;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,GAAG,IAAI;IA0D/D;;;;;;;;;OASG;IACG,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAkBlE;;;;;;;;;;OAUG;IACG,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC;CAS/D"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinearPlatformAdapter
|
|
3
|
+
*
|
|
4
|
+
* Extends LinearFrontendAdapter with Governor event integration methods.
|
|
5
|
+
* Structurally satisfies the PlatformAdapter interface from @supaku/agentfactory
|
|
6
|
+
* without an explicit `implements` clause, avoiding a circular package dependency
|
|
7
|
+
* (core depends on linear, so linear cannot import core).
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Normalize Linear webhook payloads into GovernorEvents
|
|
11
|
+
* - Scan Linear projects for non-terminal issues
|
|
12
|
+
* - Convert Linear SDK Issue objects to GovernorIssue
|
|
13
|
+
*/
|
|
14
|
+
import { LinearFrontendAdapter } from './frontend-adapter.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Terminal statuses
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* Statuses that represent terminal states in Linear.
|
|
20
|
+
* Issues in these states are excluded from project scans.
|
|
21
|
+
*/
|
|
22
|
+
const TERMINAL_STATUSES = ['Accepted', 'Canceled', 'Duplicate'];
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Generate an ISO-8601 timestamp for the current moment.
|
|
28
|
+
* Equivalent to `eventTimestamp()` from @supaku/agentfactory.
|
|
29
|
+
*/
|
|
30
|
+
function eventTimestamp() {
|
|
31
|
+
return new Date().toISOString();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a GovernorIssue from Linear webhook issue data.
|
|
35
|
+
* Does not make API calls -- uses only the data present in the webhook payload.
|
|
36
|
+
*/
|
|
37
|
+
function webhookIssueToGovernorIssue(issueData) {
|
|
38
|
+
return {
|
|
39
|
+
id: issueData.id,
|
|
40
|
+
identifier: issueData.identifier,
|
|
41
|
+
title: issueData.title,
|
|
42
|
+
description: issueData.description ?? undefined,
|
|
43
|
+
status: issueData.state?.name ?? 'Backlog',
|
|
44
|
+
labels: issueData.labels?.map((l) => l.name) ?? [],
|
|
45
|
+
createdAt: issueData.createdAt
|
|
46
|
+
? new Date(issueData.createdAt).getTime()
|
|
47
|
+
: Date.now(),
|
|
48
|
+
parentId: issueData.parent?.id,
|
|
49
|
+
project: issueData.project?.name,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Convert a Linear SDK Issue object to a GovernorIssue.
|
|
54
|
+
* Resolves lazy-loaded relations (state, labels, parent, project).
|
|
55
|
+
*/
|
|
56
|
+
async function sdkIssueToGovernorIssue(issue) {
|
|
57
|
+
const state = await issue.state;
|
|
58
|
+
const labels = await issue.labels();
|
|
59
|
+
const parent = await issue.parent;
|
|
60
|
+
const project = await issue.project;
|
|
61
|
+
return {
|
|
62
|
+
id: issue.id,
|
|
63
|
+
identifier: issue.identifier,
|
|
64
|
+
title: issue.title,
|
|
65
|
+
description: issue.description ?? undefined,
|
|
66
|
+
status: state?.name ?? 'Backlog',
|
|
67
|
+
labels: labels.nodes.map((l) => l.name),
|
|
68
|
+
createdAt: issue.createdAt.getTime(),
|
|
69
|
+
parentId: parent?.id,
|
|
70
|
+
project: project?.name,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Type guards for webhook payloads
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Check if a payload looks like a Linear issue webhook.
|
|
78
|
+
*/
|
|
79
|
+
function isIssuePayload(payload) {
|
|
80
|
+
if (typeof payload !== 'object' || payload === null)
|
|
81
|
+
return false;
|
|
82
|
+
const p = payload;
|
|
83
|
+
return (p.type === 'Issue' &&
|
|
84
|
+
typeof p.action === 'string' &&
|
|
85
|
+
typeof p.data === 'object' &&
|
|
86
|
+
p.data !== null);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if a payload looks like a Linear comment webhook.
|
|
90
|
+
*/
|
|
91
|
+
function isCommentPayload(payload) {
|
|
92
|
+
if (typeof payload !== 'object' || payload === null)
|
|
93
|
+
return false;
|
|
94
|
+
const p = payload;
|
|
95
|
+
return (p.type === 'Comment' &&
|
|
96
|
+
typeof p.action === 'string' &&
|
|
97
|
+
typeof p.data === 'object' &&
|
|
98
|
+
p.data !== null);
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// LinearPlatformAdapter
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Linear platform adapter for the EventDrivenGovernor.
|
|
105
|
+
*
|
|
106
|
+
* Extends LinearFrontendAdapter to inherit all frontend operations
|
|
107
|
+
* (status mapping, issue read/write, agent sessions) and adds
|
|
108
|
+
* Governor-specific methods for webhook normalization, project scanning,
|
|
109
|
+
* and issue conversion.
|
|
110
|
+
*
|
|
111
|
+
* Structurally satisfies PlatformAdapter from @supaku/agentfactory.
|
|
112
|
+
*/
|
|
113
|
+
export class LinearPlatformAdapter extends LinearFrontendAdapter {
|
|
114
|
+
/**
|
|
115
|
+
* The underlying Linear client, exposed to subclass methods.
|
|
116
|
+
* Re-declared here because the parent's `client` field is private.
|
|
117
|
+
*/
|
|
118
|
+
linearAgentClient;
|
|
119
|
+
constructor(client) {
|
|
120
|
+
super(client);
|
|
121
|
+
this.linearAgentClient = client;
|
|
122
|
+
}
|
|
123
|
+
// ---- PlatformAdapter methods ----
|
|
124
|
+
/**
|
|
125
|
+
* Normalize a raw Linear webhook payload into GovernorEvents.
|
|
126
|
+
*
|
|
127
|
+
* Handles two payload types:
|
|
128
|
+
* - Issue updates with state changes -> IssueStatusChangedEvent
|
|
129
|
+
* - Comment creations -> CommentAddedEvent
|
|
130
|
+
*
|
|
131
|
+
* Returns `null` for unrecognized payloads (e.g., label changes,
|
|
132
|
+
* AgentSession events, or other resource types).
|
|
133
|
+
*
|
|
134
|
+
* @param payload - Raw Linear webhook payload
|
|
135
|
+
* @returns Array of GovernorEvents, or null if not relevant
|
|
136
|
+
*/
|
|
137
|
+
normalizeWebhookEvent(payload) {
|
|
138
|
+
// Handle Issue update with state change
|
|
139
|
+
if (isIssuePayload(payload)) {
|
|
140
|
+
// Only handle updates (not creates/removes)
|
|
141
|
+
if (payload.action !== 'update')
|
|
142
|
+
return null;
|
|
143
|
+
// Only produce an event if the state actually changed
|
|
144
|
+
const hasStateChange = payload.updatedFrom?.stateId !== undefined;
|
|
145
|
+
if (!hasStateChange)
|
|
146
|
+
return null;
|
|
147
|
+
const issue = webhookIssueToGovernorIssue(payload.data);
|
|
148
|
+
const event = {
|
|
149
|
+
type: 'issue-status-changed',
|
|
150
|
+
issueId: payload.data.id,
|
|
151
|
+
issue,
|
|
152
|
+
newStatus: issue.status,
|
|
153
|
+
// Previous status name is not directly available from stateId alone;
|
|
154
|
+
// we only know the stateId changed. The previous status name would
|
|
155
|
+
// require an API call, so we leave it undefined.
|
|
156
|
+
previousStatus: undefined,
|
|
157
|
+
timestamp: eventTimestamp(),
|
|
158
|
+
source: 'webhook',
|
|
159
|
+
};
|
|
160
|
+
return [event];
|
|
161
|
+
}
|
|
162
|
+
// Handle Comment creation
|
|
163
|
+
if (isCommentPayload(payload)) {
|
|
164
|
+
if (payload.action !== 'create')
|
|
165
|
+
return null;
|
|
166
|
+
const commentData = payload.data;
|
|
167
|
+
const issueData = commentData.issue;
|
|
168
|
+
if (!issueData)
|
|
169
|
+
return null;
|
|
170
|
+
const issue = webhookIssueToGovernorIssue(issueData);
|
|
171
|
+
const event = {
|
|
172
|
+
type: 'comment-added',
|
|
173
|
+
issueId: issueData.id,
|
|
174
|
+
issue,
|
|
175
|
+
commentId: commentData.id,
|
|
176
|
+
commentBody: commentData.body,
|
|
177
|
+
userId: commentData.user?.id,
|
|
178
|
+
userName: commentData.user?.name,
|
|
179
|
+
timestamp: eventTimestamp(),
|
|
180
|
+
source: 'webhook',
|
|
181
|
+
};
|
|
182
|
+
return [event];
|
|
183
|
+
}
|
|
184
|
+
// Unrecognized payload type
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Scan a Linear project for all non-terminal issues.
|
|
189
|
+
*
|
|
190
|
+
* Queries the Linear API with a filter that excludes terminal statuses
|
|
191
|
+
* (Accepted, Canceled, Duplicate). Each issue is converted to a
|
|
192
|
+
* GovernorIssue for evaluation by the Governor.
|
|
193
|
+
*
|
|
194
|
+
* @param project - Linear project name to scan
|
|
195
|
+
* @returns Array of GovernorIssue for all active issues
|
|
196
|
+
*/
|
|
197
|
+
async scanProjectIssues(project) {
|
|
198
|
+
const linearClient = this.linearAgentClient.linearClient;
|
|
199
|
+
const issueConnection = await linearClient.issues({
|
|
200
|
+
filter: {
|
|
201
|
+
project: { name: { eq: project } },
|
|
202
|
+
state: { name: { nin: [...TERMINAL_STATUSES] } },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const issue of issueConnection.nodes) {
|
|
207
|
+
results.push(await sdkIssueToGovernorIssue(issue));
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Convert a Linear SDK Issue object to a GovernorIssue.
|
|
213
|
+
*
|
|
214
|
+
* The `native` parameter is typed as `unknown` to satisfy the
|
|
215
|
+
* PlatformAdapter interface. Internally it is cast to the Linear SDK
|
|
216
|
+
* `Issue` type. Callers must ensure they pass a valid Linear Issue.
|
|
217
|
+
*
|
|
218
|
+
* @param native - Linear SDK Issue object
|
|
219
|
+
* @returns GovernorIssue representation
|
|
220
|
+
* @throws Error if the native object is not a valid Linear Issue
|
|
221
|
+
*/
|
|
222
|
+
async toGovernorIssue(native) {
|
|
223
|
+
const issue = native;
|
|
224
|
+
if (!issue || typeof issue.id !== 'string') {
|
|
225
|
+
throw new Error('LinearPlatformAdapter.toGovernorIssue: expected a Linear SDK Issue object');
|
|
226
|
+
}
|
|
227
|
+
return sdkIssueToGovernorIssue(issue);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-adapter.test.d.ts","sourceRoot":"","sources":["../../src/platform-adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { LinearPlatformAdapter } from './platform-adapter.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Create a mock Linear SDK Issue object (matching the pattern from frontend-adapter.test.ts).
|
|
8
|
+
*/
|
|
9
|
+
function mockLinearIssue(overrides = {}) {
|
|
10
|
+
const { id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'A test issue description', url = 'https://linear.app/team/issue/SUP-100', priority = 2, createdAt = new Date('2025-01-15T10:00:00Z'), stateName = 'Backlog', labels = ['Feature'], parentId = null, projectName = 'MyProject', } = overrides;
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
identifier,
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
url,
|
|
17
|
+
priority,
|
|
18
|
+
createdAt,
|
|
19
|
+
get state() {
|
|
20
|
+
return Promise.resolve(stateName ? { name: stateName } : null);
|
|
21
|
+
},
|
|
22
|
+
labels: () => Promise.resolve({ nodes: labels.map((name) => ({ name })) }),
|
|
23
|
+
get parent() {
|
|
24
|
+
return Promise.resolve(parentId ? { id: parentId } : null);
|
|
25
|
+
},
|
|
26
|
+
get project() {
|
|
27
|
+
return Promise.resolve(projectName ? { name: projectName } : null);
|
|
28
|
+
},
|
|
29
|
+
get team() {
|
|
30
|
+
return Promise.resolve({ id: 'team-1', name: 'Engineering' });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a mock LinearAgentClient for the platform adapter.
|
|
36
|
+
*/
|
|
37
|
+
function createMockClient() {
|
|
38
|
+
const mocks = {
|
|
39
|
+
getIssue: vi.fn(),
|
|
40
|
+
updateIssue: vi.fn(),
|
|
41
|
+
getTeamStatuses: vi.fn(),
|
|
42
|
+
updateIssueStatus: vi.fn(),
|
|
43
|
+
createComment: vi.fn(),
|
|
44
|
+
getIssueComments: vi.fn(),
|
|
45
|
+
createIssue: vi.fn(),
|
|
46
|
+
isParentIssue: vi.fn(),
|
|
47
|
+
isChildIssue: vi.fn(),
|
|
48
|
+
getSubIssues: vi.fn(),
|
|
49
|
+
getIssueRelations: vi.fn(),
|
|
50
|
+
createIssueRelation: vi.fn(),
|
|
51
|
+
createAgentSessionOnIssue: vi.fn(),
|
|
52
|
+
updateAgentSession: vi.fn(),
|
|
53
|
+
createAgentActivity: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
const linearClient = {
|
|
56
|
+
issues: vi.fn(),
|
|
57
|
+
issueLabels: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
const client = {
|
|
60
|
+
...mocks,
|
|
61
|
+
linearClient,
|
|
62
|
+
};
|
|
63
|
+
mocks.linearClientIssues = linearClient.issues;
|
|
64
|
+
return { client, mocks };
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Webhook payload factories
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
function makeIssueUpdatePayload(overrides = {}) {
|
|
70
|
+
const { stateName = 'Started', hasStateChange = true, action = 'update', id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'Issue description', labels = [{ name: 'Feature' }], parentId, projectName = 'MyProject', createdAt = '2025-01-15T10:00:00.000Z', } = overrides;
|
|
71
|
+
return {
|
|
72
|
+
action,
|
|
73
|
+
type: 'Issue',
|
|
74
|
+
data: {
|
|
75
|
+
id,
|
|
76
|
+
identifier,
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
url: `https://linear.app/team/issue/${identifier}`,
|
|
80
|
+
state: { id: 'state-1', name: stateName, type: 'started' },
|
|
81
|
+
labels: labels.map((l) => ({ id: `label-${l.name}`, ...l })),
|
|
82
|
+
parent: parentId ? { id: parentId, identifier: 'SUP-99', title: 'Parent' } : undefined,
|
|
83
|
+
project: projectName ? { id: 'proj-1', name: projectName } : undefined,
|
|
84
|
+
createdAt,
|
|
85
|
+
},
|
|
86
|
+
updatedFrom: hasStateChange ? { stateId: 'old-state-id' } : {},
|
|
87
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function makeCommentPayload(overrides = {}) {
|
|
91
|
+
const { action = 'create', commentId = 'comment-uuid-1', body = 'This is a comment', issueId = 'issue-uuid-1', issueIdentifier = 'SUP-100', issueTitle = 'Test Issue', issueStateName = 'Started', userId = 'user-1', userName = 'Test User', } = overrides;
|
|
92
|
+
return {
|
|
93
|
+
action,
|
|
94
|
+
type: 'Comment',
|
|
95
|
+
data: {
|
|
96
|
+
id: commentId,
|
|
97
|
+
body,
|
|
98
|
+
issue: {
|
|
99
|
+
id: issueId,
|
|
100
|
+
identifier: issueIdentifier,
|
|
101
|
+
title: issueTitle,
|
|
102
|
+
url: `https://linear.app/team/issue/${issueIdentifier}`,
|
|
103
|
+
state: { id: 'state-1', name: issueStateName, type: 'started' },
|
|
104
|
+
labels: [{ id: 'label-1', name: 'Feature' }],
|
|
105
|
+
createdAt: '2025-01-15T10:00:00.000Z',
|
|
106
|
+
},
|
|
107
|
+
user: { id: userId, name: userName },
|
|
108
|
+
},
|
|
109
|
+
createdAt: '2025-01-15T12:00:00.000Z',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Tests
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe('LinearPlatformAdapter', () => {
|
|
116
|
+
let adapter;
|
|
117
|
+
let mocks;
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
const { client, mocks: m } = createMockClient();
|
|
120
|
+
mocks = m;
|
|
121
|
+
adapter = new LinearPlatformAdapter(client);
|
|
122
|
+
});
|
|
123
|
+
// ========================================================================
|
|
124
|
+
// name
|
|
125
|
+
// ========================================================================
|
|
126
|
+
it('has name "linear"', () => {
|
|
127
|
+
expect(adapter.name).toBe('linear');
|
|
128
|
+
});
|
|
129
|
+
// ========================================================================
|
|
130
|
+
// normalizeWebhookEvent — Issue updates
|
|
131
|
+
// ========================================================================
|
|
132
|
+
describe('normalizeWebhookEvent — issue updates', () => {
|
|
133
|
+
it('returns IssueStatusChangedEvent for issue update with state change', () => {
|
|
134
|
+
const payload = makeIssueUpdatePayload({ stateName: 'Started' });
|
|
135
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
136
|
+
expect(events).not.toBeNull();
|
|
137
|
+
expect(events).toHaveLength(1);
|
|
138
|
+
const event = events[0];
|
|
139
|
+
expect(event.type).toBe('issue-status-changed');
|
|
140
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
141
|
+
expect(event.source).toBe('webhook');
|
|
142
|
+
if (event.type === 'issue-status-changed') {
|
|
143
|
+
expect(event.newStatus).toBe('Started');
|
|
144
|
+
expect(event.issue.id).toBe('issue-uuid-1');
|
|
145
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
146
|
+
expect(event.issue.title).toBe('Test Issue');
|
|
147
|
+
expect(event.issue.status).toBe('Started');
|
|
148
|
+
expect(event.issue.labels).toEqual(['Feature']);
|
|
149
|
+
expect(event.issue.project).toBe('MyProject');
|
|
150
|
+
expect(event.timestamp).toBeTruthy();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
it('returns null for issue update without state change', () => {
|
|
154
|
+
const payload = makeIssueUpdatePayload({ hasStateChange: false });
|
|
155
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
156
|
+
expect(events).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
it('returns null for issue create action', () => {
|
|
159
|
+
const payload = makeIssueUpdatePayload({ action: 'create' });
|
|
160
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
161
|
+
expect(events).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
it('returns null for issue remove action', () => {
|
|
164
|
+
const payload = makeIssueUpdatePayload({ action: 'remove' });
|
|
165
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
166
|
+
expect(events).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
it('includes parentId when issue has a parent', () => {
|
|
169
|
+
const payload = makeIssueUpdatePayload({ parentId: 'parent-uuid-1' });
|
|
170
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
171
|
+
expect(events).not.toBeNull();
|
|
172
|
+
const event = events[0];
|
|
173
|
+
if (event.type === 'issue-status-changed') {
|
|
174
|
+
expect(event.issue.parentId).toBe('parent-uuid-1');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
it('handles issue with no description', () => {
|
|
178
|
+
const payload = makeIssueUpdatePayload();
|
|
179
|
+
// Remove description from data
|
|
180
|
+
delete payload.data.description;
|
|
181
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
182
|
+
expect(events).not.toBeNull();
|
|
183
|
+
const event = events[0];
|
|
184
|
+
if (event.type === 'issue-status-changed') {
|
|
185
|
+
expect(event.issue.description).toBeUndefined();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
it('handles issue with no labels', () => {
|
|
189
|
+
const payload = makeIssueUpdatePayload({ labels: [] });
|
|
190
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
191
|
+
expect(events).not.toBeNull();
|
|
192
|
+
const event = events[0];
|
|
193
|
+
if (event.type === 'issue-status-changed') {
|
|
194
|
+
expect(event.issue.labels).toEqual([]);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
it('parses createdAt from webhook data into milliseconds', () => {
|
|
198
|
+
const payload = makeIssueUpdatePayload({ createdAt: '2025-06-01T00:00:00.000Z' });
|
|
199
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
200
|
+
expect(events).not.toBeNull();
|
|
201
|
+
const event = events[0];
|
|
202
|
+
if (event.type === 'issue-status-changed') {
|
|
203
|
+
expect(event.issue.createdAt).toBe(new Date('2025-06-01T00:00:00.000Z').getTime());
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
// ========================================================================
|
|
208
|
+
// normalizeWebhookEvent — Comments
|
|
209
|
+
// ========================================================================
|
|
210
|
+
describe('normalizeWebhookEvent — comments', () => {
|
|
211
|
+
it('returns CommentAddedEvent for comment creation', () => {
|
|
212
|
+
const payload = makeCommentPayload();
|
|
213
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
214
|
+
expect(events).not.toBeNull();
|
|
215
|
+
expect(events).toHaveLength(1);
|
|
216
|
+
const event = events[0];
|
|
217
|
+
expect(event.type).toBe('comment-added');
|
|
218
|
+
expect(event.issueId).toBe('issue-uuid-1');
|
|
219
|
+
expect(event.source).toBe('webhook');
|
|
220
|
+
if (event.type === 'comment-added') {
|
|
221
|
+
expect(event.commentId).toBe('comment-uuid-1');
|
|
222
|
+
expect(event.commentBody).toBe('This is a comment');
|
|
223
|
+
expect(event.userId).toBe('user-1');
|
|
224
|
+
expect(event.userName).toBe('Test User');
|
|
225
|
+
expect(event.issue.identifier).toBe('SUP-100');
|
|
226
|
+
expect(event.issue.status).toBe('Started');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
it('returns null for comment update action', () => {
|
|
230
|
+
const payload = makeCommentPayload({ action: 'update' });
|
|
231
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
232
|
+
expect(events).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
it('returns null for comment remove action', () => {
|
|
235
|
+
const payload = makeCommentPayload({ action: 'remove' });
|
|
236
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
237
|
+
expect(events).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
it('handles comment without user', () => {
|
|
240
|
+
const payload = makeCommentPayload();
|
|
241
|
+
delete payload.data.user;
|
|
242
|
+
const events = adapter.normalizeWebhookEvent(payload);
|
|
243
|
+
expect(events).not.toBeNull();
|
|
244
|
+
const event = events[0];
|
|
245
|
+
if (event.type === 'comment-added') {
|
|
246
|
+
expect(event.userId).toBeUndefined();
|
|
247
|
+
expect(event.userName).toBeUndefined();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ========================================================================
|
|
252
|
+
// normalizeWebhookEvent — Unrecognized payloads
|
|
253
|
+
// ========================================================================
|
|
254
|
+
describe('normalizeWebhookEvent — unrecognized payloads', () => {
|
|
255
|
+
it('returns null for null payload', () => {
|
|
256
|
+
expect(adapter.normalizeWebhookEvent(null)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
it('returns null for undefined payload', () => {
|
|
259
|
+
expect(adapter.normalizeWebhookEvent(undefined)).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
it('returns null for non-object payload', () => {
|
|
262
|
+
expect(adapter.normalizeWebhookEvent('string')).toBeNull();
|
|
263
|
+
expect(adapter.normalizeWebhookEvent(42)).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
it('returns null for AgentSessionEvent payloads', () => {
|
|
266
|
+
const payload = {
|
|
267
|
+
action: 'created',
|
|
268
|
+
type: 'AgentSessionEvent',
|
|
269
|
+
data: { id: 'session-1' },
|
|
270
|
+
};
|
|
271
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
it('returns null for unknown resource types', () => {
|
|
274
|
+
const payload = {
|
|
275
|
+
action: 'update',
|
|
276
|
+
type: 'Project',
|
|
277
|
+
data: { id: 'proj-1' },
|
|
278
|
+
};
|
|
279
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
it('returns null for payload without data', () => {
|
|
282
|
+
const payload = {
|
|
283
|
+
action: 'update',
|
|
284
|
+
type: 'Issue',
|
|
285
|
+
};
|
|
286
|
+
expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// ========================================================================
|
|
290
|
+
// scanProjectIssues
|
|
291
|
+
// ========================================================================
|
|
292
|
+
describe('scanProjectIssues', () => {
|
|
293
|
+
it('returns GovernorIssues for all non-terminal issues', async () => {
|
|
294
|
+
const issues = [
|
|
295
|
+
mockLinearIssue({ id: 'i-1', identifier: 'SUP-1', stateName: 'Backlog' }),
|
|
296
|
+
mockLinearIssue({ id: 'i-2', identifier: 'SUP-2', stateName: 'Started' }),
|
|
297
|
+
mockLinearIssue({ id: 'i-3', identifier: 'SUP-3', stateName: 'Finished' }),
|
|
298
|
+
];
|
|
299
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
|
|
300
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
301
|
+
expect(result).toHaveLength(3);
|
|
302
|
+
expect(result[0].id).toBe('i-1');
|
|
303
|
+
expect(result[0].identifier).toBe('SUP-1');
|
|
304
|
+
expect(result[0].status).toBe('Backlog');
|
|
305
|
+
expect(result[1].id).toBe('i-2');
|
|
306
|
+
expect(result[1].status).toBe('Started');
|
|
307
|
+
expect(result[2].id).toBe('i-3');
|
|
308
|
+
expect(result[2].status).toBe('Finished');
|
|
309
|
+
});
|
|
310
|
+
it('passes correct filter to Linear API', async () => {
|
|
311
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [] });
|
|
312
|
+
await adapter.scanProjectIssues('TestProject');
|
|
313
|
+
expect(mocks.linearClientIssues).toHaveBeenCalledWith({
|
|
314
|
+
filter: {
|
|
315
|
+
project: { name: { eq: 'TestProject' } },
|
|
316
|
+
state: { name: { nin: ['Accepted', 'Canceled', 'Duplicate'] } },
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
it('returns empty array when no issues found', async () => {
|
|
321
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [] });
|
|
322
|
+
const result = await adapter.scanProjectIssues('EmptyProject');
|
|
323
|
+
expect(result).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
it('resolves lazy-loaded issue properties', async () => {
|
|
326
|
+
const issue = mockLinearIssue({
|
|
327
|
+
labels: ['Bug', 'Urgent'],
|
|
328
|
+
parentId: 'parent-1',
|
|
329
|
+
projectName: 'MyProject',
|
|
330
|
+
});
|
|
331
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [issue] });
|
|
332
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
333
|
+
expect(result[0].labels).toEqual(['Bug', 'Urgent']);
|
|
334
|
+
expect(result[0].parentId).toBe('parent-1');
|
|
335
|
+
expect(result[0].project).toBe('MyProject');
|
|
336
|
+
});
|
|
337
|
+
it('converts createdAt Date to epoch milliseconds', async () => {
|
|
338
|
+
const issue = mockLinearIssue({
|
|
339
|
+
createdAt: new Date('2025-03-01T08:00:00Z'),
|
|
340
|
+
});
|
|
341
|
+
mocks.linearClientIssues.mockResolvedValue({ nodes: [issue] });
|
|
342
|
+
const result = await adapter.scanProjectIssues('MyProject');
|
|
343
|
+
expect(result[0].createdAt).toBe(new Date('2025-03-01T08:00:00Z').getTime());
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// ========================================================================
|
|
347
|
+
// toGovernorIssue
|
|
348
|
+
// ========================================================================
|
|
349
|
+
describe('toGovernorIssue', () => {
|
|
350
|
+
it('converts a Linear SDK Issue to GovernorIssue', async () => {
|
|
351
|
+
const issue = mockLinearIssue({
|
|
352
|
+
id: 'conv-1',
|
|
353
|
+
identifier: 'SUP-50',
|
|
354
|
+
title: 'Convert me',
|
|
355
|
+
description: 'Some description',
|
|
356
|
+
stateName: 'Delivered',
|
|
357
|
+
labels: ['Feature', 'Frontend'],
|
|
358
|
+
parentId: 'parent-2',
|
|
359
|
+
projectName: 'ConvertProject',
|
|
360
|
+
createdAt: new Date('2025-02-20T00:00:00Z'),
|
|
361
|
+
});
|
|
362
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
363
|
+
expect(result).toEqual({
|
|
364
|
+
id: 'conv-1',
|
|
365
|
+
identifier: 'SUP-50',
|
|
366
|
+
title: 'Convert me',
|
|
367
|
+
description: 'Some description',
|
|
368
|
+
status: 'Delivered',
|
|
369
|
+
labels: ['Feature', 'Frontend'],
|
|
370
|
+
parentId: 'parent-2',
|
|
371
|
+
project: 'ConvertProject',
|
|
372
|
+
createdAt: new Date('2025-02-20T00:00:00Z').getTime(),
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
it('handles issue with no parent', async () => {
|
|
376
|
+
const issue = mockLinearIssue({ parentId: null });
|
|
377
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
378
|
+
expect(result.parentId).toBeUndefined();
|
|
379
|
+
});
|
|
380
|
+
it('handles issue with no project', async () => {
|
|
381
|
+
const issue = mockLinearIssue({ projectName: null });
|
|
382
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
383
|
+
expect(result.project).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
it('handles issue with no state (defaults to Backlog)', async () => {
|
|
386
|
+
const issue = mockLinearIssue({ stateName: '' });
|
|
387
|
+
// Simulate null state
|
|
388
|
+
Object.defineProperty(issue, 'state', {
|
|
389
|
+
get: () => Promise.resolve(null),
|
|
390
|
+
});
|
|
391
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
392
|
+
expect(result.status).toBe('Backlog');
|
|
393
|
+
});
|
|
394
|
+
it('handles issue with null description', async () => {
|
|
395
|
+
const issue = mockLinearIssue({ description: null });
|
|
396
|
+
const result = await adapter.toGovernorIssue(issue);
|
|
397
|
+
expect(result.description).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
it('throws for invalid native object', async () => {
|
|
400
|
+
await expect(adapter.toGovernorIssue(null)).rejects.toThrow('expected a Linear SDK Issue object');
|
|
401
|
+
});
|
|
402
|
+
it('throws for object without id', async () => {
|
|
403
|
+
await expect(adapter.toGovernorIssue({ title: 'no id' })).rejects.toThrow('expected a Linear SDK Issue object');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// ========================================================================
|
|
407
|
+
// isParentIssue (inherited from LinearFrontendAdapter)
|
|
408
|
+
// ========================================================================
|
|
409
|
+
describe('isParentIssue', () => {
|
|
410
|
+
it('delegates to client.isParentIssue', async () => {
|
|
411
|
+
mocks.isParentIssue.mockResolvedValue(true);
|
|
412
|
+
const result = await adapter.isParentIssue('issue-1');
|
|
413
|
+
expect(result).toBe(true);
|
|
414
|
+
expect(mocks.isParentIssue).toHaveBeenCalledWith('issue-1');
|
|
415
|
+
});
|
|
416
|
+
it('returns false for non-parent issues', async () => {
|
|
417
|
+
mocks.isParentIssue.mockResolvedValue(false);
|
|
418
|
+
const result = await adapter.isParentIssue('leaf-issue');
|
|
419
|
+
expect(result).toBe(false);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
// ========================================================================
|
|
423
|
+
// Inherited methods still work
|
|
424
|
+
// ========================================================================
|
|
425
|
+
describe('inherits LinearFrontendAdapter', () => {
|
|
426
|
+
it('resolveStatus works', () => {
|
|
427
|
+
expect(adapter.resolveStatus('backlog')).toBe('Backlog');
|
|
428
|
+
expect(adapter.resolveStatus('started')).toBe('Started');
|
|
429
|
+
});
|
|
430
|
+
it('abstractStatus works', () => {
|
|
431
|
+
expect(adapter.abstractStatus('Backlog')).toBe('backlog');
|
|
432
|
+
expect(adapter.abstractStatus('Finished')).toBe('finished');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-linear",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Linear issue tracker integration for AgentFactory — status transitions, agent sessions, work routing",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|