@intentius/chant-lexicon-k8s 0.57.0 → 0.59.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 (89) hide show
  1. package/dist/api/fake-cluster.d.ts +7 -6
  2. package/dist/api/fake-cluster.d.ts.map +1 -1
  3. package/dist/api/sweep-types.d.ts +1 -2
  4. package/dist/api/sweep-types.d.ts.map +1 -1
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/composites/cron-schedule.d.ts +26 -0
  7. package/dist/composites/cron-schedule.d.ts.map +1 -0
  8. package/dist/composites/cron-workload.d.ts +1 -1
  9. package/dist/composites/cron-workload.d.ts.map +1 -1
  10. package/dist/composites/operator-stack.d.ts +20 -19
  11. package/dist/composites/operator-stack.d.ts.map +1 -1
  12. package/dist/config-schema.d.ts +3 -0
  13. package/dist/config-schema.d.ts.map +1 -1
  14. package/dist/config.d.ts +26 -4
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/deep-observe-hooks.d.ts +4 -4
  17. package/dist/deep-observe.d.ts +23 -16
  18. package/dist/deep-observe.d.ts.map +1 -1
  19. package/dist/describe-resources.d.ts.map +1 -1
  20. package/dist/effect-receipt-row.d.ts +164 -0
  21. package/dist/effect-receipt-row.d.ts.map +1 -0
  22. package/dist/export-resources.d.ts +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/integrity.json +4 -4
  26. package/dist/lint/audit-catalog.d.ts.map +1 -1
  27. package/dist/lint/audit-lineage.d.ts +10 -0
  28. package/dist/lint/audit-lineage.d.ts.map +1 -0
  29. package/dist/lint/post-synth/wk8505.d.ts +3 -1
  30. package/dist/lint/post-synth/wk8505.d.ts.map +1 -1
  31. package/dist/manifest.json +1 -1
  32. package/dist/okf/index.md +1 -1
  33. package/dist/okf/rules/WK8505.md +2 -2
  34. package/dist/okf/types/Kustomization.md +1 -1
  35. package/dist/op/activities/argo.d.ts +4 -4
  36. package/dist/op/activities/argo.d.ts.map +1 -1
  37. package/dist/op/activities/index.d.ts +15 -7
  38. package/dist/op/activities/index.d.ts.map +1 -1
  39. package/dist/op/activities/kubectl.d.ts +7 -7
  40. package/dist/op/activities/kubectl.d.ts.map +1 -1
  41. package/dist/op/activities/wait-for-ready.d.ts +6 -5
  42. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  43. package/dist/op/builders.d.ts +13 -13
  44. package/dist/plugin.d.ts.map +1 -1
  45. package/dist/receipt-store.d.ts +113 -0
  46. package/dist/receipt-store.d.ts.map +1 -0
  47. package/dist/rules/wk8505.ts +4 -2
  48. package/dist/serializer.d.ts.map +1 -1
  49. package/dist/skills/chant-k8s-argo.md +27 -13
  50. package/dist/subscribe-changes.d.ts +84 -0
  51. package/dist/subscribe-changes.d.ts.map +1 -0
  52. package/package.json +3 -3
  53. package/src/api/fake-cluster.ts +17 -4
  54. package/src/api/sweep-types.ts +1 -2
  55. package/src/codegen/docs.ts +7 -0
  56. package/src/composites/composites.test.ts +72 -3
  57. package/src/composites/cron-schedule.ts +47 -0
  58. package/src/composites/cron-workload.ts +4 -1
  59. package/src/composites/operator-stack.ts +25 -24
  60. package/src/config-schema.ts +5 -0
  61. package/src/config.ts +27 -4
  62. package/src/crd/crd-sources.ts +4 -4
  63. package/src/deep-observe-hooks.ts +4 -4
  64. package/src/deep-observe.test.ts +219 -46
  65. package/src/deep-observe.ts +54 -28
  66. package/src/describe-resources.ts +31 -7
  67. package/src/effect-receipt-row.test.ts +285 -0
  68. package/src/effect-receipt-row.ts +268 -0
  69. package/src/export-resources.ts +1 -1
  70. package/src/index.ts +22 -0
  71. package/src/lint/audit-catalog.ts +6 -2
  72. package/src/lint/audit-lineage.ts +127 -0
  73. package/src/lint/post-synth/wk8505.ts +4 -2
  74. package/src/op/activities/argo.test.ts +7 -8
  75. package/src/op/activities/argo.ts +4 -4
  76. package/src/op/activities/index.ts +23 -7
  77. package/src/op/activities/kubectl.test.ts +43 -4
  78. package/src/op/activities/kubectl.ts +67 -67
  79. package/src/op/activities/wait-for-ready.test.ts +2 -2
  80. package/src/op/activities/wait-for-ready.ts +7 -7
  81. package/src/op/builders.ts +13 -13
  82. package/src/plugin.ts +16 -1
  83. package/src/receipt-store.test.ts +380 -0
  84. package/src/receipt-store.ts +290 -0
  85. package/src/serializer.test.ts +26 -0
  86. package/src/serializer.ts +92 -1
  87. package/src/skills/chant-k8s-argo.md +27 -13
  88. package/src/subscribe-changes.test.ts +368 -0
  89. package/src/subscribe-changes.ts +210 -0
