@calltelemetry/openclaw-linear 0.9.5 → 0.9.7
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/package.json +1 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +232 -156
- package/src/__test__/smoke-linear-api.test.ts +208 -3
- package/src/__test__/webhook-scenarios.test.ts +5 -0
- package/src/infra/shared-profiles.ts +59 -1
- package/src/pipeline/sub-issue-decomposition.test.ts +388 -0
- package/src/pipeline/webhook.test.ts +16 -3
- package/src/pipeline/webhook.ts +94 -11
package/package.json
CHANGED
|
@@ -1,172 +1,248 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Recorded API responses from sub-issue decomposition smoke test.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Last recorded: seed (placeholder)
|
|
3
|
+
* Auto-generated — do not edit manually.
|
|
4
|
+
* Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts
|
|
5
|
+
* Last recorded: 2026-02-22T02:01:43.760Z
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
export const RECORDED = {
|
|
11
|
-
teamStates: [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
user: { name: string } | null;
|
|
37
|
-
createdAt: string;
|
|
38
|
-
}>,
|
|
39
|
-
},
|
|
40
|
-
project: null as { id: string; name: string } | null,
|
|
41
|
-
parent: null as { id: string; identifier: string } | null,
|
|
42
|
-
relations: {
|
|
43
|
-
nodes: [] as Array<{
|
|
44
|
-
type: string;
|
|
45
|
-
relatedIssue: { id: string; identifier: string; title: string };
|
|
46
|
-
}>,
|
|
9
|
+
"teamStates": [
|
|
10
|
+
{
|
|
11
|
+
"id": "cf937197-a76e-4f36-9938-5013442f2137",
|
|
12
|
+
"name": "Todo",
|
|
13
|
+
"type": "unstarted"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "9e40430a-5404-4fed-be64-544417c7c46c",
|
|
17
|
+
"name": "Duplicate",
|
|
18
|
+
"type": "canceled"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "9bf913d6-f32d-452f-af43-3139cc8e5f3f",
|
|
22
|
+
"name": "Canceled",
|
|
23
|
+
"type": "canceled"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "7510263f-555b-42c4-98c3-f4196d755c4b",
|
|
27
|
+
"name": "In Progress",
|
|
28
|
+
"type": "started"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "6bcb2601-dc7b-487e-a407-416521aa235e",
|
|
32
|
+
"name": "Backlog",
|
|
33
|
+
"type": "backlog"
|
|
47
34
|
},
|
|
35
|
+
{
|
|
36
|
+
"id": "69c39ddf-d135-47f7-8dbe-3fc6a539d504",
|
|
37
|
+
"name": "Done",
|
|
38
|
+
"type": "completed"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "2a4c7f41-80cd-4798-91e7-0aa6656d73ca",
|
|
42
|
+
"name": "In Review",
|
|
43
|
+
"type": "started"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"createParent": {
|
|
47
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
48
|
+
"identifier": "UAT-265"
|
|
49
|
+
},
|
|
50
|
+
"createSubIssue1": {
|
|
51
|
+
"id": "de6d1ea0-3358-406a-bcfd-1d19b1516442",
|
|
52
|
+
"identifier": "UAT-266"
|
|
48
53
|
},
|
|
49
|
-
|
|
50
|
-
id: "
|
|
51
|
-
identifier: "UAT-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
createdAt: string;
|
|
67
|
-
}>,
|
|
68
|
-
},
|
|
69
|
-
project: null as { id: string; name: string } | null,
|
|
70
|
-
parent: { id: "parent-001", identifier: "UAT-100" },
|
|
71
|
-
relations: {
|
|
72
|
-
nodes: [] as Array<{
|
|
73
|
-
type: string;
|
|
74
|
-
relatedIssue: { id: string; identifier: string; title: string };
|
|
75
|
-
}>,
|
|
54
|
+
"createSubIssue2": {
|
|
55
|
+
"id": "7ca89fc7-b246-44b6-8400-0e2a4d169f47",
|
|
56
|
+
"identifier": "UAT-267"
|
|
57
|
+
},
|
|
58
|
+
"subIssue1Details": {
|
|
59
|
+
"id": "de6d1ea0-3358-406a-bcfd-1d19b1516442",
|
|
60
|
+
"identifier": "UAT-266",
|
|
61
|
+
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
62
|
+
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
63
|
+
"estimate": 2,
|
|
64
|
+
"state": {
|
|
65
|
+
"name": "Backlog",
|
|
66
|
+
"type": "backlog"
|
|
67
|
+
},
|
|
68
|
+
"creator": {
|
|
69
|
+
"name": "jason.barbee@calltelemetry.com",
|
|
70
|
+
"email": "jason.barbee@calltelemetry.com"
|
|
76
71
|
},
|
|
72
|
+
"assignee": null,
|
|
73
|
+
"labels": {
|
|
74
|
+
"nodes": []
|
|
75
|
+
},
|
|
76
|
+
"team": {
|
|
77
|
+
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
78
|
+
"name": "UAT",
|
|
79
|
+
"issueEstimationType": "tShirt"
|
|
80
|
+
},
|
|
81
|
+
"comments": {
|
|
82
|
+
"nodes": []
|
|
83
|
+
},
|
|
84
|
+
"project": null,
|
|
85
|
+
"parent": {
|
|
86
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
87
|
+
"identifier": "UAT-265"
|
|
88
|
+
},
|
|
89
|
+
"relations": {
|
|
90
|
+
"nodes": []
|
|
91
|
+
}
|
|
77
92
|
},
|
|
78
|
-
subIssue2Details: {
|
|
79
|
-
id: "
|
|
80
|
-
identifier: "UAT-
|
|
81
|
-
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
82
|
-
description:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
"subIssue2Details": {
|
|
94
|
+
"id": "7ca89fc7-b246-44b6-8400-0e2a4d169f47",
|
|
95
|
+
"identifier": "UAT-267",
|
|
96
|
+
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
97
|
+
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
98
|
+
"estimate": 3,
|
|
99
|
+
"state": {
|
|
100
|
+
"name": "Backlog",
|
|
101
|
+
"type": "backlog"
|
|
102
|
+
},
|
|
103
|
+
"creator": {
|
|
104
|
+
"name": "jason.barbee@calltelemetry.com",
|
|
105
|
+
"email": "jason.barbee@calltelemetry.com"
|
|
106
|
+
},
|
|
107
|
+
"assignee": null,
|
|
108
|
+
"labels": {
|
|
109
|
+
"nodes": []
|
|
110
|
+
},
|
|
111
|
+
"team": {
|
|
112
|
+
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
113
|
+
"name": "UAT",
|
|
114
|
+
"issueEstimationType": "tShirt"
|
|
115
|
+
},
|
|
116
|
+
"comments": {
|
|
117
|
+
"nodes": []
|
|
118
|
+
},
|
|
119
|
+
"project": null,
|
|
120
|
+
"parent": {
|
|
121
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
122
|
+
"identifier": "UAT-265"
|
|
105
123
|
},
|
|
124
|
+
"relations": {
|
|
125
|
+
"nodes": []
|
|
126
|
+
}
|
|
106
127
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
creator: {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
nodes: [
|
|
132
|
-
{
|
|
133
|
-
type: "blocks",
|
|
134
|
-
relatedIssue: {
|
|
135
|
-
id: "sub2-001",
|
|
136
|
-
identifier: "UAT-102",
|
|
137
|
-
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
],
|
|
128
|
+
"parentDetails": {
|
|
129
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
130
|
+
"identifier": "UAT-265",
|
|
131
|
+
"title": "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
132
|
+
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T02:01:42.469Z",
|
|
133
|
+
"estimate": null,
|
|
134
|
+
"state": {
|
|
135
|
+
"name": "Backlog",
|
|
136
|
+
"type": "backlog"
|
|
137
|
+
},
|
|
138
|
+
"creator": {
|
|
139
|
+
"name": "jason.barbee@calltelemetry.com",
|
|
140
|
+
"email": "jason.barbee@calltelemetry.com"
|
|
141
|
+
},
|
|
142
|
+
"assignee": null,
|
|
143
|
+
"labels": {
|
|
144
|
+
"nodes": []
|
|
145
|
+
},
|
|
146
|
+
"team": {
|
|
147
|
+
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
148
|
+
"name": "UAT",
|
|
149
|
+
"issueEstimationType": "tShirt"
|
|
150
|
+
},
|
|
151
|
+
"comments": {
|
|
152
|
+
"nodes": []
|
|
141
153
|
},
|
|
154
|
+
"project": null,
|
|
155
|
+
"parent": null,
|
|
156
|
+
"relations": {
|
|
157
|
+
"nodes": []
|
|
158
|
+
}
|
|
142
159
|
},
|
|
143
|
-
|
|
144
|
-
id: "
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
"createRelation": {
|
|
161
|
+
"id": "4c97b0aa-563e-4aad-84aa-826078f012fd"
|
|
162
|
+
},
|
|
163
|
+
"subIssue1WithRelation": {
|
|
164
|
+
"id": "de6d1ea0-3358-406a-bcfd-1d19b1516442",
|
|
165
|
+
"identifier": "UAT-266",
|
|
166
|
+
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
167
|
+
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
168
|
+
"estimate": 2,
|
|
169
|
+
"state": {
|
|
170
|
+
"name": "Backlog",
|
|
171
|
+
"type": "backlog"
|
|
172
|
+
},
|
|
173
|
+
"creator": {
|
|
174
|
+
"name": "jason.barbee@calltelemetry.com",
|
|
175
|
+
"email": "jason.barbee@calltelemetry.com"
|
|
176
|
+
},
|
|
177
|
+
"assignee": null,
|
|
178
|
+
"labels": {
|
|
179
|
+
"nodes": []
|
|
180
|
+
},
|
|
181
|
+
"team": {
|
|
182
|
+
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
183
|
+
"name": "UAT",
|
|
184
|
+
"issueEstimationType": "tShirt"
|
|
185
|
+
},
|
|
186
|
+
"comments": {
|
|
187
|
+
"nodes": []
|
|
170
188
|
},
|
|
189
|
+
"project": null,
|
|
190
|
+
"parent": {
|
|
191
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
192
|
+
"identifier": "UAT-265"
|
|
193
|
+
},
|
|
194
|
+
"relations": {
|
|
195
|
+
"nodes": [
|
|
196
|
+
{
|
|
197
|
+
"type": "blocks",
|
|
198
|
+
"relatedIssue": {
|
|
199
|
+
"id": "7ca89fc7-b246-44b6-8400-0e2a4d169f47",
|
|
200
|
+
"identifier": "UAT-267",
|
|
201
|
+
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
171
206
|
},
|
|
207
|
+
"subIssue2WithRelation": {
|
|
208
|
+
"id": "7ca89fc7-b246-44b6-8400-0e2a4d169f47",
|
|
209
|
+
"identifier": "UAT-267",
|
|
210
|
+
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
211
|
+
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
212
|
+
"estimate": 3,
|
|
213
|
+
"state": {
|
|
214
|
+
"name": "Backlog",
|
|
215
|
+
"type": "backlog"
|
|
216
|
+
},
|
|
217
|
+
"creator": {
|
|
218
|
+
"name": "jason.barbee@calltelemetry.com",
|
|
219
|
+
"email": "jason.barbee@calltelemetry.com"
|
|
220
|
+
},
|
|
221
|
+
"assignee": null,
|
|
222
|
+
"labels": {
|
|
223
|
+
"nodes": []
|
|
224
|
+
},
|
|
225
|
+
"team": {
|
|
226
|
+
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
227
|
+
"name": "UAT",
|
|
228
|
+
"issueEstimationType": "tShirt"
|
|
229
|
+
},
|
|
230
|
+
"comments": {
|
|
231
|
+
"nodes": [
|
|
232
|
+
{
|
|
233
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
234
|
+
"user": null,
|
|
235
|
+
"createdAt": "2026-02-22T02:01:43.418Z"
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
"project": null,
|
|
240
|
+
"parent": {
|
|
241
|
+
"id": "7b5cc5d3-ec16-47e0-b0c4-caf8eea47609",
|
|
242
|
+
"identifier": "UAT-265"
|
|
243
|
+
},
|
|
244
|
+
"relations": {
|
|
245
|
+
"nodes": []
|
|
246
|
+
}
|
|
247
|
+
}
|
|
172
248
|
};
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Requires: ~/.openclaw/auth-profiles.json with a valid linear:api-key profile.
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
15
16
|
import { LinearAgentApi } from "../api/linear-api.js";
|
|
16
17
|
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
17
20
|
// ── Setup ──────────────────────────────────────────────────────────
|
|
18
21
|
|
|
19
22
|
const AUTH_PROFILES_PATH = join(
|
|
@@ -438,7 +441,7 @@ describe("Linear API smoke tests", () => {
|
|
|
438
441
|
}
|
|
439
442
|
|
|
440
443
|
expect(issue.id).toBe(sessionIssueId);
|
|
441
|
-
});
|
|
444
|
+
}, 20_000);
|
|
442
445
|
|
|
443
446
|
it("Path B: createSessionOnIssue via OAuth (programmatic)", async () => {
|
|
444
447
|
expect(sessionIssueId).toBeTruthy();
|
|
@@ -479,6 +482,208 @@ describe("Linear API smoke tests", () => {
|
|
|
479
482
|
});
|
|
480
483
|
});
|
|
481
484
|
|
|
485
|
+
describe("sub-issue decomposition", () => {
|
|
486
|
+
let parentIssueId: string | null = null;
|
|
487
|
+
let parentIssueIdentifier: string | null = null;
|
|
488
|
+
let subIssue1Id: string | null = null;
|
|
489
|
+
let subIssue1Identifier: string | null = null;
|
|
490
|
+
let subIssue2Id: string | null = null;
|
|
491
|
+
let subIssue2Identifier: string | null = null;
|
|
492
|
+
const recorded: Record<string, unknown> = {};
|
|
493
|
+
|
|
494
|
+
let teamStates: Array<{ id: string; name: string; type: string }>;
|
|
495
|
+
|
|
496
|
+
it("fetches team states for sub-issue tests", async () => {
|
|
497
|
+
teamStates = await api.getTeamStates(TEAM_ID);
|
|
498
|
+
expect(teamStates.length).toBeGreaterThan(0);
|
|
499
|
+
recorded.teamStates = teamStates;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("creates a parent issue", async () => {
|
|
503
|
+
const backlogState = teamStates.find((s) => s.type === "backlog");
|
|
504
|
+
expect(backlogState).toBeTruthy();
|
|
505
|
+
|
|
506
|
+
const result = await api.createIssue({
|
|
507
|
+
teamId: TEAM_ID,
|
|
508
|
+
title: "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
509
|
+
description:
|
|
510
|
+
"Auto-generated by smoke test to verify sub-issue decomposition.\n\n" +
|
|
511
|
+
"This parent issue should have two sub-issues created under it.\n\n" +
|
|
512
|
+
`Created: ${new Date().toISOString()}`,
|
|
513
|
+
stateId: backlogState!.id,
|
|
514
|
+
priority: 4,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(result.id).toBeTruthy();
|
|
518
|
+
expect(result.identifier).toBeTruthy();
|
|
519
|
+
parentIssueId = result.id;
|
|
520
|
+
parentIssueIdentifier = result.identifier;
|
|
521
|
+
recorded.createParent = result;
|
|
522
|
+
console.log(`Created parent issue: ${result.identifier} (${result.id})`);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("creates sub-issue 1 under parent (Backend API)", async () => {
|
|
526
|
+
expect(parentIssueId).toBeTruthy();
|
|
527
|
+
const backlogState = teamStates.find((s) => s.type === "backlog");
|
|
528
|
+
|
|
529
|
+
const result = await api.createIssue({
|
|
530
|
+
teamId: TEAM_ID,
|
|
531
|
+
title: "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
532
|
+
description:
|
|
533
|
+
"Implement the backend search API endpoint.\n\n" +
|
|
534
|
+
"Given a search query, when the API is called, then matching results are returned.",
|
|
535
|
+
stateId: backlogState!.id,
|
|
536
|
+
parentId: parentIssueId!,
|
|
537
|
+
priority: 3,
|
|
538
|
+
estimate: 2,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
expect(result.id).toBeTruthy();
|
|
542
|
+
expect(result.identifier).toBeTruthy();
|
|
543
|
+
subIssue1Id = result.id;
|
|
544
|
+
subIssue1Identifier = result.identifier;
|
|
545
|
+
recorded.createSubIssue1 = result;
|
|
546
|
+
console.log(`Created sub-issue 1: ${result.identifier} (parentId=${parentIssueId})`);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("creates sub-issue 2 under parent (Frontend UI)", async () => {
|
|
550
|
+
expect(parentIssueId).toBeTruthy();
|
|
551
|
+
const backlogState = teamStates.find((s) => s.type === "backlog");
|
|
552
|
+
|
|
553
|
+
const result = await api.createIssue({
|
|
554
|
+
teamId: TEAM_ID,
|
|
555
|
+
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
556
|
+
description:
|
|
557
|
+
"Build the frontend search UI component.\n\n" +
|
|
558
|
+
"Given the search page loads, when the user types a query, then results display in real-time.",
|
|
559
|
+
stateId: backlogState!.id,
|
|
560
|
+
parentId: parentIssueId!,
|
|
561
|
+
priority: 3,
|
|
562
|
+
estimate: 3,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(result.id).toBeTruthy();
|
|
566
|
+
expect(result.identifier).toBeTruthy();
|
|
567
|
+
subIssue2Id = result.id;
|
|
568
|
+
subIssue2Identifier = result.identifier;
|
|
569
|
+
recorded.createSubIssue2 = result;
|
|
570
|
+
console.log(`Created sub-issue 2: ${result.identifier} (parentId=${parentIssueId})`);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("sub-issue 1 has parent field pointing to parent", async () => {
|
|
574
|
+
expect(subIssue1Id).toBeTruthy();
|
|
575
|
+
const details = await api.getIssueDetails(subIssue1Id!);
|
|
576
|
+
|
|
577
|
+
expect(details.parent).not.toBeNull();
|
|
578
|
+
expect(details.parent!.id).toBe(parentIssueId);
|
|
579
|
+
expect(details.parent!.identifier).toBe(parentIssueIdentifier);
|
|
580
|
+
recorded.subIssue1Details = details;
|
|
581
|
+
console.log(`Sub-issue 1 parent: ${details.parent!.identifier}`);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("sub-issue 2 has parent field pointing to parent", async () => {
|
|
585
|
+
expect(subIssue2Id).toBeTruthy();
|
|
586
|
+
const details = await api.getIssueDetails(subIssue2Id!);
|
|
587
|
+
|
|
588
|
+
expect(details.parent).not.toBeNull();
|
|
589
|
+
expect(details.parent!.id).toBe(parentIssueId);
|
|
590
|
+
expect(details.parent!.identifier).toBe(parentIssueIdentifier);
|
|
591
|
+
recorded.subIssue2Details = details;
|
|
592
|
+
console.log(`Sub-issue 2 parent: ${details.parent!.identifier}`);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("parent issue has no parent (it is the root)", async () => {
|
|
596
|
+
expect(parentIssueId).toBeTruthy();
|
|
597
|
+
const details = await api.getIssueDetails(parentIssueId!);
|
|
598
|
+
|
|
599
|
+
expect(details.parent).toBeNull();
|
|
600
|
+
recorded.parentDetails = details;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("creates blocks relation (sub-issue 1 blocks sub-issue 2)", async () => {
|
|
604
|
+
expect(subIssue1Id).toBeTruthy();
|
|
605
|
+
expect(subIssue2Id).toBeTruthy();
|
|
606
|
+
|
|
607
|
+
const result = await api.createIssueRelation({
|
|
608
|
+
issueId: subIssue1Id!,
|
|
609
|
+
relatedIssueId: subIssue2Id!,
|
|
610
|
+
type: "blocks",
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(result.id).toBeTruthy();
|
|
614
|
+
recorded.createRelation = result;
|
|
615
|
+
console.log(`Created blocks relation: ${subIssue1Identifier} blocks ${subIssue2Identifier}`);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("sub-issue 1 shows blocks relation to sub-issue 2", async () => {
|
|
619
|
+
expect(subIssue1Id).toBeTruthy();
|
|
620
|
+
const details = await api.getIssueDetails(subIssue1Id!);
|
|
621
|
+
const blocksRels = details.relations.nodes.filter(
|
|
622
|
+
(r) => r.type === "blocks",
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
expect(blocksRels.length).toBeGreaterThan(0);
|
|
626
|
+
expect(
|
|
627
|
+
blocksRels.some((r) => r.relatedIssue.id === subIssue2Id),
|
|
628
|
+
).toBe(true);
|
|
629
|
+
recorded.subIssue1WithRelation = details;
|
|
630
|
+
console.log(`Sub-issue 1 blocks: ${blocksRels.map((r) => r.relatedIssue.identifier).join(", ")}`);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("sub-issue 2 shows inverse relation", async () => {
|
|
634
|
+
expect(subIssue2Id).toBeTruthy();
|
|
635
|
+
const details = await api.getIssueDetails(subIssue2Id!);
|
|
636
|
+
|
|
637
|
+
// Linear may or may not populate an inverse "is_blocked_by" relation.
|
|
638
|
+
// Record whatever we get — the mock test replays it as-is.
|
|
639
|
+
recorded.subIssue2WithRelation = details;
|
|
640
|
+
const rels = details.relations.nodes;
|
|
641
|
+
console.log(
|
|
642
|
+
`Sub-issue 2 relations: ${rels.length > 0 ? rels.map((r) => `${r.type} ${r.relatedIssue.identifier}`).join(", ") : "(none — inverse may not be returned)"}`,
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("saves recorded API responses to fixture file", () => {
|
|
647
|
+
// Only write if we actually ran the full flow
|
|
648
|
+
if (!parentIssueId || !subIssue1Id || !subIssue2Id) {
|
|
649
|
+
console.log("Skipping fixture write — not all issues were created.");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const fixturePath = join(
|
|
654
|
+
__dirname,
|
|
655
|
+
"fixtures",
|
|
656
|
+
"recorded-sub-issue-flow.ts",
|
|
657
|
+
);
|
|
658
|
+
const content =
|
|
659
|
+
`/**\n` +
|
|
660
|
+
` * Recorded API responses from sub-issue decomposition smoke test.\n` +
|
|
661
|
+
` * Auto-generated — do not edit manually.\n` +
|
|
662
|
+
` * Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts\n` +
|
|
663
|
+
` * Last recorded: ${new Date().toISOString()}\n` +
|
|
664
|
+
` */\n\n` +
|
|
665
|
+
`export const RECORDED = ${JSON.stringify(recorded, null, 2)};\n`;
|
|
666
|
+
|
|
667
|
+
writeFileSync(fixturePath, content, "utf8");
|
|
668
|
+
console.log(`Recorded fixture written to: ${fixturePath}`);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("cleans up: cancels parent and sub-issues", async () => {
|
|
672
|
+
const canceledState = teamStates?.find(
|
|
673
|
+
(s) => s.type === "canceled" || s.name.toLowerCase().includes("cancel"),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
for (const id of [subIssue1Id, subIssue2Id, parentIssueId]) {
|
|
677
|
+
if (!id || !canceledState) continue;
|
|
678
|
+
try {
|
|
679
|
+
await api.updateIssue(id, { stateId: canceledState.id });
|
|
680
|
+
} catch {
|
|
681
|
+
// Best effort
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
482
687
|
describe("cleanup", () => {
|
|
483
688
|
it("cancels the smoke test issue", async () => {
|
|
484
689
|
if (!smokeIssueId) return;
|
|
@@ -152,6 +152,11 @@ vi.mock("../pipeline/dag-dispatch.js", () => ({
|
|
|
152
152
|
startProjectDispatch: vi.fn().mockResolvedValue(undefined),
|
|
153
153
|
}));
|
|
154
154
|
|
|
155
|
+
vi.mock("../infra/shared-profiles.js", async (importOriginal) => {
|
|
156
|
+
const actual = await importOriginal() as Record<string, unknown>;
|
|
157
|
+
return { ...actual, validateProfiles: vi.fn().mockReturnValue(null) };
|
|
158
|
+
});
|
|
159
|
+
|
|
155
160
|
// ── Imports (after mocks) ────────────────────────────────────────
|
|
156
161
|
|
|
157
162
|
import { handleLinearWebhook, _resetForTesting } from "../pipeline/webhook.js";
|