@homeflare/alchemy 0.33.0 → 0.34.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/dist/grafana/alerting-provenance.d.ts +80 -0
  2. package/dist/grafana/alerting-provenance.d.ts.map +1 -0
  3. package/dist/grafana/contact-point-fixtures.d.ts +28 -0
  4. package/dist/grafana/contact-point-fixtures.d.ts.map +1 -0
  5. package/dist/grafana/contact-point.d.ts +99 -0
  6. package/dist/grafana/contact-point.d.ts.map +1 -0
  7. package/dist/grafana/dashboard-model.d.ts +8 -38
  8. package/dist/grafana/dashboard-model.d.ts.map +1 -1
  9. package/dist/grafana/datasource.d.ts +2 -9
  10. package/dist/grafana/datasource.d.ts.map +1 -1
  11. package/dist/grafana/index.d.ts +10 -4
  12. package/dist/grafana/index.d.ts.map +1 -1
  13. package/dist/grafana/index.js +198 -41
  14. package/dist/grafana/index.js.map +12 -6
  15. package/dist/grafana/message-template.d.ts +70 -0
  16. package/dist/grafana/message-template.d.ts.map +1 -0
  17. package/dist/grafana/mute-timing.d.ts +112 -0
  18. package/dist/grafana/mute-timing.d.ts.map +1 -0
  19. package/dist/grafana/providers.d.ts +1 -1
  20. package/dist/grafana/providers.d.ts.map +1 -1
  21. package/dist/grafana/secret-refs.d.ts +19 -0
  22. package/dist/grafana/secret-refs.d.ts.map +1 -0
  23. package/dist/grafana/subset-match.d.ts +2 -0
  24. package/dist/grafana/subset-match.d.ts.map +1 -0
  25. package/dist/linux/index.d.ts +1 -1
  26. package/dist/linux/index.d.ts.map +1 -1
  27. package/dist/linux/index.js +3 -1
  28. package/dist/linux/index.js.map +1 -1
  29. package/docs/grafana-alerting.md +145 -0
  30. package/docs/grafana.md +4 -3
  31. package/docs/upstream-conformance.md +26 -4
  32. package/package.json +1 -1
  33. package/src/grafana/alerting-provenance.ts +109 -0
  34. package/src/grafana/contact-point-fixtures.ts +25 -0
  35. package/src/grafana/contact-point-provenance.test.ts +113 -0
  36. package/src/grafana/contact-point-secrets.test.ts +117 -0
  37. package/src/grafana/contact-point.test.ts +123 -0
  38. package/src/grafana/contact-point.ts +179 -0
  39. package/src/grafana/dashboard-model.ts +8 -57
  40. package/src/grafana/datasource.ts +5 -27
  41. package/src/grafana/index.ts +24 -4
  42. package/src/grafana/message-template.test.ts +212 -0
  43. package/src/grafana/message-template.ts +114 -0
  44. package/src/grafana/mute-timing.test.ts +227 -0
  45. package/src/grafana/mute-timing.ts +160 -0
  46. package/src/grafana/providers.ts +6 -0
  47. package/src/grafana/secret-refs.ts +48 -0
  48. package/src/grafana/subset-match.ts +71 -0
  49. package/src/linux/index-export.test.ts +25 -0
  50. package/src/linux/index.ts +15 -1
@@ -6,16 +6,17 @@
6
6
  * dashboard-model.ts, fake-grafana.ts) are internals a provider needs but a consumer should not
7
7
  * depend on.
8
8
  *