package/src/plugin.ts CHANGED
@@ -632,7 +632,7 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
632
632
  {
633
633
  file: "chant-k8s-argo.md",
634
634
  name: "chant-k8s-argo",
635
- description: "Argo CD composites — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and the Argo-vs-Temporal split",
635
+ description: "Argo CD composites — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and how a deploy splits between Argo and a chant Op",
636
636
  triggers: [
637
637
  { type: "context", value: "argo" },
638
638
  { type: "context", value: "argo cd" },
@@ -738,6 +738,21 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
738
738
  return exportResources(options);
739
739
  },
740
740
 
741
+ /**
742
+ * The change signal (#1981). A Watch on every declared kind, reported as a
743
+ * bare `onChange()` that wakes an operator tick early. Nothing a frame
744
+ * carries reaches the tick: the seam has no payload, and the tick that runs
745
+ * is the tick the timer would have run. Kubernetes is the one substrate
746
+ * chant reaches whose change stream needs nothing deployed into the observed
747
+ * cluster; see the verdict table in the operator guide. Implementation in
748
+ * ./subscribe-changes.ts, dynamically imported for the same reason
749
+ * describeResources is.
750
+ */
751
+ async subscribeChanges(options) {
752
+ const { subscribeChanges } = await import("./subscribe-changes");
753
+ return subscribeChanges(options);
754
+ },
755
+
741
756
  // Property-level live drift via SSA managed-fields (#1076, epic #1073).
742
757
  // The reader lives in ./deep-observe.ts, loaded only through this dynamic
743
758
  // import — same reason describeResources/exportResources are — so the API
@@ -0,0 +1,380 @@
1
+ /**
2
+ * The k8s ConfigMap receipt row, end to end (#2074, epic #1703).
3
+ *
4
+ * Every leg runs against ./api/fake-cluster.ts, a real
5
+ * `@intentius/chant-k8s-client` with only the HTTP send replaced, so no
6
+ * ambient kubeconfig is read and no cluster is contacted. The three legs the
7
+ * row has to close:
8
+ *
9
+ * 1. the `effect()` step materializes the receipt (the store's `write`);
10
+ * 2. the k8s observation reads it back (`describeResources` and the deep
11
+ * read), with the stored value on `attributes.value` where core's
12
+ * `readReceiptValue` looks for it;
13
+ * 3. WatchOp's staleness phase (`receiptStaleness`, #1834) fires on it.
14
+ *
15
+ * Plus the two things a receipt must NOT do: show up as property drift or as
16
+ * an unclaimed field, and be swept by the owned-only prune.
17
+ */
18
+
19
+ import { describe, test, expect } from "vitest";
20
+ import type { K8sObject } from "@intentius/chant-k8s-client";
21
+
22
+ const { k8sReceiptStore, observeReceiptRows, observeReceiptRowsDeep, receiptRowsFor, receiptValueOf } =
23
+ await import("./receipt-store");
24
+ const { describeResources } = await import("./describe-resources");
25
+ const { observeResourcesDeepK8s } = await import("./deep-observe");
26
+ const { k8sDeepNormalizationHooks } = await import("./deep-observe-hooks");
27
+ const { fakeCluster, objectKey } = await import("./api/fake-cluster");
28
+ const {
29
+ EffectReceipt,
30
+ receiptConfigMapRef,
31
+ renderReceiptComment,
32
+ K8S_EFFECT_RECEIPT_ENTITY_TYPE,
33
+ RECEIPT_DATA_KEY,
34
+ RECEIPT_LABEL_KEY,
35
+ } = await import("./effect-receipt-row");
36
+ const { receiptActivities, receiptCheckInput } = await import("@intentius/chant/op/receipt-store");
37
+ const { receiptExpectation, EXISTENCE_EXPECTATION } = await import("@intentius/chant/effect-receipt");
38
+ const { readReceiptValue, planReceipts, observedValueResolver } = await import(
39
+ "@intentius/chant/lifecycle/receipt-plan"
40
+ );
41
+ const { diffDeepObservation } = await import("@intentius/chant/lifecycle/deep-observe");
42
+ const { normalizeDeepObservation } = await import("@intentius/chant/deep-observation");
43
+ const { statusBody } = await import("@intentius/chant-k8s-client/testing");
44
+
45
+ const IDENTITY = { stack: "demo", environment: "dev", namespace: "default" } as const;
46
+ const REF = receiptConfigMapRef("demo", "dev", "db-seed");
47
+
48
+ const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { version: "0042" } });
49
+ const EXPECTED = receiptExpectation(seeded);
50
+
51
+ /** The build output the serializer produces for a project declaring `seeded`. */
52
+ function buildOutput(namespace = "default"): string {
53
+ return `${renderReceiptComment({
54
+ seeded: {
55
+ kind: "ConfigMap",
56
+ namespace,
57
+ name: REF.name,
58
+ data: { [RECEIPT_DATA_KEY]: EXPECTED },
59
+ },
60
+ })}\n`;
61
+ }
62
+
63
+ /** A live receipt ConfigMap holding `value`. */
64
+ function liveReceipt(value: string, namespace = "default"): K8sObject {
65
+ return {
66
+ apiVersion: "v1",
67
+ kind: "ConfigMap",
68
+ metadata: {
69
+ name: REF.name,
70
+ namespace,
71
+ uid: "uid-receipt",
72
+ resourceVersion: "3",
73
+ labels: {
74
+ "app.kubernetes.io/managed-by": "chant",
75
+ "chant.intentius.io/stack": "demo",
76
+ "chant.intentius.io/env": "dev",
77
+ [RECEIPT_LABEL_KEY]: "db-seed",
78
+ },
79
+ },
80
+ data: { [RECEIPT_DATA_KEY]: value },
81
+ } as K8sObject;
82
+ }
83
+
84
+ /** Echo an applied object back, the way an API server does. */
85
+ const echoApplies = (req: { method: string; body?: unknown }) =>
86
+ req.method === "PATCH" ? { body: JSON.parse(String(req.body)) } : undefined;
87
+
88
+ const store = (cluster: { connector: unknown }, namespace = "default") =>
89
+ k8sReceiptStore({
90
+ ...IDENTITY,
91
+ namespace,
92
+ connect: cluster.connector as never,
93
+ });
94
+
95
+ // ── Leg 1: the effect step materializes the receipt ─────────────────────────
96
+
97
+ describe("k8sReceiptStore: the effect() step's sole write", () => {
98
+ test("read answers undefined for a receipt that is not there", async () => {
99
+ const cluster = fakeCluster();
100
+ expect(await store(cluster).read({ name: "seeded", effect: "db-seed", flavor: "hash", inputs: {} })).toBeUndefined();
101
+ });
102
+
103
+ test("write applies a ConfigMap at the derived address, ownership-marked and receipt-labelled", async () => {
104
+ const cluster = fakeCluster({ respond: echoApplies });
105
+ await store(cluster).write({ name: "seeded", effect: "db-seed", flavor: "hash", inputs: {} }, EXPECTED);
106
+
107
+ const patch = cluster.layer.requests.find((r) => r.method === "PATCH");
108
+ expect(patch?.path).toBe(`/api/v1/namespaces/default/configmaps/${REF.name}`);
109
+ const applied = JSON.parse(String(patch?.body)) as K8sObject;
110
+ expect(applied.kind).toBe("ConfigMap");
111
+ expect(applied.metadata?.name).toBe("chant-receipt.demo.dev.db-seed");
112
+ expect(applied.metadata?.namespace).toBe("default");
113
+ expect((applied as { data?: Record<string, string> }).data).toEqual({ [RECEIPT_DATA_KEY]: EXPECTED });
114
+ expect(applied.metadata?.labels).toMatchObject({
115
+ "app.kubernetes.io/managed-by": "chant",
116
+ "chant.intentius.io/stack": "demo",
117
+ "chant.intentius.io/env": "dev",
118
+ [RECEIPT_LABEL_KEY]: "db-seed",
119
+ });
120
+ expect(patch?.path).not.toContain("secrets");
121
+ });
122
+
123
+ test("write lands in the configured receipt namespace", async () => {
124
+ const cluster = fakeCluster({ respond: echoApplies });
125
+ await store(cluster, "chant-system").write(
126
+ { name: "seeded", effect: "db-seed", flavor: "existence", inputs: {} },
127
+ EXISTENCE_EXPECTATION,
128
+ );
129
+ const patch = cluster.layer.requests.find((r) => r.method === "PATCH");
130
+ expect(patch?.path).toBe(`/api/v1/namespaces/chant-system/configmaps/${REF.name}`);
131
+ });
132
+
133
+ test("read gets back exactly what write stored, the round trip the effect step compares on", async () => {
134
+ const cluster = fakeCluster({
135
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED) },
136
+ });
137
+ const activities = receiptActivities(store(cluster));
138
+ const result = await activities.receiptRead(receiptCheckInput(seeded));
139
+ expect(result.current).toBe(EXPECTED);
140
+ expect(result.expectation).toBe(EXPECTED);
141
+ expect(result.applied).toBe(true);
142
+ });
143
+
144
+ test("receiptValueOf reads only the expectation key, and nothing from an unrelated ConfigMap", () => {
145
+ expect(receiptValueOf(liveReceipt("x"))).toBe("x");
146
+ expect(receiptValueOf({ apiVersion: "v1", kind: "ConfigMap", data: { other: "x" } } as K8sObject)).toBeUndefined();
147
+ expect(receiptValueOf(undefined)).toBeUndefined();
148
+ });
149
+ });
150
+
151
+ // ── Leg 2: the observation reads it back ────────────────────────────────────
152
+
153
+ describe("the k8s observation reads the receipt back", () => {
154
+ const entities = new Map([["seeded", { entityType: K8S_EFFECT_RECEIPT_ENTITY_TYPE, props: {} }]]);
155
+
156
+ test("describeResources maps the stored value onto attributes.value, where the plan reads it", async () => {
157
+ const cluster = fakeCluster({
158
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED) },
159
+ });
160
+ const result = await describeResources(
161
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
162
+ cluster.connector,
163
+ );
164
+
165
+ expect(result.resources.seeded.type).toBe(K8S_EFFECT_RECEIPT_ENTITY_TYPE);
166
+ expect(result.resources.seeded.status).toBe("EXTERNAL");
167
+ expect(result.resources.seeded.ownership).toBe("owned");
168
+ expect(readReceiptValue(result.resources.seeded.attributes)).toBe(EXPECTED);
169
+ expect(result.unobserved?.seeded).toBeUndefined();
170
+ });
171
+
172
+ test("a receipt that is not there is a real absence, in neither map", async () => {
173
+ const cluster = fakeCluster();
174
+ const result = await describeResources(
175
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
176
+ cluster.connector,
177
+ );
178
+ expect(result.resources.seeded).toBeUndefined();
179
+ expect(result.unobserved?.seeded).toBeUndefined();
180
+ });
181
+
182
+ test("a failed read is a hole with a reason, never 'the effect never ran'", async () => {
183
+ const cluster = fakeCluster({
184
+ respond: (req) =>
185
+ req.path.includes("/configmaps/")
186
+ ? { status: 403, body: statusBody(403, "Forbidden", "configmaps is forbidden") }
187
+ : undefined,
188
+ });
189
+ const result = await describeResources(
190
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
191
+ cluster.connector,
192
+ );
193
+ expect(result.resources.seeded).toBeUndefined();
194
+ expect(result.unobserved?.seeded.reason).toBeDefined();
195
+ expect(result.unobserved?.seeded.detail).toContain(REF.name);
196
+ });
197
+
198
+ test("without a receipt block in the build output nothing is read, and the entity is not invented", async () => {
199
+ const cluster = fakeCluster({
200
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED) },
201
+ });
202
+ expect(receiptRowsFor(["seeded"], "")).toEqual(new Map());
203
+ const result = await describeResources(
204
+ { environment: "dev", buildOutput: "", entityNames: ["seeded"], entities },
205
+ cluster.connector,
206
+ );
207
+ expect(result.resources.seeded).toBeUndefined();
208
+ });
209
+
210
+ test("the plan turns the reading into an effect row when stale and a noop when applied (#1832)", async () => {
211
+ const cluster = fakeCluster({
212
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt("sha256:something-else") },
213
+ });
214
+ const observed = await describeResources(
215
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
216
+ cluster.connector,
217
+ );
218
+ const reading = {
219
+ observed: true,
220
+ present: true,
221
+ value: readReceiptValue(observed.resources.seeded.attributes),
222
+ lexicon: "k8s",
223
+ };
224
+ const stale = planReceipts(
225
+ new Map([["seeded", seeded]]),
226
+ new Map([["seeded", reading]]),
227
+ observedValueResolver(observed.resources),
228
+ );
229
+ expect(stale).toHaveLength(1);
230
+ expect(stale[0].action).toBe("effect");
231
+
232
+ const applied = planReceipts(
233
+ new Map([["seeded", seeded]]),
234
+ new Map([["seeded", { ...reading, value: EXPECTED }]]),
235
+ observedValueResolver(observed.resources),
236
+ );
237
+ expect(applied[0].action).toBe("noop");
238
+ });
239
+ });
240
+
241
+ // ── The receipt is neither drift nor an unclaimed field ─────────────────────
242
+
243
+ describe("a receipt is never drift and never an unclaimed field (#2160)", () => {
244
+ const entities = new Map([["seeded", { entityType: K8S_EFFECT_RECEIPT_ENTITY_TYPE, props: {} }]]);
245
+
246
+ test("the deep read reports the receipt observed, with no property paths at all", async () => {
247
+ const cluster = fakeCluster({
248
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED) },
249
+ });
250
+ const deep = await observeResourcesDeepK8s(
251
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
252
+ cluster.connector,
253
+ );
254
+ expect(deep.resources.seeded.properties).toEqual({});
255
+ expect(deep.resources.seeded.physicalId).toBe("uid-receipt");
256
+ expect(deep.unobserved?.seeded).toBeUndefined();
257
+ });
258
+
259
+ test("the deep diff proposes nothing for it: no field drift, no unclaimed path", async () => {
260
+ const cluster = fakeCluster({
261
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt("sha256:stale") },
262
+ });
263
+ const deep = await observeResourcesDeepK8s(
264
+ { environment: "dev", buildOutput: buildOutput(), entityNames: ["seeded"], entities },
265
+ cluster.connector,
266
+ );
267
+ const diff = diffDeepObservation(entities, normalizeDeepObservation(deep), k8sDeepNormalizationHooks);
268
+ expect(diff.drifted).toEqual([]);
269
+ expect(diff.unclaimed).toEqual([]);
270
+ expect(diff.held).toEqual([]);
271
+ expect(diff.unobserved).toEqual([]);
272
+ expect(diff.undeclaredEntities).toEqual([]);
273
+ expect(diff.unchanged).toEqual(["seeded"]);
274
+ });
275
+
276
+ test("the receipt ConfigMap is not swept into any other entity's observation", async () => {
277
+ const cluster = fakeCluster({
278
+ objects: {
279
+ [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED),
280
+ [objectKey("v1", "ConfigMap", "app-config", "default")]: {
281
+ apiVersion: "v1",
282
+ kind: "ConfigMap",
283
+ metadata: { name: "app-config", namespace: "default", uid: "uid-app" },
284
+ data: { a: "1" },
285
+ } as K8sObject,
286
+ },
287
+ });
288
+ const result = await describeResources(
289
+ {
290
+ environment: "dev",
291
+ buildOutput: buildOutput(),
292
+ entityNames: ["seeded", "appConfig"],
293
+ entities: new Map([
294
+ ["seeded", { entityType: K8S_EFFECT_RECEIPT_ENTITY_TYPE, props: {} }],
295
+ [
296
+ "appConfig",
297
+ { entityType: "K8s::Core::ConfigMap", props: { metadata: { name: "app-config", namespace: "default" } } },
298
+ ],
299
+ ]),
300
+ },
301
+ cluster.connector,
302
+ );
303
+ expect(Object.keys(result.resources).sort()).toEqual(["appConfig", "seeded"]);
304
+ expect(result.resources.appConfig.physicalId).toBe("uid-app");
305
+ });
306
+ });
307
+
308
+ // ── Leg 3: WatchOp staleness ────────────────────────────────────────────────
309
+
310
+ describe("WatchOp stale-receipt reporting over the k8s row (#1834)", () => {
311
+ const inputs = [receiptCheckInput(seeded)];
312
+
313
+ test("an absent receipt is reported stale, and nothing is written", async () => {
314
+ const cluster = fakeCluster();
315
+ const { receiptStaleness } = receiptActivities(store(cluster));
316
+ const result = await receiptStaleness({ receipts: inputs });
317
+ expect(result.stale).toBe(true);
318
+ expect(result.findings).toEqual([
319
+ { receipt: "seeded", effect: "db-seed", kind: "absent", expected: EXPECTED },
320
+ ]);
321
+ expect(cluster.layer.requests.some((r) => r.method === "PATCH" || r.method === "DELETE")).toBe(false);
322
+ });
323
+
324
+ test("a receipt holding a different value is reported stale with both values", async () => {
325
+ const cluster = fakeCluster({
326
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt("sha256:old") },
327
+ });
328
+ const { receiptStaleness } = receiptActivities(store(cluster));
329
+ const result = await receiptStaleness({ receipts: inputs });
330
+ expect(result.stale).toBe(true);
331
+ expect(result.findings[0]).toEqual({
332
+ receipt: "seeded",
333
+ effect: "db-seed",
334
+ kind: "differs",
335
+ expected: EXPECTED,
336
+ current: "sha256:old",
337
+ });
338
+ });
339
+
340
+ test("a receipt that matches is not stale", async () => {
341
+ const cluster = fakeCluster({
342
+ objects: { [objectKey("v1", "ConfigMap", REF.name, "default")]: liveReceipt(EXPECTED) },
343
+ });
344
+ const { receiptStaleness } = receiptActivities(store(cluster));
345
+ expect(await receiptStaleness({ receipts: inputs })).toEqual({ stale: false, findings: [] });
346
+ });
347
+
348
+ test("the activities barrel exports the three receipt activities by the names the registry resolves", async () => {
349
+ const barrel = await import("./op/activities/index");
350
+ expect(typeof barrel.receiptRead).toBe("function");
351
+ expect(typeof barrel.receiptWrite).toBe("function");
352
+ expect(typeof barrel.receiptStaleness).toBe("function");
353
+ });
354
+ });
355
+
356
+ // ── The observation leg's own contract ──────────────────────────────────────
357
+
358
+ describe("observeReceiptRows", () => {
359
+ test("reads only the entities the caller asked about", () => {
360
+ const rows = receiptRowsFor(["seeded"], `${buildOutput()}`);
361
+ expect([...rows.keys()]).toEqual(["seeded"]);
362
+ expect(receiptRowsFor(["other"], buildOutput()).size).toBe(0);
363
+ });
364
+
365
+ test("the deep leg reports the same holes the thin leg does", async () => {
366
+ const cluster = fakeCluster({
367
+ respond: (req) =>
368
+ req.path.includes("/configmaps/")
369
+ ? { status: 500, body: statusBody(500, "InternalError", "boom") }
370
+ : undefined,
371
+ });
372
+ const { client } = await cluster.connector({ environment: "dev" });
373
+ const rows = receiptRowsFor(["seeded"], buildOutput());
374
+ const thin = await observeReceiptRows(client, rows);
375
+ const deep = await observeReceiptRowsDeep(client, rows);
376
+ expect(Object.keys(thin.unobserved)).toEqual(["seeded"]);
377
+ expect(Object.keys(deep.unobserved)).toEqual(["seeded"]);
378
+ expect(deep.resources.seeded).toBeUndefined();
379
+ });
380
+ });