@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,172 +1,248 @@
1
1
  /**
2
2
  * Recorded API responses from sub-issue decomposition smoke test.
3
- *
4
- * Initially seeded with placeholder data. Overwritten with real API responses
5
- * when the smoke test runs: npx vitest run src/__test__/smoke-linear-api.test.ts
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
- { id: "state-backlog", name: "Backlog", type: "backlog" },
13
- { id: "state-started", name: "In Progress", type: "started" },
14
- { id: "state-done", name: "Done", type: "completed" },
15
- { id: "state-canceled", name: "Canceled", type: "canceled" },
16
- ],
17
- createParent: { id: "parent-001", identifier: "UAT-100" },
18
- createSubIssue1: { id: "sub1-001", identifier: "UAT-101" },
19
- createSubIssue2: { id: "sub2-001", identifier: "UAT-102" },
20
- parentDetails: {
21
- id: "parent-001",
22
- identifier: "UAT-100",
23
- title: "[SMOKE TEST] Sub-Issue Parent: Search Feature",
24
- description:
25
- "Auto-generated by smoke test to verify sub-issue decomposition.\n\n" +
26
- "This parent issue should have two sub-issues created under it.",
27
- estimate: null,
28
- state: { name: "Backlog", type: "backlog" },
29
- creator: { name: "Test User", email: null as string | null },
30
- assignee: null as { name: string } | null,
31
- labels: { nodes: [] as Array<{ id: string; name: string }> },
32
- team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
33
- comments: {
34
- nodes: [] as Array<{
35
- body: string;
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
- subIssue1Details: {
50
- id: "sub1-001",
51
- identifier: "UAT-101",
52
- title: "[SMOKE TEST] Sub-Issue 1: Backend API",
53
- description:
54
- "Implement the backend search API endpoint.\n\n" +
55
- "Given a search query, when the API is called, then matching results are returned.",
56
- estimate: 2,
57
- state: { name: "Backlog", type: "backlog" },
58
- creator: { name: "Test User", email: null as string | null },
59
- assignee: null as { name: string } | null,
60
- labels: { nodes: [] as Array<{ id: string; name: string }> },
61
- team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
62
- comments: {
63
- nodes: [] as Array<{
64
- body: string;
65
- user: { name: string } | null;
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: "sub2-001",
80
- identifier: "UAT-102",
81
- title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
82
- description:
83
- "Build the frontend search UI component.\n\n" +
84
- "Given the search page loads, when the user types a query, then results display in real-time.",
85
- estimate: 3,
86
- state: { name: "Backlog", type: "backlog" },
87
- creator: { name: "Test User", email: null as string | null },
88
- assignee: null as { name: string } | null,
89
- labels: { nodes: [] as Array<{ id: string; name: string }> },
90
- team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
91
- comments: {
92
- nodes: [] as Array<{
93
- body: string;
94
- user: { name: string } | null;
95
- createdAt: string;
96
- }>,
97
- },
98
- project: null as { id: string; name: string } | null,
99
- parent: { id: "parent-001", identifier: "UAT-100" },
100
- relations: {
101
- nodes: [] as Array<{
102
- type: string;
103
- relatedIssue: { id: string; identifier: string; title: string };
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
- createRelation: { id: "rel-001" },
108
- subIssue1WithRelation: {
109
- id: "sub1-001",
110
- identifier: "UAT-101",
111
- title: "[SMOKE TEST] Sub-Issue 1: Backend API",
112
- description:
113
- "Implement the backend search API endpoint.\n\n" +
114
- "Given a search query, when the API is called, then matching results are returned.",
115
- estimate: 2,
116
- state: { name: "Backlog", type: "backlog" },
117
- creator: { name: "Test User", email: null as string | null },
118
- assignee: null as { name: string } | null,
119
- labels: { nodes: [] as Array<{ id: string; name: string }> },
120
- team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
121
- comments: {
122
- nodes: [] as Array<{
123
- body: string;
124
- user: { name: string } | null;
125
- createdAt: string;
126
- }>,
127
- },
128
- project: null as { id: string; name: string } | null,
129
- parent: { id: "parent-001", identifier: "UAT-100" },
130
- relations: {
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
- subIssue2WithRelation: {
144
- id: "sub2-001",
145
- identifier: "UAT-102",
146
- title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
147
- description:
148
- "Build the frontend search UI component.\n\n" +
149
- "Given the search page loads, when the user types a query, then results display in real-time.",
150
- estimate: 3,
151
- state: { name: "Backlog", type: "backlog" },
152
- creator: { name: "Test User", email: null as string | null },
153
- assignee: null as { name: string } | null,
154
- labels: { nodes: [] as Array<{ id: string; name: string }> },
155
- team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
156
- comments: {
157
- nodes: [] as Array<{
158
- body: string;
159
- user: { name: string } | null;
160
- createdAt: string;
161
- }>,
162
- },
163
- project: null as { id: string; name: string } | null,
164
- parent: { id: "parent-001", identifier: "UAT-100" },
165
- relations: {
166
- nodes: [] as Array<{
167
- type: string;
168
- relatedIssue: { id: string; identifier: string; title: string };
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";