9
- * `Grafana.Datasource`, `Grafana.Folder` and `Grafana.Dashboard` ship here. `Grafana.AlertRule`
10
- * and friends are still follow-up work on the same now-available SDK operations — see
11
- * docs/grafana.md and docs/grafana-folder-dashboard.md for what shipped and what is still open.
9
+ * `Grafana.Datasource`, `Grafana.Folder`, `Grafana.Dashboard`, `Grafana.ContactPoint`,
10
+ * `Grafana.MuteTiming` and `Grafana.MessageTemplate` ship here. `Grafana.AlertRuleGroup` and
11
+ * `Grafana.NotificationPolicy` are stacked follow-up PRs on the same SDK operations — see
12
+ * docs/grafana.md, docs/grafana-folder-dashboard.md and docs/grafana-alerting.md for what shipped
13
+ * and what is still open.
12
14
  */
13
15
  export {
14
16
  type DatasourceAttributes,
15
17
  type DatasourceProps,
16
18
  GrafanaDatasource,
17
19
  GrafanaDatasourceProvider,
18
- GrafanaSecretRefUnsetError,
19
20
  } from './datasource.ts';
20
21
  export {
21
22
  type DashboardAttributes,
@@ -31,6 +32,25 @@ export {
31
32
  GrafanaFolderProvider,
32
33
  GrafanaFolderReparentError,
33
34
  } from './folder.ts';
35
+ export {
36
+ type ContactPointAttributes,
37
+ type ContactPointProps,
38
+ GrafanaContactPoint,
39
+ GrafanaContactPointProvider,
40
+ } from './contact-point.ts';
41
+ export {
42
+ type MessageTemplateAttributes,
43
+ type MessageTemplateProps,
44
+ GrafanaMessageTemplate,
45
+ GrafanaMessageTemplateProvider,
46
+ } from './message-template.ts';
47
+ export {
48
+ type MuteTimingAttributes,
49
+ type MuteTimingProps,
50
+ GrafanaMuteTiming,
51
+ GrafanaMuteTimingProvider,
52
+ } from './mute-timing.ts';
34
53
  export { GrafanaProvisionedObjectError } from './provisioned.ts';
54
+ export { GrafanaSecretRefUnsetError } from './secret-refs.ts';
35
55
  export { type GrafanaTarget, grafanaCredentials } from './credentials.ts';
36
56
  export { grafanaProviders } from './providers.ts';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `Grafana.MessageTemplate` against a fake Grafana — the PUT-is-both-create-and-update route (no
3
+ * `routePostTemplate` exists), the foreign-provenance refusal, and version-carrying updates.
4
+ * Mirrors `message-template.ts`'s own file header.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import * as Effect from 'effect/Effect';
8
+ import { GrafanaProvisionedObjectError } from './alerting-provenance.ts';
9
+ import { fakeFailure, fakeGrafana, fakeGrafanaLayer } from './fake-grafana.ts';
10
+ import { spec } from './message-template.ts';
11
+ import { grafanaOperations } from './resource.ts';
12
+
13
+ const PATH = '/api/v1/provisioning/templates/slack-body';
14
+
15
+ const liveJson = {
16
+ name: 'slack-body',
17
+ provenance: 'api',
18
+ template: '{{ define "slack-body" }}…{{ end }}',
19
+ version: '3',
20
+ };
21
+ const props = { name: 'slack-body', template: '{{ define "slack-body" }}…{{ end }}' };
22
+
23
+ describe('spec.fetchLive', () => {
24
+ test('a live template decodes into typed attributes', async () => {
25
+ const fake = fakeGrafana((method, url) =>
26
+ method === 'GET' && url.pathname === PATH ? Response.json(liveJson) : fakeFailure(404, 'x'),
27
+ );
28
+ const live = await Effect.runPromise(
29
+ spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
30
+ );
31
+ expect(live !== undefined && spec.attributes(live, props)).toEqual({
32
+ name: 'slack-body',
33
+ provenance: 'api',
34
+ template: '{{ define "slack-body" }}…{{ end }}',
35
+ });
36
+ expect(fake.bodies[0]).toBeUndefined();
37
+ });
38
+
39
+ test('a 404 folds to undefined', async () => {
40
+ const fake = fakeGrafana(() => fakeFailure(404, 'template not found'));
41
+ const result = await Effect.runPromise(
42
+ spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
43
+ );
44
+ expect(result).toBeUndefined();
45
+ });
46
+
47
+ test('a 401 propagates — never folded to absent', async () => {
48
+ const fake = fakeGrafana(() => fakeFailure(401, 'bad token'));
49
+ const failure = await Effect.runPromise(
50
+ Effect.flip(spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch)))),
51
+ );
52
+ expect(failure._tag).toBe('Unauthorized');
53
+ });
54
+ });
55
+
56
+ describe('create and update — the same PUT route', () => {
57
+ test('create sends no version — nothing has been read yet', async () => {
58
+ const fake = fakeGrafana((method, url) =>
59
+ method === 'PUT' && url.pathname === PATH ? Response.json(liveJson) : fakeFailure(404, 'x'),
60
+ );
61
+ await Effect.runPromise(spec.create(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))));
62
+ // ⛔ `name` is a URL path label (`/templates/{name}`), never part of the body — only
63
+ // `template` (and, on update, `version`) travel in the JSON payload.
64
+ expect(fake.bodies[0]).toEqual({ template: props.template });
65
+ });
66
+
67
+ test('an api-provenance template updates and sends the live version for concurrency', async () => {
68
+ const changed = { name: 'slack-body', template: 'v2' };
69
+ const fake = fakeGrafana((method, url) => {
70
+ if (method === 'GET' && url.pathname === PATH) return Response.json(liveJson);
71
+ if (method === 'PUT' && url.pathname === PATH)
72
+ return Response.json({ ...liveJson, template: 'v2' });
73
+ return fakeFailure(500, 'unexpected request');
74
+ });
75
+ await Effect.runPromise(
76
+ grafanaOperations(spec)
77
+ .reconcile(changed)
78
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
79
+ );
80
+ const put = fake.seen.findIndex((s) => s.method === 'PUT');
81
+ expect(fake.bodies[put]).toEqual({ template: 'v2', version: '3' });
82
+ });
83
+
84
+ test('reconcile is a noop when the declared template already matches what is live', async () => {
85
+ const fake = fakeGrafana((method, url) =>
86
+ method === 'GET' && url.pathname === PATH
87
+ ? Response.json(liveJson)
88
+ : fakeFailure(500, 'reconcile should not write when nothing changed'),
89
+ );
90
+ const after = await Effect.runPromise(
91
+ grafanaOperations(spec)
92
+ .reconcile(props)
93
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
94
+ );
95
+ expect(after.name).toBe('slack-body');
96
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe('foreign provenance refuses update and destroy', () => {
101
+ test('a file-provisioned template refuses update and sends no PUT', async () => {
102
+ const fileProvisioned = { ...liveJson, provenance: 'file' };
103
+ const fake = fakeGrafana((method, url) =>
104
+ method === 'GET' && url.pathname === PATH
105
+ ? Response.json(fileProvisioned)
106
+ : fakeFailure(500, 'no write should be attempted'),
107
+ );
108
+ const failure = await Effect.runPromise(
109
+ Effect.flip(
110
+ grafanaOperations(spec)
111
+ .reconcile({ name: 'slack-body', template: 'v2' })
112
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
113
+ ),
114
+ );
115
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
116
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
117
+ });
118
+
119
+ test('a file-provisioned template refuses destroy and sends no DELETE', async () => {
120
+ const fileProvisioned = { ...liveJson, provenance: 'file' };
121
+ const fake = fakeGrafana((method, url) =>
122
+ method === 'GET' && url.pathname === PATH
123
+ ? Response.json(fileProvisioned)
124
+ : fakeFailure(500, 'no DELETE should be attempted'),
125
+ );
126
+ const failure = await Effect.runPromise(
127
+ Effect.flip(
128
+ grafanaOperations(spec)
129
+ .destroy(props)
130
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
131
+ ),
132
+ );
133
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
134
+ expect(fake.seen).toEqual([{ method: 'GET', path: PATH }]);
135
+ });
136
+
137
+ test('deletes a live, api-provenance template', async () => {
138
+ const fake = fakeGrafana((method, url) => {
139
+ if (method === 'GET' && url.pathname === PATH) return Response.json(liveJson);
140
+ if (method === 'DELETE' && url.pathname === PATH) return Response.json({});
141
+ return fakeFailure(500, 'unexpected request');
142
+ });
143
+ await Effect.runPromise(
144
+ grafanaOperations(spec)
145
+ .destroy(props)
146
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
147
+ );
148
+ expect(fake.seen).toEqual([
149
+ { method: 'GET', path: PATH },
150
+ { method: 'DELETE', path: PATH },
151
+ ]);
152
+ });
153
+ });
154
+
155
+ describe('missing provenance fails closed', () => {
156
+ // ⛔ THE REGRESSION AN ADVERSARIAL REVIEW OF THIS PR FOUND — see contact-point.test.ts's own
157
+ // copy of this block for the full account.
158
+ const noProvenanceField: Record<string, unknown> = { ...liveJson };
159
+ delete noProvenanceField.provenance;
160
+
161
+ test('a response with no provenance field at all refuses update', async () => {
162
+ const fake = fakeGrafana((method, url) =>
163
+ method === 'GET' && url.pathname === PATH
164
+ ? Response.json(noProvenanceField)
165
+ : fakeFailure(500, 'no write should be attempted'),
166
+ );
167
+ const failure = await Effect.runPromise(
168
+ Effect.flip(
169
+ grafanaOperations(spec)
170
+ .reconcile({ name: 'slack-body', template: 'v2' })
171
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
172
+ ),
173
+ );
174
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
175
+ expect(failure.message).toContain('did not report a provenance');
176
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
177
+ });
178
+
179
+ test('a response with no provenance field at all refuses destroy', async () => {
180
+ const fake = fakeGrafana((method, url) =>
181
+ method === 'GET' && url.pathname === PATH
182
+ ? Response.json(noProvenanceField)
183
+ : fakeFailure(500, 'no DELETE should be attempted'),
184
+ );
185
+ const failure = await Effect.runPromise(
186
+ Effect.flip(
187
+ grafanaOperations(spec)
188
+ .destroy(props)
189
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
190
+ ),
191
+ );
192
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
193
+ expect(fake.seen).toEqual([{ method: 'GET', path: PATH }]);
194
+ });
195
+
196
+ test('an EXPLICIT empty-string provenance (ProvenanceNone) is writable', async () => {
197
+ const explicitlyNone = { ...liveJson, provenance: '' };
198
+ const fake = fakeGrafana((method, url) => {
199
+ if (method === 'GET' && url.pathname === PATH) return Response.json(explicitlyNone);
200
+ if (method === 'PUT' && url.pathname === PATH) {
201
+ return Response.json({ ...explicitlyNone, template: 'v2' });
202
+ }
203
+ return fakeFailure(500, 'unexpected request');
204
+ });
205
+ await Effect.runPromise(
206
+ grafanaOperations(spec)
207
+ .reconcile({ name: 'slack-body', template: 'v2' })
208
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
209
+ );
210
+ expect(fake.seen.some((s) => s.method === 'PUT')).toBe(true);
211
+ });
212
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `Grafana.MessageTemplate` — one Grafana notification-template group, keyed by `name`.
3
+ *
4
+ * ⚠️ CREATE AND UPDATE ARE THE SAME WIRE CALL — MEASURED: `services/grafana.ts` has no
5
+ * `routePostTemplate` at all, only `routePutTemplate` (`PUT /v1/provisioning/templates/{name}`,
6
+ * upsert by name), `routeGetTemplate`/`routeGetTemplates` and `routeDeleteTemplate`. `spec.create`
7
+ * and `spec.update` both call it — kept as two separate functions anyway (rather than omitting
8
+ * `update` and letting them collapse into one) so the shared engine's `diff` reports a content
9
+ * change as `update`, not the `replace` it would show for any resource whose `spec.update` is
10
+ * undefined (`resource.ts`).
11
+ *
12
+ * ⛔ A FOREIGN-PROVENANCE TEMPLATE REFUSES EVERY UPDATE AND DESTROY — `alerting-provenance.ts`,
13
+ * shared with `Grafana.ContactPoint`/`AlertRuleGroup`/`NotificationPolicy`; see that file's
14
+ * header. `create` is never at risk: it only runs when `fetchLive` already found nothing.
15
+ *
16
+ * ⚠️ `version` IS SENT FOR OPTIMISTIC CONCURRENCY WHEN GRAFANA REPORTS ONE — `RoutePutTemplateRequest.
17
+ * version`'s own doc comment: "Version of template to use for optimistic concurrency. Leave empty
18
+ * to disable validation." Unlike `Grafana.Dashboard`'s 412, no live call confirms what error this
19
+ * produces on a genuine mismatch (no live calls in this PR) — whatever typed error the SDK
20
+ * declares for `routePutTemplate` is left uncaught either way, per the house rule against
21
+ * folding a non-not-found error into absent.
22
+ *
23
+ * ⚠️ `X-Disable-Provenance` IS NEVER SET — same reasoning as `contact-point.ts`'s file header.
24
+ */
25
+ import { Resource } from 'alchemy';
26
+ import * as Provider from 'alchemy/Provider';
27
+ import * as grafana from '@distilled.cloud/grafana';
28
+ import * as Effect from 'effect/Effect';
29
+ import {
30
+ type GrafanaProvisionedObjectError,
31
+ isForeignProvenance,
32
+ refuseIfForeignProvenance,
33
+ } from './alerting-provenance.ts';
34
+ import { type GrafanaSpec, grafanaHandlers } from './resource.ts';
35
+
36
+ export interface MessageTemplateProps {
37
+ name: string;
38
+ /** The Go-template body, e.g. Grafana's own "Export as JSON/YAML" template text. */
39
+ template: string;
40
+ }
41
+
42
+ export interface MessageTemplateAttributes {
43
+ name: string;
44
+ template: string;
45
+ /** `""`/`"api"` (this family may write); anything else, INCLUDING `undefined` (Grafana didn't
46
+ * report the field), refuses — see alerting-provenance.ts's "fails open" note. */
47
+ provenance: string | undefined;
48
+ }
49
+
50
+ export interface GrafanaMessageTemplate extends Resource<
51
+ 'Grafana.MessageTemplate',
52
+ MessageTemplateProps,
53
+ MessageTemplateAttributes,
54
+ never
55
+ > {}
56
+
57
+ export const GrafanaMessageTemplate = Resource<GrafanaMessageTemplate>('Grafana.MessageTemplate');
58
+
59
+ export const spec: GrafanaSpec<
60
+ MessageTemplateProps,
61
+ grafana.NotificationTemplate,
62
+ MessageTemplateAttributes,
63
+ | grafana.RouteGetTemplateError
64
+ | grafana.RoutePutTemplateError
65
+ | grafana.RouteDeleteTemplateError
66
+ | GrafanaProvisionedObjectError
67
+ > = {
68
+ attributes: (live) => ({
69
+ name: live.name ?? '',
70
+ // ⛔ NEVER `?? ''` — see alerting-provenance.ts's "fails open" note.
71
+ provenance: live.provenance,
72
+ template: live.template ?? '',
73
+ }),
74
+ create: (props) => grafana.routePutTemplate({ name: props.name, template: props.template }),
75
+ destroy: (props, live) =>
76
+ Effect.gen(function* () {
77
+ if (isForeignProvenance(live.provenance)) {
78
+ return yield* refuseIfForeignProvenance(
79
+ 'Grafana.MessageTemplate',
80
+ props.name,
81
+ live.provenance,
82
+ );
83
+ }
84
+ yield* grafana.routeDeleteTemplate({ name: props.name });
85
+ }),
86
+ fetchLive: (props) =>
87
+ grafana
88
+ .routeGetTemplate({ name: props.name })
89
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
90
+ matches: (attributes, props) => attributes.template === props.template,
91
+ update: (props, live) =>
92
+ Effect.gen(function* () {
93
+ if (isForeignProvenance(live.provenance)) {
94
+ return yield* refuseIfForeignProvenance(
95
+ 'Grafana.MessageTemplate',
96
+ props.name,
97
+ live.provenance,
98
+ );
99
+ }
100
+ yield* grafana.routePutTemplate({
101
+ name: props.name,
102
+ template: props.template,
103
+ ...(live.version === undefined ? {} : { version: live.version }),
104
+ });
105
+ }),
106
+ };
107
+
108
+ export const handlers = grafanaHandlers(spec);
109
+
110
+ export const GrafanaMessageTemplateProvider = () =>
111
+ Provider.effect(
112
+ GrafanaMessageTemplate,
113
+ Effect.succeed(GrafanaMessageTemplate.Provider.of(handlers)),
114
+ );
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `Grafana.MuteTiming` against a fake Grafana — the widened-type `provenance`/`version` read (see
3
+ * the file's own header for why the SDK's declared `MuteTimeInterval` type undersells the runtime
4
+ * value), the foreign-provenance refusal, and opaque time-interval passthrough (weekdays/times
5
+ * fields the SDK's own `TimeInterval` type does not model either). Mirrors `contact-point.test.ts`.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import * as Effect from 'effect/Effect';
9
+ import { GrafanaProvisionedObjectError } from './alerting-provenance.ts';
10
+ import { fakeFailure, fakeGrafana, fakeGrafanaLayer } from './fake-grafana.ts';
11
+ import { spec } from './mute-timing.ts';
12
+ import { grafanaOperations } from './resource.ts';
13
+
14
+ const PATH = '/api/v1/provisioning/mute-timings/weekends';
15
+ const LIST_PATH = '/api/v1/provisioning/mute-timings';
16
+
17
+ const timeIntervals = [
18
+ { times: [{ end_time: '09:00', start_time: '17:00' }], weekdays: ['friday'] },
19
+ ];
20
+
21
+ const liveJson = {
22
+ name: 'weekends',
23
+ provenance: 'api',
24
+ time_intervals: timeIntervals,
25
+ version: 'v1',
26
+ };
27
+ const props = { name: 'weekends', timeIntervals };
28
+
29
+ describe('spec.fetchLive', () => {
30
+ test('provenance and the real weekday/time fields survive decode despite the SDK type', async () => {
31
+ const fake = fakeGrafana((method, url) =>
32
+ method === 'GET' && url.pathname === PATH ? Response.json(liveJson) : fakeFailure(404, 'x'),
33
+ );
34
+ const live = await Effect.runPromise(
35
+ spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
36
+ );
37
+ expect(live !== undefined && spec.attributes(live, props)).toEqual({
38
+ name: 'weekends',
39
+ provenance: 'api',
40
+ timeIntervals,
41
+ });
42
+ expect(fake.bodies[0]).toBeUndefined();
43
+ });
44
+
45
+ test('a 404 folds to undefined', async () => {
46
+ const fake = fakeGrafana(() => fakeFailure(404, 'mute timing not found'));
47
+ const result = await Effect.runPromise(
48
+ spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
49
+ );
50
+ expect(result).toBeUndefined();
51
+ });
52
+
53
+ test('a 403 propagates — never folded to absent', async () => {
54
+ const fake = fakeGrafana(() => fakeFailure(403, 'forbidden'));
55
+ const failure = await Effect.runPromise(
56
+ Effect.flip(spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch)))),
57
+ );
58
+ expect(failure._tag).toBe('Forbidden');
59
+ });
60
+ });
61
+
62
+ describe('create', () => {
63
+ test('sends the real weekday/time fields on the wire — not dropped by the SDK type either', async () => {
64
+ const fake = fakeGrafana((method, url) =>
65
+ method === 'POST' && url.pathname === LIST_PATH
66
+ ? Response.json(liveJson)
67
+ : fakeFailure(404, 'x'),
68
+ );
69
+ await Effect.runPromise(spec.create(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))));
70
+ expect(fake.bodies[0]).toEqual({ name: 'weekends', time_intervals: timeIntervals });
71
+ });
72
+ });
73
+
74
+ describe('subset-match array rule — pinned, not re-measured live', () => {
75
+ // ⛔ FLAGGED BY AN ADVERSARIAL REVIEW OF THIS PR: `subset-match.ts`'s array comparison is exact
76
+ // length, then element-wise — unverified against a real Grafana instance for whether it ever
77
+ // pads or appends a default entry to `time_intervals` (no live mute timing exists in this house
78
+ // to observe). This test pins TODAY's behavior — a live observation that finds Grafana doing so
79
+ // would make this test itself the place to change, not a surprise discovered elsewhere.
80
+ test('live carrying one extra time-interval entry the declaration never mentioned still shows update', async () => {
81
+ const extraEntry = {
82
+ ...liveJson,
83
+ time_intervals: [...timeIntervals, { weekdays: ['sunday'] }],
84
+ };
85
+ const fake = fakeGrafana((method, url) =>
86
+ method === 'GET' && url.pathname === PATH ? Response.json(extraEntry) : fakeFailure(500, 'x'),
87
+ );
88
+ const live = await Effect.runPromise(
89
+ spec.fetchLive(props).pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
90
+ );
91
+ const attrs = live !== undefined ? spec.attributes(live, props) : undefined;
92
+ expect(attrs !== undefined && spec.matches(attrs, props)).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe('reconcile — adopt semantics', () => {
97
+ test('declaring exactly what is live is a noop: no write is ever sent', async () => {
98
+ const fake = fakeGrafana((method, url) =>
99
+ method === 'GET' && url.pathname === PATH
100
+ ? Response.json(liveJson)
101
+ : fakeFailure(500, 'reconcile should not write when nothing changed'),
102
+ );
103
+ const after = await Effect.runPromise(
104
+ grafanaOperations(spec)
105
+ .reconcile(props)
106
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
107
+ );
108
+ expect(after.name).toBe('weekends');
109
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
110
+ });
111
+ });
112
+
113
+ describe('foreign provenance refuses update and destroy', () => {
114
+ test('a file-provisioned mute timing refuses update and sends no PUT', async () => {
115
+ const fileProvisioned = { ...liveJson, provenance: 'file' };
116
+ const fake = fakeGrafana((method, url) =>
117
+ method === 'GET' && url.pathname === PATH
118
+ ? Response.json(fileProvisioned)
119
+ : fakeFailure(500, 'no write should be attempted'),
120
+ );
121
+ const changed = { name: 'weekends', timeIntervals: [{ weekdays: ['saturday'] }] };
122
+ const failure = await Effect.runPromise(
123
+ Effect.flip(
124
+ grafanaOperations(spec)
125
+ .reconcile(changed)
126
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
127
+ ),
128
+ );
129
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
130
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
131
+ });
132
+
133
+ test('a file-provisioned mute timing refuses destroy and sends no DELETE', async () => {
134
+ const fileProvisioned = { ...liveJson, provenance: 'file' };
135
+ const fake = fakeGrafana((method, url) =>
136
+ method === 'GET' && url.pathname === PATH
137
+ ? Response.json(fileProvisioned)
138
+ : fakeFailure(500, 'no DELETE should be attempted'),
139
+ );
140
+ const failure = await Effect.runPromise(
141
+ Effect.flip(
142
+ grafanaOperations(spec)
143
+ .destroy(props)
144
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
145
+ ),
146
+ );
147
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
148
+ expect(fake.seen).toEqual([{ method: 'GET', path: PATH }]);
149
+ });
150
+
151
+ test('deletes a live, api-provenance mute timing, sending its version for concurrency', async () => {
152
+ const fake = fakeGrafana((method, url) => {
153
+ if (method === 'GET' && url.pathname === PATH) return Response.json(liveJson);
154
+ if (method === 'DELETE' && url.pathname === PATH) return Response.json({});
155
+ return fakeFailure(500, 'unexpected request');
156
+ });
157
+ await Effect.runPromise(
158
+ grafanaOperations(spec)
159
+ .destroy(props)
160
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
161
+ );
162
+ const del = fake.seen.findIndex((s) => s.method === 'DELETE');
163
+ expect(del).toBeGreaterThanOrEqual(0);
164
+ expect(fake.seen[del]?.path).toBe(`${PATH}?version=v1`);
165
+ });
166
+ });
167
+
168
+ describe('missing provenance fails closed', () => {
169
+ // ⛔ THE REGRESSION AN ADVERSARIAL REVIEW OF THIS PR FOUND — see contact-point.test.ts's own
170
+ // copy of this block for the full account. This resource is the one where it matters most:
171
+ // `provenance` isn't even in the SDK's declared `MuteTimeInterval` type, so "the field is
172
+ // missing" is a real, plausible shape for a response to take, not a hypothetical.
173
+ const noProvenanceField: Record<string, unknown> = { ...liveJson };
174
+ delete noProvenanceField.provenance;
175
+
176
+ test('a response with no provenance field at all refuses update', async () => {
177
+ const fake = fakeGrafana((method, url) =>
178
+ method === 'GET' && url.pathname === PATH
179
+ ? Response.json(noProvenanceField)
180
+ : fakeFailure(500, 'no write should be attempted'),
181
+ );
182
+ const changed = { name: 'weekends', timeIntervals: [{ weekdays: ['saturday'] }] };
183
+ const failure = await Effect.runPromise(
184
+ Effect.flip(
185
+ grafanaOperations(spec)
186
+ .reconcile(changed)
187
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
188
+ ),
189
+ );
190
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
191
+ expect(failure.message).toContain('did not report a provenance');
192
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
193
+ });
194
+
195
+ test('a response with no provenance field at all refuses destroy', async () => {
196
+ const fake = fakeGrafana((method, url) =>
197
+ method === 'GET' && url.pathname === PATH
198
+ ? Response.json(noProvenanceField)
199
+ : fakeFailure(500, 'no DELETE should be attempted'),
200
+ );
201
+ const failure = await Effect.runPromise(
202
+ Effect.flip(
203
+ grafanaOperations(spec)
204
+ .destroy(props)
205
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
206
+ ),
207
+ );
208
+ expect(failure).toBeInstanceOf(GrafanaProvisionedObjectError);
209
+ expect(fake.seen).toEqual([{ method: 'GET', path: PATH }]);
210
+ });
211
+
212
+ test('an EXPLICIT empty-string provenance (ProvenanceNone) is writable', async () => {
213
+ const explicitlyNone = { ...liveJson, provenance: '' };
214
+ const fake = fakeGrafana((method, url) => {
215
+ if (method === 'GET' && url.pathname === PATH) return Response.json(explicitlyNone);
216
+ if (method === 'PUT' && url.pathname === PATH) return Response.json(explicitlyNone);
217
+ return fakeFailure(500, 'unexpected request');
218
+ });
219
+ const changed = { name: 'weekends', timeIntervals: [{ weekdays: ['saturday'] }] };
220
+ await Effect.runPromise(
221
+ grafanaOperations(spec)
222
+ .reconcile(changed)
223
+ .pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))),
224
+ );
225
+ expect(fake.seen.some((s) => s.method === 'PUT')).toBe(true);
226
+ });
227
+ });