@checkstack/incident-backend 1.11.0 → 1.12.0
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/CHANGELOG.md +317 -0
- package/drizzle/0005_real_leper_queen.sql +4 -0
- package/drizzle/0006_last_princess_powerful.sql +1 -0
- package/drizzle/meta/0005_snapshot.json +346 -0
- package/drizzle/meta/0006_snapshot.json +353 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +13 -13
- package/src/ai/incident-add-link.test.ts +1 -0
- package/src/ai/incident-add-update.test.ts +6 -1
- package/src/ai/incident-delete-update.test.ts +59 -0
- package/src/ai/incident-delete-update.ts +73 -0
- package/src/ai/register-ai-tools.ts +2 -0
- package/src/automations.test.ts +70 -0
- package/src/automations.ts +77 -4
- package/src/hooks.ts +54 -4
- package/src/index.ts +4 -0
- package/src/notifications.test.ts +181 -0
- package/src/notifications.ts +12 -1
- package/src/read-visibility.test.ts +158 -0
- package/src/read-visibility.ts +99 -0
- package/src/router.test.ts +69 -1
- package/src/router.ts +195 -15
- package/src/schema.ts +27 -0
- package/src/service-reads.it.test.ts +353 -0
- package/src/service-updates.it.test.ts +340 -0
- package/src/service.it.test.ts +57 -0
- package/src/service.test.ts +235 -1
- package/src/service.ts +489 -177
- package/src/status-page-widget.test.ts +149 -0
- package/src/status-page-widget.ts +133 -37
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ff7f73e0-3d6b-47a4-b759-000739af4ee6",
|
|
3
|
+
"prevId": "ad7ffe14-5ac3-4028-895f-efa6c8026073",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.incident_links": {
|
|
8
|
+
"name": "incident_links",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"incident_id": {
|
|
18
|
+
"name": "incident_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"label": {
|
|
24
|
+
"name": "label",
|
|
25
|
+
"type": "text",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": false
|
|
28
|
+
},
|
|
29
|
+
"url": {
|
|
30
|
+
"name": "url",
|
|
31
|
+
"type": "text",
|
|
32
|
+
"primaryKey": false,
|
|
33
|
+
"notNull": true
|
|
34
|
+
},
|
|
35
|
+
"visibility": {
|
|
36
|
+
"name": "visibility",
|
|
37
|
+
"type": "incident_content_visibility",
|
|
38
|
+
"typeSchema": "public",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": true,
|
|
41
|
+
"default": "'public'"
|
|
42
|
+
},
|
|
43
|
+
"created_at": {
|
|
44
|
+
"name": "created_at",
|
|
45
|
+
"type": "timestamp",
|
|
46
|
+
"primaryKey": false,
|
|
47
|
+
"notNull": true,
|
|
48
|
+
"default": "now()"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"indexes": {
|
|
52
|
+
"incident_links_incident_url_unique": {
|
|
53
|
+
"name": "incident_links_incident_url_unique",
|
|
54
|
+
"columns": [
|
|
55
|
+
{
|
|
56
|
+
"expression": "incident_id",
|
|
57
|
+
"isExpression": false,
|
|
58
|
+
"asc": true,
|
|
59
|
+
"nulls": "last"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"expression": "url",
|
|
63
|
+
"isExpression": false,
|
|
64
|
+
"asc": true,
|
|
65
|
+
"nulls": "last"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"isUnique": true,
|
|
69
|
+
"concurrently": false,
|
|
70
|
+
"method": "btree",
|
|
71
|
+
"with": {}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"foreignKeys": {
|
|
75
|
+
"incident_links_incident_id_incidents_id_fk": {
|
|
76
|
+
"name": "incident_links_incident_id_incidents_id_fk",
|
|
77
|
+
"tableFrom": "incident_links",
|
|
78
|
+
"tableTo": "incidents",
|
|
79
|
+
"columnsFrom": [
|
|
80
|
+
"incident_id"
|
|
81
|
+
],
|
|
82
|
+
"columnsTo": [
|
|
83
|
+
"id"
|
|
84
|
+
],
|
|
85
|
+
"onDelete": "cascade",
|
|
86
|
+
"onUpdate": "no action"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"compositePrimaryKeys": {},
|
|
90
|
+
"uniqueConstraints": {},
|
|
91
|
+
"policies": {},
|
|
92
|
+
"checkConstraints": {},
|
|
93
|
+
"isRLSEnabled": false
|
|
94
|
+
},
|
|
95
|
+
"public.incident_systems": {
|
|
96
|
+
"name": "incident_systems",
|
|
97
|
+
"schema": "",
|
|
98
|
+
"columns": {
|
|
99
|
+
"incident_id": {
|
|
100
|
+
"name": "incident_id",
|
|
101
|
+
"type": "text",
|
|
102
|
+
"primaryKey": false,
|
|
103
|
+
"notNull": true
|
|
104
|
+
},
|
|
105
|
+
"system_id": {
|
|
106
|
+
"name": "system_id",
|
|
107
|
+
"type": "text",
|
|
108
|
+
"primaryKey": false,
|
|
109
|
+
"notNull": true
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"indexes": {},
|
|
113
|
+
"foreignKeys": {
|
|
114
|
+
"incident_systems_incident_id_incidents_id_fk": {
|
|
115
|
+
"name": "incident_systems_incident_id_incidents_id_fk",
|
|
116
|
+
"tableFrom": "incident_systems",
|
|
117
|
+
"tableTo": "incidents",
|
|
118
|
+
"columnsFrom": [
|
|
119
|
+
"incident_id"
|
|
120
|
+
],
|
|
121
|
+
"columnsTo": [
|
|
122
|
+
"id"
|
|
123
|
+
],
|
|
124
|
+
"onDelete": "cascade",
|
|
125
|
+
"onUpdate": "no action"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"compositePrimaryKeys": {
|
|
129
|
+
"incident_systems_incident_id_system_id_pk": {
|
|
130
|
+
"name": "incident_systems_incident_id_system_id_pk",
|
|
131
|
+
"columns": [
|
|
132
|
+
"incident_id",
|
|
133
|
+
"system_id"
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"uniqueConstraints": {},
|
|
138
|
+
"policies": {},
|
|
139
|
+
"checkConstraints": {},
|
|
140
|
+
"isRLSEnabled": false
|
|
141
|
+
},
|
|
142
|
+
"public.incident_updates": {
|
|
143
|
+
"name": "incident_updates",
|
|
144
|
+
"schema": "",
|
|
145
|
+
"columns": {
|
|
146
|
+
"id": {
|
|
147
|
+
"name": "id",
|
|
148
|
+
"type": "text",
|
|
149
|
+
"primaryKey": true,
|
|
150
|
+
"notNull": true
|
|
151
|
+
},
|
|
152
|
+
"incident_id": {
|
|
153
|
+
"name": "incident_id",
|
|
154
|
+
"type": "text",
|
|
155
|
+
"primaryKey": false,
|
|
156
|
+
"notNull": true
|
|
157
|
+
},
|
|
158
|
+
"message": {
|
|
159
|
+
"name": "message",
|
|
160
|
+
"type": "text",
|
|
161
|
+
"primaryKey": false,
|
|
162
|
+
"notNull": true
|
|
163
|
+
},
|
|
164
|
+
"status_change": {
|
|
165
|
+
"name": "status_change",
|
|
166
|
+
"type": "incident_status",
|
|
167
|
+
"typeSchema": "public",
|
|
168
|
+
"primaryKey": false,
|
|
169
|
+
"notNull": false
|
|
170
|
+
},
|
|
171
|
+
"visibility": {
|
|
172
|
+
"name": "visibility",
|
|
173
|
+
"type": "incident_content_visibility",
|
|
174
|
+
"typeSchema": "public",
|
|
175
|
+
"primaryKey": false,
|
|
176
|
+
"notNull": true,
|
|
177
|
+
"default": "'public'"
|
|
178
|
+
},
|
|
179
|
+
"created_at": {
|
|
180
|
+
"name": "created_at",
|
|
181
|
+
"type": "timestamp",
|
|
182
|
+
"primaryKey": false,
|
|
183
|
+
"notNull": true,
|
|
184
|
+
"default": "now()"
|
|
185
|
+
},
|
|
186
|
+
"edited_at": {
|
|
187
|
+
"name": "edited_at",
|
|
188
|
+
"type": "timestamp",
|
|
189
|
+
"primaryKey": false,
|
|
190
|
+
"notNull": false
|
|
191
|
+
},
|
|
192
|
+
"edit_history": {
|
|
193
|
+
"name": "edit_history",
|
|
194
|
+
"type": "jsonb",
|
|
195
|
+
"primaryKey": false,
|
|
196
|
+
"notNull": true,
|
|
197
|
+
"default": "'[]'::jsonb"
|
|
198
|
+
},
|
|
199
|
+
"created_by": {
|
|
200
|
+
"name": "created_by",
|
|
201
|
+
"type": "text",
|
|
202
|
+
"primaryKey": false,
|
|
203
|
+
"notNull": false
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"indexes": {},
|
|
207
|
+
"foreignKeys": {
|
|
208
|
+
"incident_updates_incident_id_incidents_id_fk": {
|
|
209
|
+
"name": "incident_updates_incident_id_incidents_id_fk",
|
|
210
|
+
"tableFrom": "incident_updates",
|
|
211
|
+
"tableTo": "incidents",
|
|
212
|
+
"columnsFrom": [
|
|
213
|
+
"incident_id"
|
|
214
|
+
],
|
|
215
|
+
"columnsTo": [
|
|
216
|
+
"id"
|
|
217
|
+
],
|
|
218
|
+
"onDelete": "cascade",
|
|
219
|
+
"onUpdate": "no action"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"compositePrimaryKeys": {},
|
|
223
|
+
"uniqueConstraints": {},
|
|
224
|
+
"policies": {},
|
|
225
|
+
"checkConstraints": {},
|
|
226
|
+
"isRLSEnabled": false
|
|
227
|
+
},
|
|
228
|
+
"public.incidents": {
|
|
229
|
+
"name": "incidents",
|
|
230
|
+
"schema": "",
|
|
231
|
+
"columns": {
|
|
232
|
+
"id": {
|
|
233
|
+
"name": "id",
|
|
234
|
+
"type": "text",
|
|
235
|
+
"primaryKey": true,
|
|
236
|
+
"notNull": true
|
|
237
|
+
},
|
|
238
|
+
"title": {
|
|
239
|
+
"name": "title",
|
|
240
|
+
"type": "text",
|
|
241
|
+
"primaryKey": false,
|
|
242
|
+
"notNull": true
|
|
243
|
+
},
|
|
244
|
+
"description": {
|
|
245
|
+
"name": "description",
|
|
246
|
+
"type": "text",
|
|
247
|
+
"primaryKey": false,
|
|
248
|
+
"notNull": false
|
|
249
|
+
},
|
|
250
|
+
"status": {
|
|
251
|
+
"name": "status",
|
|
252
|
+
"type": "incident_status",
|
|
253
|
+
"typeSchema": "public",
|
|
254
|
+
"primaryKey": false,
|
|
255
|
+
"notNull": true,
|
|
256
|
+
"default": "'investigating'"
|
|
257
|
+
},
|
|
258
|
+
"severity": {
|
|
259
|
+
"name": "severity",
|
|
260
|
+
"type": "incident_severity",
|
|
261
|
+
"typeSchema": "public",
|
|
262
|
+
"primaryKey": false,
|
|
263
|
+
"notNull": true,
|
|
264
|
+
"default": "'major'"
|
|
265
|
+
},
|
|
266
|
+
"suppress_notifications": {
|
|
267
|
+
"name": "suppress_notifications",
|
|
268
|
+
"type": "boolean",
|
|
269
|
+
"primaryKey": false,
|
|
270
|
+
"notNull": true,
|
|
271
|
+
"default": false
|
|
272
|
+
},
|
|
273
|
+
"health_override": {
|
|
274
|
+
"name": "health_override",
|
|
275
|
+
"type": "incident_health_override",
|
|
276
|
+
"typeSchema": "public",
|
|
277
|
+
"primaryKey": false,
|
|
278
|
+
"notNull": false
|
|
279
|
+
},
|
|
280
|
+
"created_at": {
|
|
281
|
+
"name": "created_at",
|
|
282
|
+
"type": "timestamp",
|
|
283
|
+
"primaryKey": false,
|
|
284
|
+
"notNull": true,
|
|
285
|
+
"default": "now()"
|
|
286
|
+
},
|
|
287
|
+
"updated_at": {
|
|
288
|
+
"name": "updated_at",
|
|
289
|
+
"type": "timestamp",
|
|
290
|
+
"primaryKey": false,
|
|
291
|
+
"notNull": true,
|
|
292
|
+
"default": "now()"
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"indexes": {},
|
|
296
|
+
"foreignKeys": {},
|
|
297
|
+
"compositePrimaryKeys": {},
|
|
298
|
+
"uniqueConstraints": {},
|
|
299
|
+
"policies": {},
|
|
300
|
+
"checkConstraints": {},
|
|
301
|
+
"isRLSEnabled": false
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"enums": {
|
|
305
|
+
"public.incident_health_override": {
|
|
306
|
+
"name": "incident_health_override",
|
|
307
|
+
"schema": "public",
|
|
308
|
+
"values": [
|
|
309
|
+
"degraded",
|
|
310
|
+
"unhealthy"
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
"public.incident_severity": {
|
|
314
|
+
"name": "incident_severity",
|
|
315
|
+
"schema": "public",
|
|
316
|
+
"values": [
|
|
317
|
+
"minor",
|
|
318
|
+
"major",
|
|
319
|
+
"critical"
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
"public.incident_status": {
|
|
323
|
+
"name": "incident_status",
|
|
324
|
+
"schema": "public",
|
|
325
|
+
"values": [
|
|
326
|
+
"investigating",
|
|
327
|
+
"identified",
|
|
328
|
+
"fixing",
|
|
329
|
+
"monitoring",
|
|
330
|
+
"resolved"
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
"public.incident_content_visibility": {
|
|
334
|
+
"name": "incident_content_visibility",
|
|
335
|
+
"schema": "public",
|
|
336
|
+
"values": [
|
|
337
|
+
"public",
|
|
338
|
+
"logged_in",
|
|
339
|
+
"internal"
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"schemas": {},
|
|
344
|
+
"sequences": {},
|
|
345
|
+
"roles": {},
|
|
346
|
+
"policies": {},
|
|
347
|
+
"views": {},
|
|
348
|
+
"_meta": {
|
|
349
|
+
"columns": {},
|
|
350
|
+
"schemas": {},
|
|
351
|
+
"tables": {}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -36,6 +36,20 @@
|
|
|
36
36
|
"when": 1783287429703,
|
|
37
37
|
"tag": "0004_flaky_malcolm_colcord",
|
|
38
38
|
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "7",
|
|
43
|
+
"when": 1783498751737,
|
|
44
|
+
"tag": "0005_real_leper_queen",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "7",
|
|
50
|
+
"when": 1783535507970,
|
|
51
|
+
"tag": "0006_last_princess_powerful",
|
|
52
|
+
"breakpoints": true
|
|
39
53
|
}
|
|
40
54
|
]
|
|
41
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/incident-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,23 +14,23 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/ai-backend": "0.10.
|
|
17
|
+
"@checkstack/ai-backend": "0.10.10",
|
|
18
18
|
"@checkstack/ai-common": "0.6.6",
|
|
19
|
-
"@checkstack/backend-api": "0.31.
|
|
19
|
+
"@checkstack/backend-api": "0.31.1",
|
|
20
20
|
"@checkstack/cache-api": "0.3.19",
|
|
21
21
|
"@checkstack/cache-utils": "0.2.24",
|
|
22
|
-
"@checkstack/incident-common": "1.
|
|
23
|
-
"@checkstack/catalog-common": "2.
|
|
24
|
-
"@checkstack/catalog-backend": "1.
|
|
25
|
-
"@checkstack/notification-common": "1.
|
|
22
|
+
"@checkstack/incident-common": "1.10.0",
|
|
23
|
+
"@checkstack/catalog-common": "2.7.0",
|
|
24
|
+
"@checkstack/catalog-backend": "1.7.0",
|
|
25
|
+
"@checkstack/notification-common": "1.6.0",
|
|
26
26
|
"@checkstack/auth-common": "0.13.0",
|
|
27
|
-
"@checkstack/command-backend": "0.2.
|
|
27
|
+
"@checkstack/command-backend": "0.2.22",
|
|
28
28
|
"@checkstack/signal-common": "0.2.17",
|
|
29
|
-
"@checkstack/status-page-backend": "0.
|
|
30
|
-
"@checkstack/status-page-common": "0.
|
|
31
|
-
"@checkstack/integration-backend": "0.7.
|
|
29
|
+
"@checkstack/status-page-backend": "0.5.0",
|
|
30
|
+
"@checkstack/status-page-common": "0.6.0",
|
|
31
|
+
"@checkstack/integration-backend": "0.7.4",
|
|
32
32
|
"@checkstack/integration-common": "0.9.8",
|
|
33
|
-
"@checkstack/automation-backend": "0.11.
|
|
33
|
+
"@checkstack/automation-backend": "0.11.1",
|
|
34
34
|
"@checkstack/automation-common": "0.10.0",
|
|
35
35
|
"@checkstack/common": "0.22.0",
|
|
36
36
|
"drizzle-orm": "^0.45.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@checkstack/drizzle-helper": "0.0.6",
|
|
43
43
|
"@checkstack/scripts": "0.7.3",
|
|
44
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
44
|
+
"@checkstack/test-utils-backend": "0.1.56",
|
|
45
45
|
"@checkstack/tsconfig": "0.0.7",
|
|
46
46
|
"@types/bun": "^1.0.0",
|
|
47
47
|
"@types/pg": "^8.20.0",
|
|
@@ -45,7 +45,12 @@ describe("incident.addUpdate tool", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("execute (apply) posts via addUpdate", async () => {
|
|
48
|
-
const created = {
|
|
48
|
+
const created = {
|
|
49
|
+
id: "upd1",
|
|
50
|
+
...input,
|
|
51
|
+
visibility: "public" as const,
|
|
52
|
+
createdAt: new Date(),
|
|
53
|
+
};
|
|
49
54
|
const addUpdate = mock(() => Promise.resolve(created));
|
|
50
55
|
const rpcClient = fakeRpcClient({ addUpdate });
|
|
51
56
|
const tool = createIncidentAddUpdateTool();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test, mock } from "bun:test";
|
|
2
|
+
import type { AuthUser, RpcClient } from "@checkstack/backend-api";
|
|
3
|
+
import { createIncidentDeleteUpdateTool } from "./incident-delete-update";
|
|
4
|
+
|
|
5
|
+
const principal: AuthUser = {
|
|
6
|
+
type: "user",
|
|
7
|
+
id: "u1",
|
|
8
|
+
accessRules: ["incident.incident.manage"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function fakeRpcClient({
|
|
12
|
+
deleteUpdate,
|
|
13
|
+
}: {
|
|
14
|
+
deleteUpdate: ReturnType<typeof mock>;
|
|
15
|
+
}): RpcClient {
|
|
16
|
+
return {
|
|
17
|
+
forPlugin: () => ({ deleteUpdate }),
|
|
18
|
+
} as unknown as RpcClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("incident.deleteUpdate tool", () => {
|
|
22
|
+
test("declares destructive effect + the manage rule", () => {
|
|
23
|
+
const tool = createIncidentDeleteUpdateTool();
|
|
24
|
+
expect(tool.name).toBe("incident.deleteUpdate");
|
|
25
|
+
expect(tool.effect).toBe("destructive");
|
|
26
|
+
expect(tool.requiredAccessRules).toEqual(["incident.incident.manage"]);
|
|
27
|
+
expect(typeof tool.dryRun).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("dryRun returns a payload and NEVER deletes the update", async () => {
|
|
31
|
+
const deleteUpdate = mock(() => Promise.resolve({ success: true }));
|
|
32
|
+
const rpcClient = fakeRpcClient({ deleteUpdate });
|
|
33
|
+
const tool = createIncidentDeleteUpdateTool();
|
|
34
|
+
const preview = await tool.dryRun!({
|
|
35
|
+
input: { id: "upd1", incidentId: "inc1" },
|
|
36
|
+
principal,
|
|
37
|
+
rpcClient,
|
|
38
|
+
});
|
|
39
|
+
expect(deleteUpdate).not.toHaveBeenCalled();
|
|
40
|
+
expect(preview.summary).toContain("permanent");
|
|
41
|
+
expect(preview.payload).toEqual({ id: "upd1", incidentId: "inc1" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("execute (apply) deletes via deleteUpdate with {id, incidentId}", async () => {
|
|
45
|
+
const deleteUpdate = mock(() => Promise.resolve({ success: true }));
|
|
46
|
+
const rpcClient = fakeRpcClient({ deleteUpdate });
|
|
47
|
+
const tool = createIncidentDeleteUpdateTool();
|
|
48
|
+
const result = await tool.execute({
|
|
49
|
+
input: { id: "upd1", incidentId: "inc1" },
|
|
50
|
+
principal,
|
|
51
|
+
rpcClient,
|
|
52
|
+
});
|
|
53
|
+
expect(deleteUpdate).toHaveBeenCalledWith({
|
|
54
|
+
id: "upd1",
|
|
55
|
+
incidentId: "inc1",
|
|
56
|
+
});
|
|
57
|
+
expect(result).toEqual({ id: "upd1", removed: true });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { qualifyAccessRuleId } from "@checkstack/common";
|
|
3
|
+
import type { RpcClient, AuthUser } from "@checkstack/backend-api";
|
|
4
|
+
import {
|
|
5
|
+
IncidentApi,
|
|
6
|
+
incidentAccess,
|
|
7
|
+
pluginMetadata,
|
|
8
|
+
} from "@checkstack/incident-common";
|
|
9
|
+
import type { AiProposalPreview } from "@checkstack/ai-common";
|
|
10
|
+
import type { RegisteredAiTool } from "@checkstack/ai-backend";
|
|
11
|
+
|
|
12
|
+
/** Input for `incident.deleteUpdate`: the update id + its owning incident. */
|
|
13
|
+
export const IncidentDeleteUpdateInputSchema = z.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
incidentId: z.string(),
|
|
16
|
+
});
|
|
17
|
+
export type IncidentDeleteUpdateInput = z.infer<
|
|
18
|
+
typeof IncidentDeleteUpdateInputSchema
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
/** Output returned once a human applies the update removal. */
|
|
22
|
+
export interface IncidentDeleteUpdateApplyResult {
|
|
23
|
+
id: string;
|
|
24
|
+
removed: true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `incident.deleteUpdate` - delete a published status update by id.
|
|
29
|
+
*
|
|
30
|
+
* `effect: "destructive"` - deleting an update is irreversible, so it ALWAYS
|
|
31
|
+
* routes through the propose/apply confirm card in BOTH permission modes (it can
|
|
32
|
+
* never auto-apply). `dryRun` returns the captured payload for human review
|
|
33
|
+
* WITHOUT mutating; `execute` (reached only via `apply`) deletes the update. The
|
|
34
|
+
* underlying RPC uses the USER-SCOPED client passed at call time, so
|
|
35
|
+
* handler-side authorization is enforced exactly as a direct UI/RPC call.
|
|
36
|
+
*/
|
|
37
|
+
export function createIncidentDeleteUpdateTool(): RegisteredAiTool<
|
|
38
|
+
IncidentDeleteUpdateInput,
|
|
39
|
+
IncidentDeleteUpdateApplyResult
|
|
40
|
+
> {
|
|
41
|
+
const dryRun = async ({
|
|
42
|
+
input,
|
|
43
|
+
}: {
|
|
44
|
+
input: IncidentDeleteUpdateInput;
|
|
45
|
+
principal: AuthUser;
|
|
46
|
+
rpcClient: RpcClient;
|
|
47
|
+
}): Promise<AiProposalPreview<IncidentDeleteUpdateInput>> => {
|
|
48
|
+
return {
|
|
49
|
+
summary: `Delete update ${input.id} from incident ${input.incidentId}. This is permanent.`,
|
|
50
|
+
payload: { id: input.id, incidentId: input.incidentId },
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: "incident.deleteUpdate",
|
|
56
|
+
description:
|
|
57
|
+
"Delete a published status update from an incident by update id and its owning incidentId. DESTRUCTIVE and irreversible. Never deletes directly; a person must approve the confirmation. Find the update id and incidentId by reading the incident's details first.",
|
|
58
|
+
effect: "destructive",
|
|
59
|
+
input: IncidentDeleteUpdateInputSchema,
|
|
60
|
+
requiredAccessRules: [
|
|
61
|
+
qualifyAccessRuleId(pluginMetadata, incidentAccess.incident.manage),
|
|
62
|
+
],
|
|
63
|
+
dryRun,
|
|
64
|
+
async execute({ input, rpcClient }) {
|
|
65
|
+
const incidentClient = rpcClient.forPlugin(IncidentApi);
|
|
66
|
+
await incidentClient.deleteUpdate({
|
|
67
|
+
id: input.id,
|
|
68
|
+
incidentId: input.incidentId,
|
|
69
|
+
});
|
|
70
|
+
return { id: input.id, removed: true };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -6,6 +6,7 @@ import { createIncidentAddUpdateTool } from "./incident-add-update";
|
|
|
6
6
|
import { createIncidentResolveTool } from "./incident-resolve";
|
|
7
7
|
import { createIncidentAddLinkTool } from "./incident-add-link";
|
|
8
8
|
import { createIncidentRemoveLinkTool } from "./incident-remove-link";
|
|
9
|
+
import { createIncidentDeleteUpdateTool } from "./incident-delete-update";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The incident plugin's AI tools, registered into the AI registry via
|
|
@@ -29,5 +30,6 @@ export function buildIncidentAiTools(): RegisteredAiTool[] {
|
|
|
29
30
|
createIncidentResolveTool(),
|
|
30
31
|
createIncidentAddLinkTool(),
|
|
31
32
|
createIncidentRemoveLinkTool(),
|
|
33
|
+
createIncidentDeleteUpdateTool(),
|
|
32
34
|
];
|
|
33
35
|
}
|
package/src/automations.test.ts
CHANGED
|
@@ -324,6 +324,76 @@ describe("incident automation actions", () => {
|
|
|
324
324
|
expect(service.resolveIncident).toHaveBeenCalledWith("INC-1", "Fixed");
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
// SLO-1 regression: an automation-driven resolve mutates the incident WITHOUT
|
|
328
|
+
// going through the RPC router, so it must emit `incident.lifecycle.changed`
|
|
329
|
+
// itself — otherwise an override-bearing incident resolved by an automation
|
|
330
|
+
// never triggers SLO downtime reconciliation and its incident-sourced event
|
|
331
|
+
// leaks open.
|
|
332
|
+
it("emits incident.lifecycle.changed on an automation resolve (SLO downtime reconcile)", async () => {
|
|
333
|
+
const resolved = {
|
|
334
|
+
id: "INC-1",
|
|
335
|
+
status: "resolved",
|
|
336
|
+
severity: "critical",
|
|
337
|
+
systemIds: ["sys-1", "sys-2"],
|
|
338
|
+
};
|
|
339
|
+
const service = makeServiceStub({
|
|
340
|
+
resolveIncident: mock(
|
|
341
|
+
async () => resolved,
|
|
342
|
+
) as unknown as IncidentService["resolveIncident"],
|
|
343
|
+
});
|
|
344
|
+
const emit = mock(async () => {});
|
|
345
|
+
const eventBus = { emit } as unknown as Parameters<
|
|
346
|
+
typeof createIncidentActions
|
|
347
|
+
>[0]["eventBus"];
|
|
348
|
+
const resolveAction = createIncidentActions({ service, eventBus })[1];
|
|
349
|
+
|
|
350
|
+
await resolveAction.execute({
|
|
351
|
+
...actionContext,
|
|
352
|
+
config: { incidentId: "INC-1", message: "Fixed" } as never,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(emit).toHaveBeenCalledTimes(1);
|
|
356
|
+
const emitArgs = emit.mock.calls[0] as unknown[] | undefined;
|
|
357
|
+
const hook = emitArgs?.[0] as { id?: string } | undefined;
|
|
358
|
+
const payload = emitArgs?.[1] as
|
|
359
|
+
| { systemIds?: string[]; action?: string }
|
|
360
|
+
| undefined;
|
|
361
|
+
expect(hook?.id).toBe("incident.lifecycle.changed");
|
|
362
|
+
expect(payload?.action).toBe("resolved");
|
|
363
|
+
expect(payload?.systemIds).toEqual(["sys-1", "sys-2"]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("emits incident.lifecycle.changed on an automation update_status → resolved", async () => {
|
|
367
|
+
const update = {
|
|
368
|
+
id: "upd-3",
|
|
369
|
+
incidentId: "INC-1",
|
|
370
|
+
message: "Status changed to resolved",
|
|
371
|
+
createdAt: new Date(),
|
|
372
|
+
};
|
|
373
|
+
const service = makeServiceStub({
|
|
374
|
+
addUpdate: mock(
|
|
375
|
+
async () => update,
|
|
376
|
+
) as unknown as IncidentService["addUpdate"],
|
|
377
|
+
});
|
|
378
|
+
const emit = mock(async () => {});
|
|
379
|
+
const eventBus = { emit } as unknown as Parameters<
|
|
380
|
+
typeof createIncidentActions
|
|
381
|
+
>[0]["eventBus"];
|
|
382
|
+
const updateStatusAction = createIncidentActions({ service, eventBus })[3];
|
|
383
|
+
|
|
384
|
+
await updateStatusAction.execute({
|
|
385
|
+
...actionContext,
|
|
386
|
+
config: { incidentId: "INC-1", status: "resolved" } as never,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(emit).toHaveBeenCalledTimes(1);
|
|
390
|
+
const payload = (emit.mock.calls[0] as unknown[] | undefined)?.[1] as
|
|
391
|
+
| { systemIds?: string[]; action?: string }
|
|
392
|
+
| undefined;
|
|
393
|
+
expect(payload?.action).toBe("resolved");
|
|
394
|
+
expect(payload?.systemIds).toEqual(["sys-1"]);
|
|
395
|
+
});
|
|
396
|
+
|
|
327
397
|
// 6(b) regression: an action-driven resolve must route through the reactive
|
|
328
398
|
// entity (like the RPC router) so it appends an `entity_transitions` row,
|
|
329
399
|
// emits `ENTITY_CHANGED` (waking `wait_until`), and fires the
|