@checkstack/incident-backend 1.1.4 → 1.1.5
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 +93 -0
- package/package.json +11 -11
- package/src/router.ts +2 -0
- package/src/service.test.ts +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# @checkstack/incident-backend
|
|
2
2
|
|
|
3
|
+
## 1.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f23f3c9: Phase 12 of the v1 polishing plan: three coordinated cleanup items that
|
|
8
|
+
close out half-finished features ahead of v1.0.
|
|
9
|
+
|
|
10
|
+
`@checkstack/incident-backend` adds focused unit-test coverage for
|
|
11
|
+
`IncidentService.hasActiveIncidentWithSuppression` in
|
|
12
|
+
`core/incident-backend/src/service.test.ts`. The new tests exercise the
|
|
13
|
+
real query-builder logic against a programmable mock data source and
|
|
14
|
+
pin down the active-only silencing contract: returns `true` only when
|
|
15
|
+
an unresolved incident with `suppressNotifications=true` is associated
|
|
16
|
+
with the queried `systemId`; returns `false` for resolved incidents,
|
|
17
|
+
incidents with `suppressNotifications=false`, systems with no incident
|
|
18
|
+
associations, and other systems' silenced incidents. No runtime
|
|
19
|
+
changes; the service code was already correct end-to-end (write path
|
|
20
|
+
through `IncidentEditor`, read path through the healthcheck queue
|
|
21
|
+
executor and dependency notifications). A companion docs page,
|
|
22
|
+
`docs/src/content/docs/architecture/alert-silencing.md`, documents the
|
|
23
|
+
contract, the two read sites, and the dispatch paths silencing does
|
|
24
|
+
NOT cover so users aren't surprised when an unaware channel keeps
|
|
25
|
+
firing.
|
|
26
|
+
|
|
27
|
+
`@checkstack/auth-frontend` surfaces inline role assignment inside the
|
|
28
|
+
user-creation dialog so admins can pick role(s) atomically with the
|
|
29
|
+
create call. `CreateUserDialog` now renders a checkbox list of
|
|
30
|
+
assignable roles (those with `isAssignable !== false`); on submit,
|
|
31
|
+
`UsersTab` awaits `createCredentialUser`, then immediately calls
|
|
32
|
+
`updateUserRoles` with the selected role IDs. On partial failure
|
|
33
|
+
(user created, role assignment failed) the UI surfaces a warning toast
|
|
34
|
+
naming the recovery path rather than silently misreporting success. No
|
|
35
|
+
new endpoints — reuses the existing `createCredentialUser` +
|
|
36
|
+
`updateUserRoles` contract pair. A companion docs page,
|
|
37
|
+
`docs/src/content/docs/architecture/users-and-teams.md`, documents the
|
|
38
|
+
identity / role / team model, the two S2S endpoints
|
|
39
|
+
(`checkResourceTeamAccess`, `getAccessibleResourceIds`) other plugins
|
|
40
|
+
should call to honour team grants, and explicitly defers audit
|
|
41
|
+
logging, CSV export, team-scoped resource-management UI, and deletion
|
|
42
|
+
side-effect handling to v1.1.
|
|
43
|
+
|
|
44
|
+
The third item — deleting the empty `core/status-frontend/` and
|
|
45
|
+
`core/status-page-backend/` shells — is tooling-only and intentionally
|
|
46
|
+
ships without a changeset; neither shell had a `package.json`, source
|
|
47
|
+
file, or downstream importer.
|
|
48
|
+
|
|
49
|
+
- f23f3c9: Add `correlationMiddleware` to `@checkstack/backend-api` and apply it
|
|
50
|
+
to every plugin/core router so each request carries a stable
|
|
51
|
+
`x-correlation-id` (read from the inbound header, or freshly minted
|
|
52
|
+
via `crypto.randomUUID()` when absent) and an auto-injected child
|
|
53
|
+
logger bound with `{ correlationId, pluginId, userId? }`. The ID is
|
|
54
|
+
echoed back on the response header so the caller can correlate their
|
|
55
|
+
client-side trace to the server logs.
|
|
56
|
+
|
|
57
|
+
The `Logger` interface in `@checkstack/backend-api` now formally
|
|
58
|
+
documents the structured-metadata convention (`logger.info("msg",
|
|
59
|
+
{ ...meta })`) alongside the long-standing varargs shape. Winston's
|
|
60
|
+
splat handling already routes both shapes through the same vararg
|
|
61
|
+
slot, so existing call sites are unaffected. A new optional
|
|
62
|
+
`Logger.child(meta)` method captures the metadata-binding contract the
|
|
63
|
+
new middleware relies on; production loggers always implement it,
|
|
64
|
+
minimal test mocks may omit it (the middleware falls back gracefully).
|
|
65
|
+
|
|
66
|
+
`RpcContext` grew two optional `Headers` bags, `requestHeaders` and
|
|
67
|
+
`responseHeaders`, populated by the outer Hono `/api/*` and `/rest/*`
|
|
68
|
+
handlers in `@checkstack/backend`. They are write-through observation
|
|
69
|
+
points for middleware; an `RpcContext` constructed without them (S2S
|
|
70
|
+
clients, tests) keeps working — the echo is a silent no-op and the ID
|
|
71
|
+
is still bound onto the child logger for server-side correlation.
|
|
72
|
+
|
|
73
|
+
The scaffolding template in `@checkstack/scripts` was updated so any
|
|
74
|
+
new plugin generated via `bun run create` wires the middleware in the
|
|
75
|
+
expected `.use(correlationMiddleware).use(autoAuthMiddleware)` order
|
|
76
|
+
out of the box.
|
|
77
|
+
|
|
78
|
+
- Updated dependencies [f23f3c9]
|
|
79
|
+
- Updated dependencies [f23f3c9]
|
|
80
|
+
- Updated dependencies [f23f3c9]
|
|
81
|
+
- Updated dependencies [f23f3c9]
|
|
82
|
+
- @checkstack/common@0.11.0
|
|
83
|
+
- @checkstack/backend-api@0.17.0
|
|
84
|
+
- @checkstack/catalog-backend@1.1.5
|
|
85
|
+
- @checkstack/command-backend@0.1.29
|
|
86
|
+
- @checkstack/integration-backend@0.1.29
|
|
87
|
+
- @checkstack/notification-common@1.2.0
|
|
88
|
+
- @checkstack/integration-common@0.5.0
|
|
89
|
+
- @checkstack/auth-common@0.7.1
|
|
90
|
+
- @checkstack/catalog-common@2.2.2
|
|
91
|
+
- @checkstack/incident-common@1.2.2
|
|
92
|
+
- @checkstack/signal-common@0.2.4
|
|
93
|
+
- @checkstack/cache-api@0.3.4
|
|
94
|
+
- @checkstack/cache-utils@0.2.9
|
|
95
|
+
|
|
3
96
|
## 1.1.4
|
|
4
97
|
|
|
5
98
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/incident-backend",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/backend-api": "0.
|
|
18
|
-
"@checkstack/cache-api": "0.3.
|
|
19
|
-
"@checkstack/cache-utils": "0.2.
|
|
20
|
-
"@checkstack/incident-common": "1.2.
|
|
21
|
-
"@checkstack/catalog-common": "2.2.
|
|
22
|
-
"@checkstack/catalog-backend": "1.1.
|
|
23
|
-
"@checkstack/notification-common": "1.1.
|
|
17
|
+
"@checkstack/backend-api": "0.16.0",
|
|
18
|
+
"@checkstack/cache-api": "0.3.3",
|
|
19
|
+
"@checkstack/cache-utils": "0.2.8",
|
|
20
|
+
"@checkstack/incident-common": "1.2.1",
|
|
21
|
+
"@checkstack/catalog-common": "2.2.1",
|
|
22
|
+
"@checkstack/catalog-backend": "1.1.4",
|
|
23
|
+
"@checkstack/notification-common": "1.1.1",
|
|
24
24
|
"@checkstack/auth-common": "0.7.0",
|
|
25
|
-
"@checkstack/command-backend": "0.1.
|
|
25
|
+
"@checkstack/command-backend": "0.1.28",
|
|
26
26
|
"@checkstack/signal-common": "0.2.3",
|
|
27
|
-
"@checkstack/integration-backend": "0.1.
|
|
27
|
+
"@checkstack/integration-backend": "0.1.28",
|
|
28
28
|
"@checkstack/integration-common": "0.4.0",
|
|
29
29
|
"@checkstack/common": "0.10.0",
|
|
30
30
|
"drizzle-orm": "^0.45.0",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
36
36
|
"@checkstack/scripts": "0.3.2",
|
|
37
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
37
|
+
"@checkstack/test-utils-backend": "0.1.28",
|
|
38
38
|
"@checkstack/tsconfig": "0.0.7",
|
|
39
39
|
"@types/bun": "^1.0.0",
|
|
40
40
|
"drizzle-kit": "^0.31.10",
|
package/src/router.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@checkstack/incident-common";
|
|
6
6
|
import {
|
|
7
7
|
autoAuthMiddleware,
|
|
8
|
+
correlationMiddleware,
|
|
8
9
|
Logger,
|
|
9
10
|
type RpcContext,
|
|
10
11
|
} from "@checkstack/backend-api";
|
|
@@ -89,6 +90,7 @@ export function createRouter(
|
|
|
89
90
|
|
|
90
91
|
const os = implement(incidentContract)
|
|
91
92
|
.$context<RpcContext>()
|
|
93
|
+
.use(correlationMiddleware)
|
|
92
94
|
.use(autoAuthMiddleware);
|
|
93
95
|
|
|
94
96
|
return os.router({
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { IncidentService } from "./service";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Programmable mock DB that records each `select(...).from(...).where(...)`
|
|
6
|
+
* (and optional `.limit(...)`) chain and returns a configurable row array
|
|
7
|
+
* per invocation. Tests exercise the real query-builder calls inside
|
|
8
|
+
* `IncidentService`, only swapping out the terminal data source.
|
|
9
|
+
*/
|
|
10
|
+
function createProgrammableSelectDb(resultsByCall: unknown[][]) {
|
|
11
|
+
let callIndex = 0;
|
|
12
|
+
|
|
13
|
+
const nextResult = (): unknown[] => {
|
|
14
|
+
const result = resultsByCall[callIndex] ?? [];
|
|
15
|
+
callIndex += 1;
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const select = mock((projection?: Record<string, unknown>) => {
|
|
20
|
+
void projection;
|
|
21
|
+
const rows = nextResult();
|
|
22
|
+
|
|
23
|
+
const limit = mock(() => Promise.resolve(rows));
|
|
24
|
+
const whereResult = Object.assign(Promise.resolve(rows), { limit });
|
|
25
|
+
const where = mock(() => whereResult);
|
|
26
|
+
const fromResult = Object.assign(Promise.resolve(rows), { where });
|
|
27
|
+
const from = mock(() => fromResult);
|
|
28
|
+
|
|
29
|
+
return { from };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
db: { select } as unknown,
|
|
34
|
+
select,
|
|
35
|
+
getCallCount: () => callIndex,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("IncidentService.hasActiveIncidentWithSuppression", () => {
|
|
40
|
+
let dbHelper: ReturnType<typeof createProgrammableSelectDb>;
|
|
41
|
+
let service: IncidentService;
|
|
42
|
+
|
|
43
|
+
const setup = (resultsByCall: unknown[][]) => {
|
|
44
|
+
dbHelper = createProgrammableSelectDb(resultsByCall);
|
|
45
|
+
service = new IncidentService(dbHelper.db as never);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
dbHelper = createProgrammableSelectDb([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns true when an active incident with suppressNotifications=true exists for the system", async () => {
|
|
53
|
+
setup([
|
|
54
|
+
// 1st query: incidentSystems lookup for systemId="sys-1"
|
|
55
|
+
[{ incidentId: "inc-1" }],
|
|
56
|
+
// 2nd query: incidents lookup with .where(active AND suppression).limit(1)
|
|
57
|
+
[{ id: "inc-1" }],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const result = await service.hasActiveIncidentWithSuppression("sys-1");
|
|
61
|
+
|
|
62
|
+
expect(result).toBe(true);
|
|
63
|
+
expect(dbHelper.getCallCount()).toBe(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false when no incidents are associated with the system", async () => {
|
|
67
|
+
setup([
|
|
68
|
+
// 1st query: empty -> short-circuits before the 2nd query
|
|
69
|
+
[],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const result = await service.hasActiveIncidentWithSuppression("sys-1");
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(false);
|
|
75
|
+
// Only one query should have run; the incidents lookup is skipped.
|
|
76
|
+
expect(dbHelper.getCallCount()).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns false when the matching incident is resolved (silencing is scoped to active incidents)", async () => {
|
|
80
|
+
setup([
|
|
81
|
+
// 1st query: the system has an incident association.
|
|
82
|
+
[{ incidentId: "inc-resolved" }],
|
|
83
|
+
// 2nd query: the WHERE clause filters out resolved incidents, so the
|
|
84
|
+
// limit(1) projection finds nothing. The real query builder enforces
|
|
85
|
+
// this via `ne(incidents.status, "resolved")`.
|
|
86
|
+
[],
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const result = await service.hasActiveIncidentWithSuppression("sys-1");
|
|
90
|
+
|
|
91
|
+
expect(result).toBe(false);
|
|
92
|
+
expect(dbHelper.getCallCount()).toBe(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns false when the matching incident has suppressNotifications=false", async () => {
|
|
96
|
+
setup([
|
|
97
|
+
// 1st query: the system has an incident association.
|
|
98
|
+
[{ incidentId: "inc-no-suppress" }],
|
|
99
|
+
// 2nd query: the WHERE clause filters by suppressNotifications=true,
|
|
100
|
+
// so a row with suppressNotifications=false is excluded — the result
|
|
101
|
+
// set is empty.
|
|
102
|
+
[],
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const result = await service.hasActiveIncidentWithSuppression("sys-1");
|
|
106
|
+
|
|
107
|
+
expect(result).toBe(false);
|
|
108
|
+
expect(dbHelper.getCallCount()).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("filters by systemId — does not return true for another system's silenced incident", async () => {
|
|
112
|
+
// The systemId filter is enforced by the WHERE clause on the
|
|
113
|
+
// incidentSystems lookup. Querying "sys-other" returns an empty
|
|
114
|
+
// association set even though "sys-1" has a silenced incident, so the
|
|
115
|
+
// method short-circuits to false.
|
|
116
|
+
setup([
|
|
117
|
+
// 1st query for systemId="sys-other": no associations.
|
|
118
|
+
[],
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const result = await service.hasActiveIncidentWithSuppression("sys-other");
|
|
122
|
+
|
|
123
|
+
expect(result).toBe(false);
|
|
124
|
+
expect(dbHelper.getCallCount()).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
});
|