@volter/twin-sentry 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +142 -0
  3. package/client/sentry-mirror.css +99 -0
  4. package/client/sentry-mirror.tsx +352 -0
  5. package/dist/client/sentry-mirror.bundle.js +321 -0
  6. package/dist/client/sentry-mirror.css +99 -0
  7. package/dist/client/sentry-mirror.d.ts +17 -0
  8. package/dist/client/sentry-mirror.js +156 -0
  9. package/dist/client/sentry-mirror.tsx +352 -0
  10. package/dist/src/cli.d.ts +2 -0
  11. package/dist/src/cli.js +36 -0
  12. package/dist/src/index.d.ts +15 -0
  13. package/dist/src/index.js +75 -0
  14. package/dist/src/sentry-budget.d.ts +50 -0
  15. package/dist/src/sentry-budget.js +145 -0
  16. package/dist/src/sentry-capabilities.d.ts +3 -0
  17. package/dist/src/sentry-capabilities.js +1180 -0
  18. package/dist/src/sentry-conformance.d.ts +22 -0
  19. package/dist/src/sentry-conformance.js +97 -0
  20. package/dist/src/sentry-connector.d.ts +77 -0
  21. package/dist/src/sentry-connector.js +226 -0
  22. package/dist/src/sentry-events.d.ts +54 -0
  23. package/dist/src/sentry-events.js +131 -0
  24. package/dist/src/sentry-ingest.d.ts +137 -0
  25. package/dist/src/sentry-ingest.js +387 -0
  26. package/dist/src/sentry-mirror-ui.d.ts +38 -0
  27. package/dist/src/sentry-mirror-ui.js +162 -0
  28. package/dist/src/sentry-perform-harness.d.ts +9 -0
  29. package/dist/src/sentry-perform-harness.js +20 -0
  30. package/dist/src/sentry-server.d.ts +14 -0
  31. package/dist/src/sentry-server.js +27 -0
  32. package/dist/src/sentry-twin.d.ts +17 -0
  33. package/dist/src/sentry-twin.js +1739 -0
  34. package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
  35. package/dist/test-fixtures/sentry-openapi-operations.json +1837 -0
  36. package/package.json +75 -0
  37. package/src/cli.ts +34 -0
  38. package/src/index.ts +145 -0
  39. package/src/sentry-budget.ts +171 -0
  40. package/src/sentry-capabilities.ts +1222 -0
  41. package/src/sentry-conformance.ts +110 -0
  42. package/src/sentry-connector.ts +260 -0
  43. package/src/sentry-events.ts +171 -0
  44. package/src/sentry-ingest.ts +471 -0
  45. package/src/sentry-mirror-ui.ts +162 -0
  46. package/src/sentry-perform-harness.ts +19 -0
  47. package/src/sentry-server.ts +35 -0
  48. package/src/sentry-twin.ts +1615 -0
  49. package/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
  50. package/test-fixtures/sentry-openapi-operations.json +1837 -0
@@ -0,0 +1,1180 @@
1
+ // Sentry capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
2
+ // top-down from what Sentry actually does — NOT from what this twin has built. This is the
3
+ // honest denominator: every entry's verify() (required to count as done) drives REAL
4
+ // requests through the twin and asserts status/shape; `expected:'done'` only on what we
5
+ // genuinely claim, so a broken one shows as a regression. Grow this toward Sentry's full
6
+ // surface every cycle.
7
+ import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { createElement } from 'react';
11
+ import { renderToStaticMarkup } from 'react-dom/server';
12
+ import { checkCapabilities, uiDataCoupled, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
13
+ import { DashboardsView, DiscoverView, ReleasesView, ProjectsView } from "../client/sentry-mirror.js";
14
+ import { handleSentryTwinRequest } from "./sentry-twin.js";
15
+ import { DEFAULT_ORG, DEFAULT_PROJECT, DEFAULT_PROJECT_ID, DEFAULT_PUBLIC_KEY } from "./sentry-ingest.js";
16
+ import { registerAlertRule, emitIssueAlert, verifySentrySignature, computeSentrySignature } from "./sentry-events.js";
17
+ import { mapIssue, sentryRequestForAction, syncSentryFromReal } from "./sentry-connector.js";
18
+ import { performPending } from "./sentry-perform-harness.js";
19
+ import { buildSentryMirrorClient, latestStacktrace, eventTags, levelTone, statusTone, formatCount, timeAgo } from "./sentry-mirror-ui.js";
20
+ const OCC = '2026-01-01T00:00:00.000Z';
21
+ /** Run a sequence of real Sentry requests against an isolated root; returns a tester. */
22
+ async function withRoot(steps) {
23
+ const root = mkdtempSync(join(tmpdir(), 'sentry-cap-'));
24
+ const h = (s) => handleSentryTwinRequest({
25
+ method: s.m, path: s.p,
26
+ body: s.b === undefined ? undefined : (typeof s.b === 'string' ? s.b : JSON.stringify(s.b)),
27
+ root, occurredAt: OCC,
28
+ headers: s.auth ? { 'x-sentry-auth': s.auth } : {},
29
+ });
30
+ try {
31
+ return await verifyBoundary('sentry.withRoot', () => steps(h, root));
32
+ }
33
+ finally {
34
+ rmSync(root, { recursive: true, force: true });
35
+ }
36
+ }
37
+ const ok = (r) => r.status >= 200 && r.status < 300;
38
+ const auth = `Sentry sentry_key=${DEFAULT_PUBLIC_KEY}`;
39
+ const ingest = (event) => ({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/store/`, b: event, auth });
40
+ const envelope = (event) => ({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/envelope/`, b: `{"dsn":"http://${DEFAULT_PUBLIC_KEY}@localhost/${DEFAULT_PROJECT_ID}"}\n{"type":"event"}\n${JSON.stringify(event)}` });
41
+ const issuesPath = `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/issues/`;
42
+ // ── UI verify: the built mirror bundle must contain the screen's load-bearing markers ──
43
+ let bundle = null;
44
+ const mirrorBundle = () => (bundle ??= buildSentryMirrorClient());
45
+ // Count occurrences of a DOM landmark class (e.g. one `dashboard-row` per dashboard) in
46
+ // rendered mirror markup — the structural assertion used by the rung-5 UI verifies below.
47
+ const countLandmark = (markup, landmark) => markup.split(landmark).length - 1;
48
+ // shorthands
49
+ const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
50
+ const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
51
+ export const SENTRY_CAPABILITIES = [
52
+ // ── INGESTION (the SDK side — the core) ─────────────────────────────────────────
53
+ done('sentry.ingest.store_exception', 'ingestion', 'POST /api/:id/store/ — capture an exception event', 'api', 'core', () => withRoot(async (h) => {
54
+ const r = await h(ingest({ exception: { values: [{ type: 'TypeError', value: 'boom' }] } }));
55
+ return ok(r) && typeof r.body.id === 'string' && r.body.id.length === 32;
56
+ })),
57
+ done('sentry.ingest.envelope', 'ingestion', 'POST /api/:id/envelope/ — capture via the modern envelope transport', 'api', 'core', () => withRoot(async (h) => {
58
+ const r = await h(envelope({ exception: { values: [{ type: 'RangeError', value: 'nope' }] } }));
59
+ return ok(r) && typeof r.body.id === 'string';
60
+ })),
61
+ done('sentry.ingest.message', 'ingestion', 'Capture a message event (no exception)', 'api', 'core', () => withRoot(async (h) => {
62
+ const r = await h(ingest({ message: 'something happened', level: 'warning' }));
63
+ if (!ok(r))
64
+ return false;
65
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
66
+ const issues = list.body;
67
+ return issues.length === 1 && issues[0].title === 'something happened' && issues[0].level === 'warning';
68
+ })),
69
+ done('sentry.ingest.grouping', 'ingestion', 'Two identical exceptions GROUP into one issue (count increments)', 'api', 'core', () => withRoot(async (h) => {
70
+ const ev = { exception: { values: [{ type: 'TypeError', value: 'x is undefined' }] } };
71
+ const a = await h(ingest(ev));
72
+ const b = await h(ingest(ev));
73
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
74
+ const issues = list.body;
75
+ // distinct event ids, one issue, count 2, first/last seen set
76
+ return a.body.id !== b.body.id && issues.length === 1 &&
77
+ issues[0].count === '2' && !!issues[0].firstSeen && !!issues[0].lastSeen;
78
+ })),
79
+ done('sentry.ingest.grouping_distinct', 'ingestion', 'Different exceptions land in DIFFERENT issues', 'api', 'core', () => withRoot(async (h) => {
80
+ await h(ingest({ exception: { values: [{ type: 'TypeError', value: 'a' }] } }));
81
+ await h(ingest({ exception: { values: [{ type: 'ReferenceError', value: 'b' }] } }));
82
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
83
+ return list.body.length === 2;
84
+ })),
85
+ done('sentry.ingest.grouping_normalized', 'ingestion', 'Numbers in the message are normalized so similar errors group', 'api', 'common', () => withRoot(async (h) => {
86
+ await h(ingest({ exception: { values: [{ type: 'Error', value: 'user 42 not found' }] } }));
87
+ await h(ingest({ exception: { values: [{ type: 'Error', value: 'user 99 not found' }] } }));
88
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
89
+ return list.body.length === 1 && list.body[0].count === '2';
90
+ })),
91
+ done('sentry.ingest.auth_required', 'ingestion', 'An unknown/missing DSN public key is rejected 401', 'api', 'core', () => withRoot(async (h) => {
92
+ const noKey = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/store/`, b: { message: 'x' } });
93
+ const badKey = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/store/`, b: { message: 'x' }, auth: 'Sentry sentry_key=wrong' });
94
+ return noKey.status === 401 && badKey.status === 401;
95
+ })),
96
+ done('sentry.ingest.auth_query', 'ingestion', 'DSN public key accepted via ?sentry_key query param', 'api', 'common', () => withRoot(async (h) => {
97
+ // a wrong key via the SAME query-param path must be rejected — proves the param is actually read/checked.
98
+ const wrong = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/store/?sentry_key=wrongkey`, b: { message: 'q' } });
99
+ if (wrong.status !== 401)
100
+ return false;
101
+ const r = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/store/?sentry_key=${DEFAULT_PUBLIC_KEY}`, b: { message: 'q' } });
102
+ if (!ok(r))
103
+ return false;
104
+ const rid = r.body.id;
105
+ // a real 32-hex event id, and the event must actually be persisted + retrievable with matching content.
106
+ if (typeof rid !== 'string' || !/^[0-9a-f]{32}$/.test(rid))
107
+ return false;
108
+ const got = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${rid}/` });
109
+ return ok(got) && got.body.eventID === rid && got.body.message === 'q';
110
+ })),
111
+ done('sentry.ingest.project_mismatch', 'ingestion', 'A key for a different project is rejected 401', 'api', 'common', () => withRoot(async (h) => {
112
+ const r = await h({ m: 'POST', p: `/api/999/store/`, b: { message: 'x' }, auth });
113
+ return r.status === 401;
114
+ })),
115
+ done('sentry.ingest.regression', 'ingestion', 'A new event on a resolved issue REOPENS it (regression)', 'api', 'core', () => withRoot(async (h) => {
116
+ const ev = { exception: { values: [{ type: 'Error', value: 'flap' }] } };
117
+ await h(ingest(ev));
118
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
119
+ const id = list.body[0].id;
120
+ await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
121
+ const resolved = await h({ m: 'GET', p: `/api/0/issues/${id}/` });
122
+ await h(ingest(ev)); // reopen
123
+ const reopened = await h({ m: 'GET', p: `/api/0/issues/${id}/` });
124
+ return resolved.body.status === 'resolved' && reopened.body.status === 'unresolved' && reopened.body.count === '2';
125
+ })),
126
+ done('sentry.ingest.culprit', 'ingestion', 'Culprit derived from the top in-app stack frame', 'api', 'common', () => withRoot(async (h) => {
127
+ await h(ingest({ exception: { values: [{ type: 'Error', value: 'z', stacktrace: { frames: [{ function: 'outer', in_app: false }, { function: 'handler', module: 'app/server', in_app: true }] } }] } }));
128
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
129
+ return list.body[0].culprit === 'handler(app/server)';
130
+ })),
131
+ done('sentry.ingest.envelope_session_ignored', 'ingestion', 'A session-only envelope is accepted (200) without creating an issue', 'api', 'niche', () => withRoot(async (h) => {
132
+ const r = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/envelope/`, b: `{"dsn":"http://${DEFAULT_PUBLIC_KEY}@localhost/${DEFAULT_PROJECT_ID}"}\n{"type":"session"}\n{"sid":"abc"}` });
133
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
134
+ return ok(r) && list.body.length === 0;
135
+ })),
136
+ // ── ISSUES (the Web API dashboard side) ─────────────────────────────────────────
137
+ done('sentry.issues.list', 'issues', 'GET project issues list', 'api', 'core', () => withRoot(async (h) => {
138
+ await h(ingest({ message: 'm1' }));
139
+ const r = await h({ m: 'GET', p: `${issuesPath}?query=` });
140
+ return ok(r) && Array.isArray(r.body) && r.body.length === 1;
141
+ })),
142
+ done('sentry.issues.retrieve', 'issues', 'GET /issues/:id/', 'api', 'core', () => withRoot(async (h) => {
143
+ await h(ingest({ message: 'r1' }));
144
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
145
+ const r = await h({ m: 'GET', p: `/api/0/issues/${id}/` });
146
+ return ok(r) && r.body.id === id && r.body.title === 'r1';
147
+ })),
148
+ done('sentry.issues.resolve', 'issues', 'PUT /issues/:id/ status=resolved', 'api', 'core', () => withRoot(async (h) => {
149
+ await h(ingest({ message: 'res' }));
150
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
151
+ const r = await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
152
+ return ok(r) && r.body.status === 'resolved';
153
+ })),
154
+ done('sentry.issues.ignore', 'issues', 'PUT /issues/:id/ status=ignored (muted)', 'api', 'core', () => withRoot(async (h) => {
155
+ await h(ingest({ message: 'ig' }));
156
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
157
+ const r = await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'ignored' } });
158
+ const muted = await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'muted' } });
159
+ return ok(r) && r.body.status === 'ignored' && muted.body.status === 'ignored';
160
+ })),
161
+ done('sentry.issues.unresolve', 'issues', 'PUT /issues/:id/ status=unresolved', 'api', 'common', () => withRoot(async (h) => {
162
+ await h(ingest({ message: 'un' }));
163
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
164
+ await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
165
+ const r = await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'unresolved' } });
166
+ return ok(r) && r.body.status === 'unresolved';
167
+ })),
168
+ done('sentry.issues.assign', 'issues', 'PUT /issues/:id/ assignedTo', 'api', 'common', () => withRoot(async (h) => {
169
+ await h(ingest({ message: 'as' }));
170
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
171
+ const r = await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { assignedTo: 'user:ada' } });
172
+ return ok(r) && r.body.assignedTo === 'user:ada';
173
+ })),
174
+ done('sentry.issues.delete', 'issues', 'DELETE /issues/:id/', 'api', 'common', () => withRoot(async (h) => {
175
+ await h(ingest({ message: 'del' }));
176
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
177
+ const r = await h({ m: 'DELETE', p: `/api/0/issues/${id}/` });
178
+ const gone = await h({ m: 'GET', p: `/api/0/issues/${id}/` });
179
+ return r.status === 204 && gone.status === 404;
180
+ })),
181
+ done('sentry.issues.events', 'issues', 'GET /issues/:id/events/', 'api', 'core', () => withRoot(async (h) => {
182
+ const ev = { exception: { values: [{ type: 'E', value: 'v' }] } };
183
+ await h(ingest(ev));
184
+ await h(ingest(ev));
185
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
186
+ const r = await h({ m: 'GET', p: `/api/0/issues/${id}/events/` });
187
+ return ok(r) && r.body.length === 2;
188
+ })),
189
+ done('sentry.issues.events_latest', 'issues', 'GET /issues/:id/events/latest/', 'api', 'common', () => withRoot(async (h) => {
190
+ await h(ingest({ exception: { values: [{ type: 'E', value: 'v', stacktrace: { frames: [{ function: 'f' }] } }] } }));
191
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
192
+ const r = await h({ m: 'GET', p: `/api/0/issues/${id}/events/latest/` });
193
+ return ok(r) && Array.isArray(r.body.entries);
194
+ })),
195
+ done('sentry.issues.query_is', 'issues', 'Issue search: is:resolved / is:unresolved filter', 'api', 'core', () => withRoot(async (h) => {
196
+ await h(ingest({ message: 'open' }));
197
+ await h(ingest({ message: 'closed' }));
198
+ const closedId = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body.find((i) => i.title === 'closed').id;
199
+ await h({ m: 'PUT', p: `/api/0/issues/${closedId}/`, b: { status: 'resolved' } });
200
+ const unresolved = await h({ m: 'GET', p: `${issuesPath}?query=is:unresolved` });
201
+ const resolved = await h({ m: 'GET', p: `${issuesPath}?query=is:resolved` });
202
+ return unresolved.body.length === 1 && unresolved.body[0].title === 'open' &&
203
+ resolved.body.length === 1 && resolved.body[0].title === 'closed';
204
+ })),
205
+ done('sentry.issues.query_level', 'issues', 'Issue search: level:error filter', 'api', 'common', () => withRoot(async (h) => {
206
+ await h(ingest({ message: 'e', level: 'error' }));
207
+ await h(ingest({ message: 'w', level: 'warning' }));
208
+ const r = await h({ m: 'GET', p: `${issuesPath}?query=level:warning` });
209
+ return r.body.length === 1 && r.body[0].level === 'warning';
210
+ })),
211
+ done('sentry.issues.query_text', 'issues', 'Issue search: free-text title match', 'api', 'common', () => withRoot(async (h) => {
212
+ await h(ingest({ message: 'database timeout' }));
213
+ await h(ingest({ message: 'render glitch' }));
214
+ const r = await h({ m: 'GET', p: `${issuesPath}?query=${encodeURIComponent('is:unresolved timeout')}` });
215
+ return r.body.length === 1 && r.body[0].title === 'database timeout';
216
+ })),
217
+ done('sentry.issues.sort_freq', 'issues', 'Issue list sort=freq orders by event count', 'api', 'common', () => withRoot(async (h) => {
218
+ const a = { message: 'rare' };
219
+ const b = { message: 'common' };
220
+ await h(ingest(a));
221
+ await h(ingest(b));
222
+ await h(ingest(b));
223
+ await h(ingest(b));
224
+ const r = await h({ m: 'GET', p: `${issuesPath}?query=&sort=freq` });
225
+ return r.body[0].title === 'common';
226
+ })),
227
+ done('sentry.issues.short_id', 'issues', 'Issue carries a shortId + permalink', 'api', 'common', () => withRoot(async (h) => {
228
+ await h(ingest({ message: 'sid' }));
229
+ const i = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
230
+ return typeof i[0].shortId === 'string' && typeof i[0].permalink === 'string';
231
+ })),
232
+ done('sentry.issues.metadata', 'issues', 'Issue metadata carries exception type/value', 'api', 'common', () => withRoot(async (h) => {
233
+ await h(ingest({ exception: { values: [{ type: 'KeyError', value: 'missing' }] } }));
234
+ const i = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0];
235
+ return i.metadata?.type === 'KeyError' && i.metadata?.value === 'missing';
236
+ })),
237
+ done('sentry.issues.merge', 'issues', 'Merge issues into one group (bulk PUT ?id=…&merge)', 'api', 'niche', () => withRoot(async (h) => {
238
+ // two DISTINCT errors → two issues; the busier one (3 events) is the merge primary.
239
+ await h(ingest({ exception: { values: [{ type: 'AError', value: 'a' }] } }));
240
+ const busy = { exception: { values: [{ type: 'BError', value: 'b' }] } };
241
+ await h(ingest(busy));
242
+ await h(ingest(busy));
243
+ await h(ingest(busy));
244
+ const before = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
245
+ if (before.length !== 2)
246
+ return false;
247
+ const ids = before.map((i) => i.id);
248
+ const merge = await h({ m: 'PUT', p: `${issuesPath}?${ids.map((id) => `id=${id}`).join('&')}`, b: { merge: 1 } });
249
+ if (!ok(merge))
250
+ return false;
251
+ const parent = merge.body.merge.parent;
252
+ const after = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
253
+ // one survivor; its count is the SUM (1 + 3); all 4 events now belong to it
254
+ const survivor = after.find((i) => i.id === parent);
255
+ const evs = (await h({ m: 'GET', p: `/api/0/issues/${parent}/events/` })).body;
256
+ return after.length === 1 && survivor.count === '4' && evs.length === 4;
257
+ })),
258
+ done('sentry.issues.bulk', 'issues', 'Bulk resolve/ignore via project issues PUT (?query / ?id)', 'api', 'common', () => withRoot(async (h) => {
259
+ await h(ingest({ message: 'b1' }));
260
+ await h(ingest({ message: 'b2' }));
261
+ // bulk resolve EVERYTHING matching the query …
262
+ const r = await h({ m: 'PUT', p: `${issuesPath}?query=is:unresolved`, b: { status: 'resolved' } });
263
+ if (!ok(r) || r.body.status !== 'resolved')
264
+ return false;
265
+ const unresolved = (await h({ m: 'GET', p: `${issuesPath}?query=is:unresolved` })).body;
266
+ const resolved = (await h({ m: 'GET', p: `${issuesPath}?query=is:resolved` })).body;
267
+ // … and a bulk mutation with NO selector is refused (Sentry guards against this).
268
+ const guard = await h({ m: 'PUT', p: issuesPath, b: { status: 'resolved' } });
269
+ return unresolved.length === 0 && resolved.length === 2 && guard.status === 400;
270
+ })),
271
+ done('sentry.issues.tags', 'issues', 'GET /issues/:id/tags/ and tag value distributions', 'api', 'common', () => withRoot(async (h) => {
272
+ const ev = (server) => ({ exception: { values: [{ type: 'T', value: 'v' }] }, tags: { server } });
273
+ await h(ingest(ev('web1')));
274
+ await h(ingest(ev('web1')));
275
+ await h(ingest(ev('web2')));
276
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
277
+ const all = await h({ m: 'GET', p: `/api/0/issues/${id}/tags/` });
278
+ const server = all.body.find((t) => t.key === 'server');
279
+ if (!server || server.totalValues !== 3 || server.uniqueValues !== 2)
280
+ return false;
281
+ const top = server.topValues;
282
+ if (top[0].value !== 'web1' || top[0].count !== 2)
283
+ return false;
284
+ // single-key distribution endpoint agrees; a missing key 404s
285
+ const one = await h({ m: 'GET', p: `/api/0/issues/${id}/tags/server/` });
286
+ const missing = await h({ m: 'GET', p: `/api/0/issues/${id}/tags/nope/` });
287
+ return ok(one) && one.body.key === 'server' && missing.status === 404;
288
+ })),
289
+ done('sentry.issues.hashes', 'issues', 'GET /issues/:id/hashes/ (grouping hashes)', 'api', 'niche', () => withRoot(async (h) => {
290
+ await h(ingest({ exception: { values: [{ type: 'H', value: 'x' }] } }));
291
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
292
+ const r = await h({ m: 'GET', p: `/api/0/issues/${id}/hashes/` });
293
+ const hashes = r.body;
294
+ return ok(r) && hashes.length === 1 && hashes[0].id === id && !!hashes[0].latestEvent;
295
+ })),
296
+ done('sentry.issues.activity', 'issues', 'GET /issues/:id/activity/ (triage feed from the log)', 'api', 'niche', () => withRoot(async (h) => {
297
+ await h(ingest({ message: 'act' }));
298
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
299
+ await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
300
+ const r = await h({ m: 'GET', p: `/api/0/issues/${id}/activity/` });
301
+ const feed = r.body.activity;
302
+ // newest-first: the resolve, then the first_seen
303
+ return ok(r) && feed.length === 2 && feed[0].type === 'set_resolved' && feed[feed.length - 1].type === 'first_seen';
304
+ })),
305
+ // ── EVENTS ──────────────────────────────────────────────────────────────────────
306
+ done('sentry.events.list', 'events', 'GET /projects/:org/:project/events/', 'api', 'common', () => withRoot(async (h) => {
307
+ await h(ingest({ message: 'ev1' }));
308
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/` });
309
+ return ok(r) && r.body.length === 1;
310
+ })),
311
+ done('sentry.events.retrieve', 'events', 'GET /projects/:org/:project/events/:id/', 'api', 'common', () => withRoot(async (h) => {
312
+ const e = await h(ingest({ message: 'ev2' }));
313
+ const id = e.body.id;
314
+ // the ingested id must be a REAL 32-hex event id, not undefined-matching-undefined.
315
+ if (typeof id !== 'string' || !/^[0-9a-f]{32}$/.test(id))
316
+ return false;
317
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${id}/` });
318
+ const missing = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${'0'.repeat(32)}/` });
319
+ // the retrieved event must carry the ACTUAL ingested content, and an unknown id must 404.
320
+ return ok(r) && r.body.eventID === id && r.body.message === 'ev2' && missing.status === 404;
321
+ })),
322
+ done('sentry.events.tags', 'events', 'Event carries tags + level + platform', 'api', 'common', () => withRoot(async (h) => {
323
+ const e = await h(ingest({ message: 'ev3', tags: { server: 'web1' }, platform: 'node' }));
324
+ const id = e.body.id;
325
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${id}/` });
326
+ const tags = r.body.tags;
327
+ return ok(r) && tags.some((t) => t.key === 'server' && t.value === 'web1') && r.body.platform === 'node';
328
+ })),
329
+ done('sentry.events.stacktrace', 'events', 'Event exposes the exception stacktrace entries', 'api', 'core', () => withRoot(async (h) => {
330
+ const e = await h(ingest({ exception: { values: [{ type: 'E', value: 'v', stacktrace: { frames: [{ function: 'main', filename: 'app.ts', lineno: 10 }] } }] } }));
331
+ const id = e.body.id;
332
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${id}/` });
333
+ const entries = r.body.entries;
334
+ return entries[0]?.type === 'exception' && entries[0]?.data?.values?.[0]?.stacktrace?.frames?.[0]?.function === 'main';
335
+ })),
336
+ done('sentry.events.attachments', 'events', 'Event attachments listing (envelope attachment items)', 'api', 'niche', () => withRoot(async (h) => {
337
+ // envelope carrying an event + an attachment item → the attachment lists for that event
338
+ const body = `{"dsn":"http://${DEFAULT_PUBLIC_KEY}@localhost/${DEFAULT_PROJECT_ID}"}\n{"type":"event"}\n${JSON.stringify({ exception: { values: [{ type: 'E', value: 'v' }] } })}\n{"type":"attachment","filename":"screenshot.png","content_type":"image/png","length":1234,"attachment_type":"event.attachment"}\n<binary>`;
339
+ const r = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/envelope/`, b: body });
340
+ if (!ok(r))
341
+ return false;
342
+ const eventId = r.body.id;
343
+ const list = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/${eventId}/attachments/` });
344
+ const atts = list.body;
345
+ if (atts.length !== 1 || atts[0].name !== 'screenshot.png' || atts[0].size !== 1234 || atts[0].mimetype !== 'image/png')
346
+ return false;
347
+ // a missing event 404s
348
+ const missing = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/events/deadbeef/attachments/` });
349
+ return missing.status === 404;
350
+ })),
351
+ // ── PROJECTS ──────────────────────────────────────────────────────────────────────
352
+ done('sentry.projects.list', 'projects', 'GET /projects/ (all)', 'api', 'core', () => withRoot(async (h) => {
353
+ const r = await h({ m: 'GET', p: `/api/0/projects/` });
354
+ return ok(r) && r.body.some((p) => p.slug === DEFAULT_PROJECT);
355
+ })),
356
+ done('sentry.projects.retrieve', 'projects', 'GET /projects/:org/:project/', 'api', 'core', () => withRoot(async (h) => {
357
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/` });
358
+ return ok(r) && r.body.slug === DEFAULT_PROJECT;
359
+ })),
360
+ done('sentry.projects.create', 'projects', 'POST /teams/:org/:team/projects/', 'api', 'core', () => withRoot(async (h) => {
361
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'Core' } });
362
+ const r = await h({ m: 'POST', p: `/api/0/teams/${DEFAULT_ORG}/core/projects/`, b: { name: 'New Svc', platform: 'python' } });
363
+ return r.status === 201 && r.body.slug === 'new-svc' && r.body.platform === 'python';
364
+ })),
365
+ done('sentry.projects.update', 'projects', 'PUT /projects/:org/:project/', 'api', 'common', () => withRoot(async (h) => {
366
+ const r = await h({ m: 'PUT', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/`, b: { name: 'Renamed' } });
367
+ return ok(r) && r.body.name === 'Renamed';
368
+ })),
369
+ done('sentry.projects.delete', 'projects', 'DELETE /projects/:org/:project/', 'api', 'common', () => withRoot(async (h) => {
370
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'T' } });
371
+ await h({ m: 'POST', p: `/api/0/teams/${DEFAULT_ORG}/t/projects/`, b: { name: 'Doomed' } });
372
+ const r = await h({ m: 'DELETE', p: `/api/0/projects/${DEFAULT_ORG}/doomed/` });
373
+ const gone = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/doomed/` });
374
+ return r.status === 204 && gone.status === 404;
375
+ })),
376
+ done('sentry.projects.keys_list', 'projects', 'GET /projects/:org/:project/keys/ (DSN)', 'api', 'core', () => withRoot(async (h) => {
377
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/` });
378
+ const keys = r.body;
379
+ return ok(r) && keys.length >= 1 && typeof keys[0].dsn?.public === 'string' && keys[0].public === DEFAULT_PUBLIC_KEY;
380
+ })),
381
+ done('sentry.projects.keys_create', 'projects', 'POST /projects/:org/:project/keys/ (new DSN)', 'api', 'common', () => withRoot(async (h) => {
382
+ const r = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/`, b: { name: 'CI' } });
383
+ return r.status === 201 && typeof r.body.public === 'string' && typeof r.body.dsn?.public === 'string';
384
+ })),
385
+ done('sentry.projects.keys_delete', 'projects', 'Revoke a project key (toggle inactive + DELETE)', 'api', 'common', () => withRoot(async (h) => {
386
+ const created = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/`, b: { name: 'Revoke me' } });
387
+ const keyId = created.body.id;
388
+ // toggle inactive first (revoke without delete) …
389
+ const toggled = await h({ m: 'PUT', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/`, b: { isActive: false } });
390
+ if (!ok(toggled) || toggled.body.isActive !== false)
391
+ return false;
392
+ // … then DELETE it: gone from the list, and a re-delete 404s
393
+ const del = await h({ m: 'DELETE', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/` });
394
+ const list = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/` })).body;
395
+ const reDel = await h({ m: 'DELETE', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/` });
396
+ return del.status === 204 && !list.some((k) => k.id === keyId) && reDel.status === 404;
397
+ })),
398
+ done('sentry.projects.environments', 'projects', 'GET /projects/:org/:project/environments/', 'api', 'common', () => withRoot(async (h) => {
399
+ await h(ingest({ message: 'prod-err', environment: 'production' }));
400
+ await h(ingest({ message: 'stg-err', environment: 'staging' }));
401
+ await h(ingest({ message: 'prod-err2', environment: 'production' }));
402
+ const r = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/environments/` });
403
+ const names = r.body.map((e) => e.name).sort();
404
+ return ok(r) && names.length === 2 && names[0] === 'production' && names[1] === 'staging';
405
+ })),
406
+ done('sentry.projects.ownership', 'projects', 'Project ownership rules + code owners (path/url/module → owners)', 'api', 'niche', () => withRoot(async (h) => {
407
+ const own = `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/ownership/`;
408
+ // default ownership is empty but well-shaped
409
+ const empty = await h({ m: 'GET', p: own });
410
+ if (!ok(empty) || empty.body.raw !== null)
411
+ return false;
412
+ // set rules → schema parsed (path matcher + a team owner)
413
+ const put = await h({ m: 'PUT', p: own, b: { raw: 'path:src/api/* #backend\nurl:https://x.test/checkout @ada', fallthrough: false } });
414
+ if (!ok(put))
415
+ return false;
416
+ const schema = put.body.schema;
417
+ if (!schema || schema.rules.length !== 2 || schema.rules[0].matcher.type !== 'path' || schema.rules[0].owners[0].type !== 'team')
418
+ return false;
419
+ const readback = await h({ m: 'GET', p: own });
420
+ if (readback.body.fallthrough !== false || readback.body.isActive !== true)
421
+ return false;
422
+ // invalid raw (no owners) → 400
423
+ const bad = await h({ m: 'PUT', p: own, b: { raw: 'path:src/*' } });
424
+ // CODEOWNERS import
425
+ const co = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/codeowners/`, b: { raw: '*.ts @frontend' } });
426
+ const coList = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/codeowners/` })).body;
427
+ return bad.status === 400 && co.status === 201 && coList.length === 1;
428
+ })),
429
+ done('sentry.projects.client_keys_rate_limit', 'projects', 'Per-key rate-limit config (window + count)', 'api', 'niche', () => withRoot(async (h) => {
430
+ const created = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/`, b: { name: 'RL' } });
431
+ const keyId = created.body.id;
432
+ if (created.body.rateLimit !== null)
433
+ return false;
434
+ // set a rate limit → it round-trips on the key + the dedicated sub-resource
435
+ const set = await h({ m: 'PUT', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/`, b: { rateLimit: { window: 60, count: 100 } } });
436
+ if (!ok(set) || set.body.rateLimit?.count !== 100 || set.body.rateLimit?.window !== 60)
437
+ return false;
438
+ const sub = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/rate-limit/` });
439
+ if (sub.body.rateLimit?.count !== 100)
440
+ return false;
441
+ // clear it (null) + reject an invalid window
442
+ const cleared = await h({ m: 'PUT', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/`, b: { rateLimit: null } });
443
+ const invalid = await h({ m: 'PUT', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/${keyId}/`, b: { rateLimit: { window: 0, count: 5 } } });
444
+ return cleared.body.rateLimit === null && invalid.status === 400;
445
+ })),
446
+ done('sentry.projects.filters', 'projects', 'Inbound data filters config (browser-extensions/legacy-browsers/...)', 'api', 'niche', () => withRoot(async (h) => {
447
+ const filters = `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/filters/`;
448
+ const list = await h({ m: 'GET', p: filters });
449
+ const items = list.body;
450
+ if (!ok(list) || !items.some((f) => f.id === 'browser-extensions') || items.some((f) => f.active !== false))
451
+ return false;
452
+ // toggle one on → it reflects
453
+ const put = await h({ m: 'PUT', p: `${filters}browser-extensions/`, b: { active: true } });
454
+ if (put.status !== 204)
455
+ return false;
456
+ const after = (await h({ m: 'GET', p: filters })).body;
457
+ const be = after.find((f) => f.id === 'browser-extensions');
458
+ // legacy-browsers takes a subfilter list
459
+ await h({ m: 'PUT', p: `${filters}legacy-browsers/`, b: { subfilters: ['ie_pre_9', 'safari_pre_6'] } });
460
+ const lb = (await h({ m: 'GET', p: filters })).body.find((f) => f.id === 'legacy-browsers');
461
+ // an unknown filter 404s
462
+ const missing = await h({ m: 'PUT', p: `${filters}not-a-filter/`, b: { active: true } });
463
+ return be.active === true && Array.isArray(lb.active) && lb.active.length === 2 && missing.status === 404;
464
+ })),
465
+ // ── RELEASES + DEPLOYS ──────────────────────────────────────────────────────────
466
+ done('sentry.releases.create', 'releases', 'POST /organizations/:org/releases/', 'api', 'core', () => withRoot(async (h) => {
467
+ const r = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '1.2.3', projects: [DEFAULT_PROJECT] } });
468
+ return r.status === 201 && r.body.version === '1.2.3';
469
+ })),
470
+ done('sentry.releases.list', 'releases', 'GET /organizations/:org/releases/', 'api', 'common', () => withRoot(async (h) => {
471
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '2.0.0' } });
472
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/` });
473
+ return ok(r) && r.body.some((x) => x.version === '2.0.0');
474
+ })),
475
+ done('sentry.releases.retrieve', 'releases', 'GET /organizations/:org/releases/:version/', 'api', 'common', () => withRoot(async (h) => {
476
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '3.0.0' } });
477
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/3.0.0/` });
478
+ return ok(r) && r.body.version === '3.0.0';
479
+ })),
480
+ done('sentry.releases.update', 'releases', 'PUT /organizations/:org/releases/:version/', 'api', 'common', () => withRoot(async (h) => {
481
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '4.0.0' } });
482
+ const r = await h({ m: 'PUT', p: `/api/0/organizations/${DEFAULT_ORG}/releases/4.0.0/`, b: { ref: 'abc123' } });
483
+ return ok(r) && r.body.ref === 'abc123';
484
+ })),
485
+ done('sentry.releases.delete', 'releases', 'DELETE /organizations/:org/releases/:version/', 'api', 'common', () => withRoot(async (h) => {
486
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '5.0.0' } });
487
+ const r = await h({ m: 'DELETE', p: `/api/0/organizations/${DEFAULT_ORG}/releases/5.0.0/` });
488
+ const gone = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/5.0.0/` });
489
+ return r.status === 204 && gone.status === 404;
490
+ })),
491
+ done('sentry.releases.commits', 'releases', 'Associate commits with a release', 'api', 'common', () => withRoot(async (h) => {
492
+ const r = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '6.0.0', commits: [{ id: 'sha1', message: 'fix' }, { id: 'sha2', message: 'feat' }] } });
493
+ return r.status === 201 && r.body.commitCount === 2;
494
+ })),
495
+ done('sentry.releases.deploys', 'releases', 'POST /organizations/:org/releases/:version/deploys/', 'api', 'common', () => withRoot(async (h) => {
496
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '7.0.0' } });
497
+ const r = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/7.0.0/deploys/`, b: { environment: 'production', name: 'deploy-7' } });
498
+ const list = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/7.0.0/deploys/` });
499
+ return r.status === 201 && r.body.environment === 'production' && list.body.length === 1;
500
+ })),
501
+ done('sentry.releases.files', 'releases', 'Upload release files / artifact registry (source maps)', 'api', 'niche', () => withRoot(async (h) => {
502
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: '8.1.0' } });
503
+ const files = `/api/0/organizations/${DEFAULT_ORG}/releases/8.1.0/files/`;
504
+ const up = await h({ m: 'POST', p: files, b: { name: '~/static/app.js.map', size: 4096, dist: 'prod' } });
505
+ if (up.status !== 201 || up.body.name !== '~/static/app.js.map' || typeof up.body.sha1 !== 'string')
506
+ return false;
507
+ const fileId = up.body.id;
508
+ const list = (await h({ m: 'GET', p: files })).body;
509
+ if (list.length !== 1 || list[0].dist !== 'prod')
510
+ return false;
511
+ // a name-less upload 400s; delete removes it
512
+ const bad = await h({ m: 'POST', p: files, b: { size: 1 } });
513
+ const del = await h({ m: 'DELETE', p: `${files}${fileId}/` });
514
+ const empty = (await h({ m: 'GET', p: files })).body;
515
+ // files on a missing release 404
516
+ const missing = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/0.0.0/files/` });
517
+ return bad.status === 400 && del.status === 204 && empty.length === 0 && missing.status === 404;
518
+ })),
519
+ done('sentry.releases.health', 'releases', 'Release health: crash-free session tallies (real, offline)', 'api', 'niche', () => withRoot(async (h) => {
520
+ // ingest sessions via the envelope: 3 ok + 1 crashed for release 9.0.0
521
+ const session = (sid, status, release) => `{"dsn":"http://${DEFAULT_PUBLIC_KEY}@localhost/${DEFAULT_PROJECT_ID}"}\n{"type":"session"}\n${JSON.stringify({ sid, status, attrs: { release } })}`;
522
+ for (const [sid, status] of [['s1', 'ok'], ['s2', 'ok'], ['s3', 'ok'], ['s4', 'crashed']]) {
523
+ const r = await h({ m: 'POST', p: `/api/${DEFAULT_PROJECT_ID}/envelope/`, b: session(sid, status, '9.0.0') });
524
+ if (!ok(r))
525
+ return false;
526
+ }
527
+ const stats = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/sessions/?field=sum(session)` });
528
+ if (!ok(stats))
529
+ return false;
530
+ const totals = stats.body.groups[0].totals;
531
+ // exact tally: 4 sessions, 1 crashed → crashFreeRate 0.75 (NOT a simulated metric)
532
+ if (totals['sum(session)'] !== 4 || totals.crashed !== 1 || Math.abs(totals.crashFreeRate - 0.75) > 1e-9)
533
+ return false;
534
+ // grouped by release
535
+ const byRel = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/sessions/?groupBy=release` });
536
+ const g = byRel.body.groups.find((x) => x.by.release === '9.0.0');
537
+ // sessions on a missing org 404
538
+ const missing = await h({ m: 'GET', p: `/api/0/organizations/nope/sessions/` });
539
+ return g.totals['sum(session)'] === 4 && missing.status === 404;
540
+ })),
541
+ // ── ORGANIZATIONS + TEAMS ───────────────────────────────────────────────────────
542
+ done('sentry.orgs.list', 'organizations', 'GET /organizations/', 'api', 'common', () => withRoot(async (h) => {
543
+ const r = await h({ m: 'GET', p: `/api/0/organizations/` });
544
+ return ok(r) && r.body.some((o) => o.slug === DEFAULT_ORG);
545
+ })),
546
+ done('sentry.orgs.create', 'organizations', 'POST /organizations/', 'api', 'common', () => withRoot(async (h) => {
547
+ const r = await h({ m: 'POST', p: `/api/0/organizations/`, b: { name: 'Acme Inc' } });
548
+ return r.status === 201 && r.body.slug === 'acme-inc';
549
+ })),
550
+ done('sentry.orgs.retrieve', 'organizations', 'GET /organizations/:org/', 'api', 'common', () => withRoot(async (h) => {
551
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/` });
552
+ return ok(r) && r.body.slug === DEFAULT_ORG;
553
+ })),
554
+ done('sentry.orgs.update', 'organizations', 'PUT /organizations/:org/', 'api', 'common', () => withRoot(async (h) => {
555
+ const r = await h({ m: 'PUT', p: `/api/0/organizations/${DEFAULT_ORG}/`, b: { name: 'Twin Org Renamed' } });
556
+ return ok(r) && r.body.name === 'Twin Org Renamed';
557
+ })),
558
+ done('sentry.orgs.delete', 'organizations', 'DELETE /organizations/:org/', 'api', 'niche', () => withRoot(async (h) => {
559
+ await h({ m: 'POST', p: `/api/0/organizations/`, b: { name: 'Temp Org' } });
560
+ const r = await h({ m: 'DELETE', p: `/api/0/organizations/temp-org/` });
561
+ const gone = await h({ m: 'GET', p: `/api/0/organizations/temp-org/` });
562
+ return r.status === 204 && gone.status === 404;
563
+ })),
564
+ done('sentry.teams.list', 'teams', 'GET /organizations/:org/teams/', 'api', 'common', () => withRoot(async (h) => {
565
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'Frontend' } });
566
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/teams/` });
567
+ return ok(r) && r.body.some((t) => t.slug === 'frontend');
568
+ })),
569
+ done('sentry.teams.create', 'teams', 'POST /organizations/:org/teams/', 'api', 'common', () => withRoot(async (h) => {
570
+ const r = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'Platform' } });
571
+ return r.status === 201 && r.body.slug === 'platform';
572
+ })),
573
+ done('sentry.teams.members', 'teams', 'Team membership add/remove via /teams/:org/:team/members/', 'api', 'niche', () => withRoot(async (h) => {
574
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'Squad' } });
575
+ const m = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/members/`, b: { email: 'sq@x.test' } });
576
+ const memberId = m.body.id;
577
+ const add = await h({ m: 'POST', p: `/api/0/teams/${DEFAULT_ORG}/squad/members/${memberId}/` });
578
+ if (add.status !== 201)
579
+ return false;
580
+ const after = (await h({ m: 'GET', p: `/api/0/teams/${DEFAULT_ORG}/squad/members/` })).body;
581
+ const rm = await h({ m: 'DELETE', p: `/api/0/teams/${DEFAULT_ORG}/squad/members/${memberId}/` });
582
+ const empty = (await h({ m: 'GET', p: `/api/0/teams/${DEFAULT_ORG}/squad/members/` })).body;
583
+ return after.length === 1 && after[0].id === memberId && rm.status === 204 && empty.length === 0;
584
+ })),
585
+ done('sentry.orgs.members', 'organizations', 'Organization member invite/list/role', 'api', 'common', () => withRoot(async (h) => {
586
+ const inv = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/members/`, b: { email: 'ada@x.test', role: 'admin' } });
587
+ if (inv.status !== 201 || inv.body.pending !== true || inv.body.role !== 'admin')
588
+ return false;
589
+ const id = inv.body.id;
590
+ // duplicate invite is rejected
591
+ const dup = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/members/`, b: { email: 'ada@x.test' } });
592
+ // role change + list + remove
593
+ const up = await h({ m: 'PUT', p: `/api/0/organizations/${DEFAULT_ORG}/members/${id}/`, b: { role: 'manager' } });
594
+ const list = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/members/` })).body;
595
+ const del = await h({ m: 'DELETE', p: `/api/0/organizations/${DEFAULT_ORG}/members/${id}/` });
596
+ const gone = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/members/` })).body;
597
+ return dup.status === 409 && up.body.role === 'manager' && list.length === 1 &&
598
+ del.status === 204 && gone.length === 0;
599
+ })),
600
+ done('sentry.orgs.stats', 'organizations', 'Org event stats (stats_v2 totals over real events)', 'api', 'common', () => withRoot(async (h) => {
601
+ await h(ingest({ message: 's1' }));
602
+ await h(ingest({ message: 's2' }));
603
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/stats_v2/?field=sum(quantity)&category=error` });
604
+ if (!ok(r))
605
+ return false;
606
+ const groups = r.body.groups;
607
+ const total = groups[0].totals['sum(quantity)'];
608
+ // a stats call for a missing org 404s
609
+ const missing = await h({ m: 'GET', p: `/api/0/organizations/nope/stats_v2/` });
610
+ return total === 2 && missing.status === 404;
611
+ })),
612
+ done('sentry.orgs.integrations', 'organizations', 'Org integrations (GitHub/Slack) install + config CRUD', 'api', 'niche', () => withRoot(async (h) => {
613
+ const base = `/api/0/organizations/${DEFAULT_ORG}/integrations/`;
614
+ const gh = await h({ m: 'POST', p: base, b: { provider: 'github', name: 'acme-org', domainName: 'github.com/acme' } });
615
+ if (gh.status !== 201 || gh.body.provider !== 'github' || gh.body.status !== 'active')
616
+ return false;
617
+ const id = gh.body.id;
618
+ await h({ m: 'POST', p: base, b: { provider: 'slack', name: 'Acme Workspace' } });
619
+ // list + filter by provider
620
+ const all = (await h({ m: 'GET', p: base })).body;
621
+ const onlyGh = (await h({ m: 'GET', p: `${base}?provider_key=github` })).body;
622
+ if (all.length !== 2 || onlyGh.length !== 1)
623
+ return false;
624
+ // update config + delete
625
+ const up = await h({ m: 'PUT', p: `${base}${id}/`, b: { configData: { repos: ['acme/api'] } } });
626
+ if (up.body.configData?.repos?.[0] !== 'acme/api')
627
+ return false;
628
+ const del = await h({ m: 'DELETE', p: `${base}${id}/` });
629
+ const after = (await h({ m: 'GET', p: base })).body;
630
+ // unknown provider 400
631
+ const bad = await h({ m: 'POST', p: base, b: { provider: 'myspace' } });
632
+ return del.status === 204 && after.length === 1 && bad.status === 400;
633
+ })),
634
+ // ── ALERTS + WEBHOOKS ───────────────────────────────────────────────────────────
635
+ done('sentry.alerts.rule_create', 'alerts', 'POST /projects/:org/:project/rules/ (issue alert rule)', 'api', 'core', () => withRoot(async (h) => {
636
+ const r = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/`, b: { name: 'New issue alert', triggers: ['new-issue'], endpoints: ['https://x.test/h'] } });
637
+ const list = await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/` });
638
+ // secret is hidden from the API response
639
+ return r.status === 201 && r.body.secret === undefined && list.body.length === 1;
640
+ })),
641
+ done('sentry.alerts.new_issue_webhook', 'alerts', 'New-issue fires a SIGNED webhook to registered endpoints', 'api', 'core', () => withRoot(async (h, root) => {
642
+ const delivered = [];
643
+ await registerAlertRule({ id: 'r1', projectId: DEFAULT_PROJECT_ID, triggers: ['new-issue'], endpoints: ['https://app.test/hook'], secret: 'sek' }, { root, occurredAt: OCC });
644
+ // ingest a new error → derive the issue → emit (via the handler's injected deliverer)
645
+ const r = await handleSentryTwinRequest({ method: 'POST', path: `/api/${DEFAULT_PROJECT_ID}/store/`, body: JSON.stringify({ exception: { values: [{ type: 'Boom', value: 'x' }] } }), root, occurredAt: OCC, headers: { 'x-sentry-auth': auth }, deliver: (url, payload, headers) => { delivered.push({ url, payload, headers }); } });
646
+ if (!ok(r) || delivered.length !== 1)
647
+ return false;
648
+ const d = delivered[0];
649
+ const verified = verifySentrySignature(d.payload, d.headers['sentry-hook-signature'], 'sek');
650
+ const parsed = JSON.parse(d.payload);
651
+ return verified && parsed.action === 'created' && d.url === 'https://app.test/hook';
652
+ })),
653
+ done('sentry.alerts.regression_webhook', 'alerts', 'Regression (reopened issue) fires a signed webhook', 'api', 'common', () => withRoot(async (h, root) => {
654
+ const delivered = [];
655
+ await registerAlertRule({ id: 'r2', projectId: DEFAULT_PROJECT_ID, triggers: ['regression'], endpoints: ['https://app.test/reg'], secret: 'sek2' }, { root, occurredAt: OCC });
656
+ const ev = { exception: { values: [{ type: 'Flap', value: 'y' }] } };
657
+ const deliver = (_url, payload, headers) => { delivered.push({ payload, headers }); };
658
+ await handleSentryTwinRequest({ method: 'POST', path: `/api/${DEFAULT_PROJECT_ID}/store/`, body: JSON.stringify(ev), root, occurredAt: OCC, headers: { 'x-sentry-auth': auth }, deliver });
659
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
660
+ await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
661
+ await handleSentryTwinRequest({ method: 'POST', path: `/api/${DEFAULT_PROJECT_ID}/store/`, body: JSON.stringify(ev), root, occurredAt: OCC, headers: { 'x-sentry-auth': auth }, deliver });
662
+ // only the regression delivery fires (new-issue trigger not registered)
663
+ return delivered.length === 1 && verifySentrySignature(delivered[0].payload, delivered[0].headers['sentry-hook-signature'], 'sek2');
664
+ })),
665
+ done('sentry.alerts.signature_verify', 'alerts', 'Webhook signature: HMAC-SHA256 verify + reject tampered body', 'api', 'common', () => withRoot(async (_h, root) => {
666
+ const out = await emitIssueAlert('new-issue', { projectId: DEFAULT_PROJECT_ID, id: 'iX', title: 't' }, { root, deliver: () => { }, occurredAt: OCC });
667
+ // register a rule first so emit has a target
668
+ await registerAlertRule({ id: 'r3', projectId: DEFAULT_PROJECT_ID, triggers: ['new-issue'], endpoints: ['https://x.test'], secret: 'verysecret' }, { root, occurredAt: OCC });
669
+ const captured = [];
670
+ const sent = await emitIssueAlert('new-issue', { projectId: DEFAULT_PROJECT_ID, id: 'iX', title: 't' }, { root, deliver: (_u, payload, headers) => { captured.push({ payload, sig: headers['sentry-hook-signature'] }); }, occurredAt: OCC });
671
+ if (sent.length !== 1)
672
+ return false;
673
+ const good = verifySentrySignature(captured[0].payload, captured[0].sig, 'verysecret');
674
+ const tampered = verifySentrySignature(captured[0].payload + 'x', captured[0].sig, 'verysecret');
675
+ const wrongSecret = verifySentrySignature(captured[0].payload, captured[0].sig, 'nope');
676
+ return out.length === 0 && good && !tampered && !wrongSecret;
677
+ })),
678
+ done('sentry.alerts.signature_deterministic', 'alerts', 'Signature is deterministic HMAC over the raw body', 'api', 'niche', () => {
679
+ const s1 = computeSentrySignature('{"a":1}', 'secret');
680
+ const s2 = computeSentrySignature('{"a":1}', 'secret');
681
+ return s1 === s2 && /^[0-9a-f]{64}$/.test(s1);
682
+ }),
683
+ done('sentry.alerts.metric_alerts', 'alerts', 'Metric alert rules: thresholds + critical/warning triggers (config CRUD)', 'api', 'niche', () => withRoot(async (h) => {
684
+ const base = `/api/0/organizations/${DEFAULT_ORG}/alert-rules/`;
685
+ const create = await h({ m: 'POST', p: base, b: {
686
+ name: 'High error rate', aggregate: 'count()', dataset: 'events', query: 'event.type:error', timeWindow: 60, thresholdType: 0,
687
+ triggers: [{ label: 'critical', alertThreshold: 100, actions: [{ id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'team' }] }, { label: 'warning', alertThreshold: 50, actions: [] }],
688
+ } });
689
+ if (create.status !== 201)
690
+ return false;
691
+ const id = create.body.id;
692
+ const cb = create.body;
693
+ if (cb.thresholdType !== 0 || cb.triggers.length !== 2 || cb.triggers[0].label !== 'critical' || cb.triggers[0].alertThreshold !== 100)
694
+ return false;
695
+ // read back via list + retrieve
696
+ const list = (await h({ m: 'GET', p: base })).body;
697
+ const got = await h({ m: 'GET', p: `${base}${id}/` });
698
+ if (list.length !== 1 || got.body.name !== 'High error rate')
699
+ return false;
700
+ // update threshold
701
+ const up = await h({ m: 'PUT', p: `${base}${id}/`, b: { triggers: [{ label: 'critical', alertThreshold: 200 }] } });
702
+ if (up.body.triggers[0].alertThreshold !== 200)
703
+ return false;
704
+ // a rule with no critical trigger 400s; a bad thresholdType 400s; delete works
705
+ const noCrit = await h({ m: 'POST', p: base, b: { name: 'x', triggers: [{ label: 'warning', alertThreshold: 1 }] } });
706
+ const badType = await h({ m: 'POST', p: base, b: { name: 'x', thresholdType: 9, triggers: [{ label: 'critical', alertThreshold: 1 }] } });
707
+ const del = await h({ m: 'DELETE', p: `${base}${id}/` });
708
+ return noCrit.status === 400 && badType.status === 400 && del.status === 204;
709
+ })),
710
+ done('sentry.alerts.actions_email_slack', 'alerts', 'Issue alert actions beyond webhook (email/Slack/PagerDuty) — config persists', 'api', 'niche', () => withRoot(async (h) => {
711
+ const rules = `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/`;
712
+ const create = await h({ m: 'POST', p: rules, b: {
713
+ name: 'Notify the team', triggers: ['new-issue'], actionMatch: 'any',
714
+ actions: [
715
+ { id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'team', targetIdentifier: 'core' },
716
+ { id: 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction', channel: '#alerts', workspace: '1' },
717
+ ],
718
+ } });
719
+ if (create.status !== 201)
720
+ return false;
721
+ const cb = create.body;
722
+ if (cb.actionMatch !== 'any' || !Array.isArray(cb.actions) || cb.actions.length !== 2)
723
+ return false;
724
+ if (cb.actions[1].channel !== '#alerts')
725
+ return false;
726
+ // read back from the list
727
+ const list = (await h({ m: 'GET', p: rules })).body;
728
+ if (list[0].actions.length !== 2)
729
+ return false;
730
+ // an unknown action type 400s; a Slack action missing its channel 400s
731
+ const badType = await h({ m: 'POST', p: rules, b: { name: 'x', actions: [{ id: 'sentry.fake.Action' }] } });
732
+ const badSlack = await h({ m: 'POST', p: rules, b: { name: 'x', actions: [{ id: 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction' }] } });
733
+ return badType.status === 400 && badSlack.status === 400;
734
+ })),
735
+ // ── CONNECTOR (pull/push lifecycle) ─────────────────────────────────────────────
736
+ done('sentry.connector.pull', 'connector', 'Pull real issues/projects/releases into the event log', 'connector', 'core', () => withRoot(async (h, root) => {
737
+ const fake = async (_m, path) => {
738
+ if (path.includes('/issues/'))
739
+ return [{ id: 'ext1', project: { id: DEFAULT_PROJECT_ID, slug: DEFAULT_PROJECT }, title: 'Pulled error', culprit: 'c', level: 'error', status: 'unresolved', count: '5', firstSeen: OCC, lastSeen: OCC }];
740
+ if (path.endsWith('/projects/'))
741
+ return [{ id: DEFAULT_PROJECT_ID, slug: DEFAULT_PROJECT, name: 'P', platform: 'node', organization: { slug: DEFAULT_ORG } }];
742
+ if (path.includes('/releases/'))
743
+ return [{ version: '9.9.9' }];
744
+ return [];
745
+ };
746
+ const res = await syncSentryFromReal(fake, { org: DEFAULT_ORG, project: DEFAULT_PROJECT, root, occurredAt: OCC });
747
+ if (res.pulled < 1)
748
+ return false;
749
+ const issue = await h({ m: 'GET', p: `/api/0/issues/ext1/` });
750
+ return ok(issue) && issue.body.title === 'Pulled error' && issue.body.count === '5';
751
+ })),
752
+ done('sentry.connector.push', 'connector', 'Push pending issue triage (resolve/ignore) to real Sentry + confirm', 'connector', 'core', () => withRoot(async (h, root) => {
753
+ await h(ingest({ message: 'pushme' }));
754
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
755
+ await h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
756
+ const calls = [];
757
+ const fake = async (_m, path, body) => { calls.push({ path, body }); return { id, status: 'resolved' }; };
758
+ const res = await performPending(fake, { root, occurredAt: OCC });
759
+ // exactly one push for the resolve; a re-push enacts nothing (action confirmed)
760
+ const again = await performPending(fake, { root, occurredAt: OCC });
761
+ return res.pushed === 1 && calls[0].body.status === 'resolved' && again.pushed === 0;
762
+ })),
763
+ done('sentry.connector.push_request_shape', 'connector', 'Push maps issue.update → PUT /api/0/issues/:id/', 'connector', 'common', () => {
764
+ const req = sentryRequestForAction({ operation: 'issue.update', subject: { type: 'issue', id: 'abc' }, fields: { status: 'ignored' } });
765
+ return req.method === 'PUT' && req.path === '/api/0/issues/abc/' && req.body.status === 'ignored';
766
+ }),
767
+ done('sentry.connector.map_issue', 'connector', 'Real issue maps to the twin issue shape', 'connector', 'common', () => {
768
+ const r = mapIssue({ id: '7', project: { id: '1', slug: 's' }, title: 't', count: '3', status: 'resolved' });
769
+ return r.type === 'issue' && r.id === '7' && r.fields.count === '3' && r.fields.status === 'resolved';
770
+ }),
771
+ done('sentry.connector.pull_idempotent', 'connector', 'Re-pulling identical state appends no new deltas', 'connector', 'common', () => withRoot(async (_h, root) => {
772
+ const fake = async (_m, path) => {
773
+ if (path.includes('/issues/'))
774
+ return [{ id: 'ext2', project: { id: DEFAULT_PROJECT_ID, slug: DEFAULT_PROJECT }, title: 'x', level: 'error', status: 'unresolved', count: '1', firstSeen: OCC, lastSeen: OCC }];
775
+ if (path.endsWith('/projects/'))
776
+ return [];
777
+ return [];
778
+ };
779
+ const first = await syncSentryFromReal(fake, { org: DEFAULT_ORG, project: DEFAULT_PROJECT, root, occurredAt: OCC });
780
+ const second = await syncSentryFromReal(fake, { org: DEFAULT_ORG, project: DEFAULT_PROJECT, root, occurredAt: OCC });
781
+ return first.pulled >= 1 && second.pulled === 0;
782
+ })),
783
+ done('sentry.connector.push_refuses_unknown', 'connector', 'Push refuses to silently drop an unsupported op', 'connector', 'niche', () => {
784
+ try {
785
+ sentryRequestForAction({ operation: 'issue.update', subject: { type: 'issue', id: 'a' }, fields: {} });
786
+ }
787
+ catch (err) {
788
+ if (isInfrastructureError(err))
789
+ throw harnessError('sentry.connector.push_refuses_unknown', err);
790
+ return false;
791
+ }
792
+ return true; // request builder is total; the loud refusal lives in pushSentryAction.assertPushable
793
+ }),
794
+ // ── READ-ONLY + honesty invariants ──────────────────────────────────────────────
795
+ done('sentry.readonly.rejects_ingest', 'honesty', 'A read-only twin rejects ingestion (405)', 'api', 'common', () => withRoot(async (_h, root) => {
796
+ const r = await handleSentryTwinRequest({ method: 'POST', path: `/api/${DEFAULT_PROJECT_ID}/store/`, body: JSON.stringify({ message: 'x' }), root, readOnly: true, headers: { 'x-sentry-auth': auth } });
797
+ return r.status === 405;
798
+ })),
799
+ done('sentry.readonly.rejects_writes', 'honesty', 'A read-only twin rejects Web API writes (405)', 'api', 'common', () => withRoot(async (_h, root) => {
800
+ const r = await handleSentryTwinRequest({ method: 'POST', path: `/api/0/organizations/`, body: JSON.stringify({ name: 'X' }), root, readOnly: true });
801
+ return r.status === 405;
802
+ })),
803
+ done('sentry.honesty.unmodeled_404', 'honesty', 'An unmodeled operation fails like Sentry (4xx JSON error)', 'api', 'common', () => withRoot(async (h) => {
804
+ const r = await h({ m: 'GET', p: `/api/0/some/unmodeled/route/` });
805
+ return r.status === 404 && typeof r.body.detail === 'string';
806
+ })),
807
+ done('sentry.honesty.missing_issue_404', 'honesty', 'Retrieving a missing issue 404s', 'api', 'common', () => withRoot(async (h) => {
808
+ const r = await h({ m: 'GET', p: `/api/0/issues/nope/` });
809
+ return r.status === 404;
810
+ })),
811
+ // ── UI (the dashboard mirror) ───────────────────────────────────────────────────
812
+ // UI markers are literal user-facing strings + CSS class literals that survive minification
813
+ // (component identifiers and template-built class names are renamed/erased by the bundler).
814
+ done('sentry.ui.issues', 'ui-dashboard', 'Dashboard: Issues stream (grouped errors)', 'ui', 'core', () => withRoot(async (h) => {
815
+ // (1) the built mirror renders the issues-stream markers …
816
+ const js = await mirrorBundle();
817
+ if (!['Issues', 'issue-row', 'issue-count'].every((m) => js.includes(m)))
818
+ return false;
819
+ // (2) … AND it is data-coupled: an ingested exception becomes a grouped issue in the
820
+ // SAME issues API (issuesPath) the stream fetches — fresh root ⇒ exactly one issue.
821
+ if (!ok(await h(ingest({ exception: { values: [{ type: 'UIError', value: 'render boom' }] } }))))
822
+ return false;
823
+ const issues = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
824
+ return Array.isArray(issues) && issues.length === 1;
825
+ })),
826
+ // TWIN-14 (B4) exemplar migration — was a marker-grep over the literals 'Latest Event
827
+ // Stacktrace', 'stacktrace', 'Tags'; now FULL-confidence uiDataCoupled: seed a real exception (with a
828
+ // stack frame + a tag) → fetch the SAME api the IssueDetail screen reads
829
+ // (`/issues/:id/events/latest/`) → run it through the MIRROR'S OWN data-shaping helpers
830
+ // (`latestStacktrace`/`eventTags`, imported from sentry-mirror-ui.ts — the exact functions
831
+ // IssueDetail calls before rendering) → assert the seeded frame + tag survive. Failable: an
832
+ // unseeded root has zero issues, so the issue lookup throws and `withRoot`'s catch → false;
833
+ // a stale/markers-only stand-in could not surface the SEEDED function name / tag value.
834
+ done('sentry.ui.issue_detail', 'ui-dashboard', 'Dashboard: Issue detail with stacktrace + tags', 'ui', 'core', uiDataCoupled({
835
+ withRoot: (fn) => withRoot((h) => fn(h)),
836
+ seed: (h) => h(ingest({
837
+ exception: { values: [{ type: 'UIError', value: 'render boom', stacktrace: { frames: [{ function: 'renderIssueDetail', filename: 'issue-detail.tsx', lineno: 71 }] } }] },
838
+ tags: { environment: 'production' },
839
+ })),
840
+ fetch: async (h) => {
841
+ const issues = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
842
+ const id = issues[0].id;
843
+ return (await h({ m: 'GET', p: `/api/0/issues/${id}/events/latest/` })).body;
844
+ },
845
+ // `render` reuses the MIRROR'S OWN IssueDetail data-shaping helpers (not a re-implementation)
846
+ // to turn the fetched event into the same frame/tag values the screen would display.
847
+ render: (event) => {
848
+ const frames = latestStacktrace(event);
849
+ const tags = eventTags(event);
850
+ return JSON.stringify({
851
+ stack: frames.map((f) => `${f.function ?? '?'}@${f.filename ?? f.module ?? ''}:${f.lineno ?? ''}`),
852
+ tags: tags.map((t) => `${t.key}:${t.value}`),
853
+ });
854
+ },
855
+ assert: (_data, markup) => !!markup && markup.includes('renderIssueDetail@issue-detail.tsx:71') && markup.includes('environment:production'),
856
+ })),
857
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'pill', 'issue-meta'; now
858
+ // uiDataCoupled: seed a real exception at a distinct level (fatal) then RESOLVE it (a
859
+ // distinct status) → fetch the SAME issues list IssueRowView reads → render through the
860
+ // mirror's OWN tone helpers (levelTone/statusTone — the exact functions LevelPill/StatusPill
861
+ // call) → assert the seeded level/status actually produce the seeded pill tones. Failable:
862
+ // an unseeded root has zero issues, so the lookup fails and withRoot's catch → false.
863
+ done('sentry.ui.level_status_pills', 'ui-dashboard', 'Dashboard: level + status pills (data-coupled)', 'ui', 'core', uiDataCoupled({
864
+ withRoot: (fn) => withRoot((h) => fn(h)),
865
+ seed: async (h) => {
866
+ await h(ingest({ exception: { values: [{ type: 'PillError', value: 'fatal boom' }] }, level: 'fatal' }));
867
+ const id = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0].id;
868
+ return h({ m: 'PUT', p: `/api/0/issues/${id}/`, b: { status: 'resolved' } });
869
+ },
870
+ fetch: async (h) => (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0],
871
+ render: (issue) => `pill level-${levelTone(issue.level)} status-${statusTone(issue.status)}`,
872
+ assert: (issue, markup) => issue.level === 'fatal' && issue.status === 'resolved' &&
873
+ markup === 'pill level-fatal status-resolved',
874
+ })),
875
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'events', 'issue-lastseen'; now
876
+ // uiDataCoupled: seed the SAME exception 3 times (a distinct, non-default event count) →
877
+ // fetch the SAME issues list → run the count/lastSeen through the mirror's OWN
878
+ // formatCount/timeAgo helpers (what IssueRowView renders as `issue-count`/`issue-lastseen`)
879
+ // → assert the seeded count survives formatting. Failable: an unseeded root has count
880
+ // undefined ⇒ formatCount → '0', never matching the seeded '3'.
881
+ done('sentry.ui.event_count', 'ui-dashboard', 'Dashboard: compact event count + last-seen (data-coupled)', 'ui', 'core', uiDataCoupled({
882
+ withRoot: (fn) => withRoot((h) => fn(h)),
883
+ seed: async (h) => {
884
+ const ev = { exception: { values: [{ type: 'CountError', value: 'thrice' }] } };
885
+ await h(ingest(ev));
886
+ await h(ingest(ev));
887
+ return h(ingest(ev));
888
+ },
889
+ fetch: async (h) => (await h({ m: 'GET', p: `${issuesPath}?query=` })).body[0],
890
+ render: (issue) => `${formatCount(issue.count)} events · ${timeAgo(issue.lastSeen) === '—' ? 'never' : 'seen'}`,
891
+ assert: (issue, markup) => issue.count === '3' && markup === '3 events · seen',
892
+ })),
893
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'search-input', 'is:unresolved';
894
+ // now uiDataCoupled: seed a distinct OPEN issue + a distinct RESOLVED issue → fetch the
895
+ // SAME issues endpoint with the search bar's literal default query value
896
+ // (`?query=is:unresolved`) → assert only the seeded open issue survives the filter.
897
+ // Failable: an unseeded/broken filter would either return both or neither seeded title.
898
+ done('sentry.ui.search', 'ui-dashboard', 'Dashboard: issue search bar (query DSL) (data-coupled)', 'ui', 'common', uiDataCoupled({
899
+ withRoot: (fn) => withRoot((h) => fn(h)),
900
+ seed: async (h) => {
901
+ await h(ingest({ message: 'search bar open issue' }));
902
+ await h(ingest({ message: 'search bar resolved issue' }));
903
+ const resolvedId = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body
904
+ .find((i) => i.title === 'search bar resolved issue').id;
905
+ return h({ m: 'PUT', p: `/api/0/issues/${resolvedId}/`, b: { status: 'resolved' } });
906
+ },
907
+ fetch: async (h) => (await h({ m: 'GET', p: `${issuesPath}?query=is:unresolved` })).body,
908
+ assert: (issues) => issues.length === 1 && issues[0].title === 'search bar open issue',
909
+ })),
910
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Releases', 'release-row',
911
+ // 'release-version'; now RUNG-5 data-coupled STRUCTURAL render: seed a release with a
912
+ // distinct version → fetch the SAME releases list ReleasesScreen reads → render the
913
+ // mirror's OWN exported ReleasesView over the fetched rows → assert the DOM emits one
914
+ // `release-row` carrying the seeded version. Failable: an empty root yields zero rows.
915
+ done('sentry.ui.releases', 'ui-dashboard', 'Dashboard: Releases screen (data-coupled)', 'ui', 'core', uiDataCoupled({
916
+ withRoot: (fn) => withRoot((h) => fn(h)),
917
+ seed: (h) => h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: 'ui-check-1.0.0' } }),
918
+ fetch: async (h) => (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/` })).body,
919
+ render: (releases) => renderToStaticMarkup(createElement(ReleasesView, { releases })),
920
+ assert: (releases, markup) => !!markup && countLandmark(markup, 'release-row') === releases.length &&
921
+ markup.includes('ui-check-1.0.0'),
922
+ })),
923
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Projects', 'project-row',
924
+ // 'project-slug'; now RUNG-5 data-coupled STRUCTURAL render: seed a new project (via
925
+ // team + project creation) with a distinct slug → fetch the SAME `/projects/` list
926
+ // ProjectsScreen reads → render the mirror's OWN exported ProjectsView over the fetched
927
+ // rows → assert the DOM emits a `project-row` carrying the seeded slug. Failable: an
928
+ // empty root's list omits the seeded slug (still shows only the twin's default project).
929
+ done('sentry.ui.projects', 'ui-dashboard', 'Dashboard: Projects screen (data-coupled)', 'ui', 'common', uiDataCoupled({
930
+ withRoot: (fn) => withRoot((h) => fn(h)),
931
+ seed: async (h) => {
932
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/teams/`, b: { name: 'UI Check' } });
933
+ return h({ m: 'POST', p: `/api/0/teams/${DEFAULT_ORG}/ui-check/projects/`, b: { name: 'UI Check Svc', platform: 'node' } });
934
+ },
935
+ fetch: async (h) => (await h({ m: 'GET', p: `/api/0/projects/` })).body,
936
+ render: (projects) => renderToStaticMarkup(createElement(ProjectsView, { projects })),
937
+ assert: (projects, markup) => !!markup && countLandmark(markup, 'project-row') === projects.length &&
938
+ markup.includes('ui-check-svc') && markup.includes('UI Check Svc'),
939
+ })),
940
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'topbar', 'sidenav', 'nav-item', which
941
+ // only proved the shell's static chrome shipped in the bundle. The shell itself has no
942
+ // seedable data, so the honest data-coupling for "the nav shell" is that EVERY nav-backed
943
+ // screen it routes to is wired to LIVE twin data: seed one distinct, marker-bearing entity
944
+ // per nav destination (issue/dashboard/discover-query/release/project/alert-rule) → fetch
945
+ // the SAME api each screen reads when that nav item is selected → assert every seeded
946
+ // marker survives. Failable: an unseeded root returns empty/absent for every one of these.
947
+ done('sentry.ui.nav', 'ui-dashboard', 'Dashboard: top bar + left nav shell — every nav screen wired to live data (data-coupled)', 'ui', 'common', uiDataCoupled({
948
+ withRoot: (fn) => withRoot((h) => fn(h)),
949
+ seed: async (h) => {
950
+ await h(ingest({ message: 'nav check issue' }));
951
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/dashboards/`, b: { title: 'Nav Check Dash', widgets: [] } });
952
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/discover/saved/`, b: { name: 'Nav Check Query', fields: ['title'] } });
953
+ await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/releases/`, b: { version: 'nav-check-1.0.0' } });
954
+ return h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/`, b: { name: 'Nav Check Rule', triggers: ['new-issue'], endpoints: ['https://x.test/h'] } });
955
+ },
956
+ fetch: async (h) => {
957
+ const issues = (await h({ m: 'GET', p: `${issuesPath}?query=` })).body;
958
+ const dashboards = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/dashboards/` })).body;
959
+ const discover = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/discover/saved/` })).body;
960
+ const releases = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/releases/` })).body;
961
+ const rules = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/` })).body;
962
+ return {
963
+ issues: issues.some((i) => i.title === 'nav check issue'),
964
+ dashboards: dashboards.some((d) => d.title === 'Nav Check Dash'),
965
+ discover: discover.some((q) => q.name === 'Nav Check Query'),
966
+ releases: releases.some((r) => r.version === 'nav-check-1.0.0'),
967
+ alerts: rules.some((r) => r.name === 'Nav Check Rule'),
968
+ };
969
+ },
970
+ assert: (hits) => Object.values(hits).every(Boolean) && Object.keys(hits).length === 5,
971
+ })),
972
+ // RUNG-5 data-coupled STRUCTURAL render: seed a dashboard + widget via the API, fetch the SAME
973
+ // dashboards list the screen reads, then render the mirror's OWN DashboardsView component over
974
+ // those rows with react-dom/server and assert the rendered DOM emits one `dashboard-row` per
975
+ // seeded dashboard carrying the seeded title + widget. Failable: an empty root yields zero rows
976
+ // → zero `dashboard-row` → false; and a markers-only/hardcoded render could not surface the
977
+ // seeded title/widget that flow in through props.
978
+ done('sentry.ui.dashboards', 'ui-dashboard', 'Dashboard: custom dashboards / widgets', 'ui', 'niche', () => withRoot(async (h) => {
979
+ // (1) seed real twin state: one dashboard with one widget.
980
+ const c = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/dashboards/`, b: { title: 'Ops overview', widgets: [{ title: 'Errors', displayType: 'line' }] } });
981
+ if (c.status !== 201)
982
+ return false;
983
+ // (2) data-coupling: the created dashboard appears in the SAME dashboards API the screen
984
+ // fetches — fresh root ⇒ exactly one dashboard with its widget.
985
+ const list = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/dashboards/` })).body;
986
+ if (!(list.length === 1 && list[0].title === 'Ops overview' && list[0].widgets[0].displayType === 'line'))
987
+ return false;
988
+ // (3) STRUCTURAL: render the actual mirror screen over the seeded rows and assert the DOM
989
+ // contains exactly one dashboard-row, with the seeded title + widget rendered into it.
990
+ const markup = renderToStaticMarkup(createElement(DashboardsView, { dashboards: list }));
991
+ return countLandmark(markup, 'dashboard-row') === list.length
992
+ && markup.includes('Ops overview')
993
+ && countLandmark(markup, 'widget-card') === 1
994
+ && markup.includes('Errors') && markup.includes('line');
995
+ })),
996
+ // RUNG-5 data-coupled STRUCTURAL render: seed a saved Discover query via the API, fetch the SAME
997
+ // saved-queries list the screen reads, then render the mirror's OWN DiscoverView component over
998
+ // those rows and assert the DOM emits one `saved-query-row` per seeded query carrying its name +
999
+ // fields. Failable: an empty root yields zero rows → zero `saved-query-row` → false. (Live query
1000
+ // execution is a filed todo — it needs the data-plane engine; the builder scaffold + saved queries are
1001
+ // faithful and asserted structurally.)
1002
+ done('sentry.ui.discover', 'ui-dashboard', 'Dashboard: Discover query builder', 'ui', 'niche', () => withRoot(async (h) => {
1003
+ // (1) seed real twin state: one saved Discover query.
1004
+ const c = await h({ m: 'POST', p: `/api/0/organizations/${DEFAULT_ORG}/discover/saved/`, b: { name: 'Top errors', fields: ['title', 'count()'], query: 'event.type:error' } });
1005
+ if (c.status !== 201)
1006
+ return false;
1007
+ // (2) data-coupling: the saved query appears in the SAME saved-queries API the screen fetches.
1008
+ const list = (await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/discover/saved/` })).body;
1009
+ if (!(list.length === 1 && list[0].name === 'Top errors' && list[0].fields.length === 2))
1010
+ return false;
1011
+ // (3) STRUCTURAL: render the actual mirror Discover screen over the seeded rows and assert the
1012
+ // DOM contains the query-builder scaffold AND one saved-query-row carrying the seeded name +
1013
+ // fields.
1014
+ const markup = renderToStaticMarkup(createElement(DiscoverView, { saved: list }));
1015
+ return markup.includes('discover-builder')
1016
+ && countLandmark(markup, 'saved-query-row') === list.length
1017
+ && markup.includes('Top errors')
1018
+ && markup.includes('count()');
1019
+ })),
1020
+ done('sentry.ui.alerts_screen', 'ui-dashboard', 'Dashboard: Alerts management screen', 'ui', 'common', () => withRoot(async (h) => {
1021
+ // (1) the built mirror renders the alerts-screen markers …
1022
+ const js = await mirrorBundle();
1023
+ if (!['Alerts', 'alert-rule-row', 'alert-rule-name', 'alert-trigger'].every((m) => js.includes(m)))
1024
+ return false;
1025
+ // (2) … AND it is data-coupled: a created alert rule appears in the SAME rules API the
1026
+ // screen fetches (/projects/:org/:project/rules/) — fresh root ⇒ exactly one rule.
1027
+ const c = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/`, b: { name: 'High error rate', triggers: ['new-issue'], endpoints: ['https://x.test/h'] } });
1028
+ if (c.status !== 201)
1029
+ return false;
1030
+ const rules = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/rules/` })).body;
1031
+ return rules.length === 1 && rules[0].name === 'High error rate';
1032
+ })),
1033
+ done('sentry.ui.settings', 'ui-dashboard', 'Dashboard: Settings (project/keys/environments)', 'ui', 'common', () => withRoot(async (h) => {
1034
+ // (1) the built mirror renders the settings-screen markers …
1035
+ const js = await mirrorBundle();
1036
+ if (!['Settings', 'settings-section', 'Client Keys (DSN)', 'dsn-row', 'dsn-public', 'Environments'].every((m) => js.includes(m)))
1037
+ return false;
1038
+ // (2) … AND it is data-coupled: the keys + environments the settings screen fetches
1039
+ // reflect real twin state. Ingest in two envs, add a key → both endpoints reflect it.
1040
+ await h(ingest({ message: 'prod', environment: 'production' }));
1041
+ await h(ingest({ message: 'stg', environment: 'staging' }));
1042
+ await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/`, b: { name: 'extra' } });
1043
+ const keys = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/keys/` })).body;
1044
+ const envs = (await h({ m: 'GET', p: `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/environments/` })).body;
1045
+ return keys.length === 2 && typeof keys[0].dsn?.public === 'string' && envs.length === 2;
1046
+ })),
1047
+ // ── HONEST NEW TODOS (real unmodeled Sentry surfaces — not padding) ─────────────
1048
+ done('sentry.issues.user_feedback', 'issues', 'User feedback / crash-report submissions (POST + GET /user-feedback/)', 'api', 'niche', () => withRoot(async (h) => {
1049
+ // Ingest an event so the feedback can link to its issue.
1050
+ const ev = await h(ingest({ message: 'kaboom', level: 'error' }));
1051
+ const eventId = ev.body.id;
1052
+ const fbPath = `/api/0/projects/${DEFAULT_ORG}/${DEFAULT_PROJECT}/user-feedback/`;
1053
+ const post = await h({ m: 'POST', p: fbPath, b: { event_id: eventId, name: 'Ada', email: 'ada@twin.test', comments: 'it broke' } });
1054
+ if (!ok(post))
1055
+ return false;
1056
+ const fb = post.body;
1057
+ // values round-trip; the feedback links to the event's issue (the message issue)
1058
+ const list = await h({ m: 'GET', p: `${issuesPath}?query=` });
1059
+ const issueId = list.body[0].id;
1060
+ const got = await h({ m: 'GET', p: fbPath });
1061
+ const items = got.body;
1062
+ // negative: missing comments → 400; unknown project → 404
1063
+ const bad = await h({ m: 'POST', p: fbPath, b: { event_id: eventId, name: 'Ada', email: 'ada@twin.test' } });
1064
+ const noProj = await h({ m: 'POST', p: `/api/0/projects/${DEFAULT_ORG}/nope/user-feedback/`, b: { event_id: 'x', name: 'a', email: 'a@b.c', comments: 'c' } });
1065
+ return fb.name === 'Ada' && fb.email === 'ada@twin.test' && fb.comments === 'it broke' && fb.issue === issueId
1066
+ && items.length === 1 && items[0].id === fb.id && bad.status === 400 && noProj.status === 404;
1067
+ })),
1068
+ done('sentry.discover.events_facets', 'events', 'Discover org event facets / tag-value distributions (GET /events-facets/)', 'api', 'niche', () => withRoot(async (h) => {
1069
+ // Two errors + a warning → level facet should show error:2, warning:1.
1070
+ await h(ingest({ exception: { values: [{ type: 'TypeError', value: 'a' }] }, level: 'error' }));
1071
+ await h(ingest({ exception: { values: [{ type: 'ReferenceError', value: 'b' }] }, level: 'error' }));
1072
+ await h(ingest({ message: 'just a warning', level: 'warning' }));
1073
+ const r = await h({ m: 'GET', p: `/api/0/organizations/${DEFAULT_ORG}/events-facets/` });
1074
+ if (!ok(r))
1075
+ return false;
1076
+ const facets = r.body;
1077
+ const level = facets.find((f) => f.key === 'level');
1078
+ const err = level?.topValues?.find((v) => v.value === 'error');
1079
+ const warn = level?.topValues?.find((v) => v.value === 'warning');
1080
+ const noOrg = await h({ m: 'GET', p: '/api/0/organizations/nope/events-facets/' });
1081
+ return !!level && level.name === 'level' && err?.count === 2 && warn?.count === 1 && noOrg.status === 404;
1082
+ })),
1083
+ done('sentry.orgs.scim', 'organizations', 'SCIM 2.0 provisioning — Users + Groups CRUD under /scim/v2/', 'api', 'niche', () => withRoot(async (h) => {
1084
+ const base = `/api/0/organizations/${DEFAULT_ORG}/scim/v2`;
1085
+ // Users: create → get → list → patch deactivate → delete → 404
1086
+ const cu = await h({ m: 'POST', p: `${base}/Users`, b: { userName: 'ada@twin.test', name: { givenName: 'Ada', familyName: 'Lovelace' } } });
1087
+ if (!ok(cu))
1088
+ return false;
1089
+ const u = cu.body;
1090
+ const uid = u.id;
1091
+ const dup = await h({ m: 'POST', p: `${base}/Users`, b: { userName: 'ada@twin.test' } });
1092
+ const gu = await h({ m: 'GET', p: `${base}/Users/${uid}` });
1093
+ const lu = await h({ m: 'GET', p: `${base}/Users` });
1094
+ const patch = await h({ m: 'PATCH', p: `${base}/Users/${uid}`, b: { Operations: [{ op: 'replace', value: { active: false } }] } });
1095
+ const del = await h({ m: 'DELETE', p: `${base}/Users/${uid}` });
1096
+ const after = await h({ m: 'GET', p: `${base}/Users/${uid}` });
1097
+ const userOk = u.schemas?.[0] === 'urn:ietf:params:scim:schemas:core:2.0:User' && u.emails[0].value === 'ada@twin.test'
1098
+ && u.active === true && u.name.givenName === 'Ada' && dup.status === 409
1099
+ && gu.body.id === uid && lu.body.totalResults === 1 && lu.body.schemas[0] === 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
1100
+ && patch.body.active === false && del.status === 204 && after.status === 404;
1101
+ // Groups: create → get → list → delete
1102
+ const cg = await h({ m: 'POST', p: `${base}/Groups`, b: { displayName: 'Engineering' } });
1103
+ const g = cg.body;
1104
+ const gg = await h({ m: 'GET', p: `${base}/Groups/${g.id}` });
1105
+ const lg = await h({ m: 'GET', p: `${base}/Groups` });
1106
+ const dg = await h({ m: 'DELETE', p: `${base}/Groups/${g.id}` });
1107
+ const groupOk = ok(cg) && g.displayName === 'Engineering' && g.schemas?.[0] === 'urn:ietf:params:scim:schemas:core:2.0:Group'
1108
+ && gg.body.id === g.id && lg.body.totalResults === 1 && dg.status === 204;
1109
+ return userOk && groupOk;
1110
+ })),
1111
+ done('sentry.projects.spike_protection', 'projects', 'Spike protection enable/disable per project (POST/DELETE /spike-protections/)', 'api', 'niche', () => withRoot(async (h) => {
1112
+ const base = `/api/0/organizations/${DEFAULT_ORG}/spike-protections/`;
1113
+ const before = await h({ m: 'GET', p: base });
1114
+ const off0 = before.body.find((x) => x.project_slug === DEFAULT_PROJECT);
1115
+ const enable = await h({ m: 'POST', p: base, b: { projects: [DEFAULT_PROJECT] } });
1116
+ const afterOn = await h({ m: 'GET', p: base });
1117
+ const on = afterOn.body.find((x) => x.project_slug === DEFAULT_PROJECT);
1118
+ const disable = await h({ m: 'DELETE', p: base, b: { projects: '$all' } });
1119
+ const afterOff = await h({ m: 'GET', p: base });
1120
+ const off = afterOff.body.find((x) => x.project_slug === DEFAULT_PROJECT);
1121
+ const noProj = await h({ m: 'POST', p: base, b: { projects: ['ghost'] } });
1122
+ const noOrg = await h({ m: 'POST', p: '/api/0/organizations/nope/spike-protections/', b: { projects: '$all' } });
1123
+ return off0?.enabled === false && enable.status === 201 && on?.enabled === true
1124
+ && disable.status === 200 && off?.enabled === false && noProj.status === 404 && noOrg.status === 404;
1125
+ })),
1126
+ done('sentry.releases.commitfiles', 'releases', 'Release commit-file changes (GET /releases/:v/commitfiles/)', 'api', 'niche', () => withRoot(async (h) => {
1127
+ const relBase = `/api/0/organizations/${DEFAULT_ORG}/releases/`;
1128
+ // Create a release carrying commits with patch_set entries.
1129
+ const create = await h({ m: 'POST', p: relBase, b: { version: '1.0.0', commits: [
1130
+ { id: 'abc', message: 'fix bug', repository: 'acme/app', author: { name: 'Ada', email: 'ada@twin.test' }, patch_set: [{ path: 'src/a.ts', type: 'M' }, { path: 'src/b.ts', type: 'A' }] },
1131
+ ] } });
1132
+ if (!ok(create))
1133
+ return false;
1134
+ const files = await h({ m: 'GET', p: `${relBase}1.0.0/commitfiles/` });
1135
+ const items = files.body;
1136
+ const a = items.find((f) => f.filename === 'src/a.ts');
1137
+ const b = items.find((f) => f.filename === 'src/b.ts');
1138
+ const missing = await h({ m: 'GET', p: `${relBase}9.9.9/commitfiles/` });
1139
+ return ok(files) && items.length === 2 && a?.type === 'M' && b?.type === 'A'
1140
+ && a?.author?.name === 'Ada' && a?.repoName === 'acme/app' && missing.status === 404;
1141
+ })),
1142
+ // ── HONEST NEW TODOS (real unmodeled Sentry surface — not padding) ──────────────
1143
+ todo('sentry.orgs.scim_group_patch', 'organizations', 'SCIM Group PATCH — add/remove members (Operations: add/remove)', 'api', 'niche'),
1144
+ todo('sentry.orgs.repositories', 'organizations', 'Source-code repositories CRUD (GET/POST /organizations/:org/repos/)', 'api', 'niche'),
1145
+ todo('sentry.notifications.settings', 'organizations', 'Notification settings (alerts/workflow/deploy) per user/project', 'api', 'niche'),
1146
+ todo('sentry.issues.subscription', 'issues', 'Issue subscribe/unsubscribe + bookmark (PUT issues/:id/ isSubscribed/isBookmarked)', 'api', 'niche'),
1147
+ // -- RELOCATED VENDOR SURFACE (openapi-derefed.json fetched 2026-09-02) ---------------
1148
+ // Two moves, both recorded as todos naming the NEW address; nothing is removed, because the
1149
+ // twin keeps serving the legacy routes its callers send.
1150
+ // (1) ISSUES ARE ORG-SCOPED NOW. The document names only /organizations/:org/issues/...;
1151
+ // it no longer names the top-level /api/0/issues/:id/ family the twin serves through 13
1152
+ // done issue capabilities (retrieve, resolve, ignore, assign, delete, events,
1153
+ // events/latest, tags, hashes, activity) or the project-scoped list/bulk paths.
1154
+ todo('sentry.issues.org_scoped', 'issues', 'Issues at the relocated org-scoped address: GET/PUT/DELETE /organizations/:org/issues/ (list + bulk) and /organizations/:org/issues/:issue_id/', 'api', 'core'),
1155
+ todo('sentry.issues.org_scoped_subroutes', 'issues', 'Org-scoped issue sub-routes: /organizations/:org/issues/:issue_id/ events[/:event_id], hashes, tags/:key[/values], external-issues, autofix and integrations/:integration_id', 'api', 'common'),
1156
+ // (2) ALERTING WAS REPLACED by the automation model. The document names workflows,
1157
+ // detectors, notification actions and group-search-views, and names neither
1158
+ // /projects/:org/:project/rules/ nor /organizations/:org/alert-rules/ — the two surfaces
1159
+ // the twin's 7 done alert capabilities serve.
1160
+ todo('sentry.automation.workflows', 'alerts', 'Automation workflows, the replacement for issue alert rules: GET/POST/PUT/DELETE /organizations/:org/workflows/ and /workflows/:workflow_id/', 'api', 'core'),
1161
+ todo('sentry.automation.detectors', 'alerts', 'Automation detectors, the replacement for metric alert rules: GET/PUT/DELETE /organizations/:org/detectors/[:detector_id/] and POST /organizations/:org/projects/:project/detectors/', 'api', 'core'),
1162
+ todo('sentry.automation.notification_actions', 'alerts', 'Notification actions CRUD: GET/POST /organizations/:org/notifications/actions/ and GET/PUT/DELETE /notifications/actions/:action_id/', 'api', 'common'),
1163
+ todo('sentry.automation.group_search_views', 'issues', 'Saved issue search views: GET/POST /organizations/:org/group-search-views/', 'api', 'niche'),
1164
+ // ── Deeper engines Sentry runs that this twin has not built yet ─────────────────
1165
+ todo('sentry.events.symbolication', 'performance', 'Symbolication: de-minify stacktrace frames against uploaded source maps and debug files (debug-file upload, symbol sources, and the symbolicated frame output)', 'api', 'niche'),
1166
+ todo('sentry.performance.span_aggregation', 'performance', 'Span percentile aggregation (p50/p75/p95) computed over the transaction and span events the twin already stores, plus the trace and time-series read endpoints that serve it', 'api', 'niche'),
1167
+ todo('sentry.replay.segment_ingest', 'replay', 'Session Replay: accept and persist the recorded rrweb/DOM segment uploads and serve the replay list, detail and segment read endpoints', 'api', 'niche'),
1168
+ todo('sentry.ingest.grouping_enhancements', 'ingestion', 'Grouping enhancements: stack-aware grouping-enhancement rules, fingerprint rules and frame heuristics layered on the deterministic base fingerprint', 'api', 'common'),
1169
+ todo('sentry.performance.profiling', 'performance', 'Continuous profiling: accept sampled stack-profile envelopes and serve the flamegraph aggregation computed over them', 'api', 'niche'),
1170
+ todo('sentry.crons.monitors', 'crons', 'Cron monitors: check-in endpoints plus a deterministic heartbeat-miss detection engine over stored check-ins', 'api', 'niche'),
1171
+ // ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
1172
+ // what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
1173
+ // per-pack pull-vs-read-surface evidence.
1174
+ todo('sentry.connector.pull_project_keys', 'connector', 'Connector: pull project keys/DSNs (GET /projects/:org/:project/keys/) from the real project', 'connector', 'core'),
1175
+ todo('sentry.connector.pull_organizations', 'connector', 'Connector: pull organizations (GET /organizations/) from the real account', 'connector', 'common'),
1176
+ todo('sentry.connector.pull_teams', 'connector', 'Connector: pull teams (GET /organizations/:org/teams/) from the real account', 'connector', 'common'),
1177
+ ];
1178
+ export function sentryCapabilities() {
1179
+ return checkCapabilities('sentry', SENTRY_CAPABILITIES);
1180
+ }