@intentius/behold 0.13.0 → 0.14.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.
@@ -11,11 +11,21 @@ import {
11
11
  leaseText,
12
12
  operatorHeadline,
13
13
  convergeGates,
14
+ approvalLink,
15
+ timelineRows,
16
+ timelineText,
17
+ gatedText,
18
+ historyHeadline,
19
+ historyMalformedNote,
14
20
  APPROVE_SEMANTICS,
15
21
  APPROVED_SEMANTICS,
16
22
  CONVERGE_LOOP_LINE,
17
23
  ONE_TICK_CAVEAT,
24
+ APPROVE_AT,
25
+ RESOLVED_AT,
18
26
  LEASE_LABEL,
27
+ shortTickId,
28
+ TICK_ID_CHARS,
19
29
  } from "./operator.js";
20
30
 
21
31
  /** src/operator.ts's OperatorState, as the server broadcasts it. Values trace to
@@ -29,6 +39,9 @@ const stripRow = {
29
39
  lease: "held",
30
40
  leaseHolder: "op-a",
31
41
  leaseExpiresAt: "2026-01-01T00:05:00.000Z",
42
+ // chant#2027's tick id, as src/operator.ts's `operatorStrip` carries it —
43
+ // whole, for the renderer to truncate.
44
+ tickId: "9f1c7a52-3b64-4d0e-8a71-2e5c6d90b4af",
32
45
  pendingGates: 1,
33
46
  };
34
47
  const gate = {
@@ -96,6 +109,44 @@ describe("operatorRows", () => {
96
109
  const [row] = operatorRows({ ...state, strip: null, gates: [] }, NOW);
97
110
  expect(row.leaseText).toBe("not read yet");
98
111
  });
112
+
113
+ it("carries the tick id whole and truncated (chant#2027)", () => {
114
+ const [row] = operatorRows(state, NOW);
115
+ expect(row.tickId).toBe("9f1c7a52-3b64-4d0e-8a71-2e5c6d90b4af");
116
+ expect(row.tick).toBe("9f1c7a52");
117
+ });
118
+
119
+ it("says nothing about a tick id a pre-0.53.1 chant never wrote", () => {
120
+ // The row must read exactly as it did before the field existed — a
121
+ // placeholder would show an identity that does not exist.
122
+ const older = { ...stripRow, tickId: null };
123
+ const [row] = operatorRows({ ...state, strip: { rows: [older], pendingGates: 1 } }, NOW);
124
+ expect(row.tickId).toBeNull();
125
+ expect(row.tick).toBeNull();
126
+ });
127
+
128
+ it("says nothing about a tick id on a loop the last read didn't mention", () => {
129
+ const rows = operatorRows({ ...state, declared: [...declared, { name: "prod-observe", env: "prod" }] }, NOW);
130
+ expect(rows[1]).toMatchObject({ tickId: null, tick: null });
131
+ });
132
+ });
133
+
134
+ describe("shortTickId", () => {
135
+ it("keeps the first block of chant's randomUUID mint", () => {
136
+ expect(shortTickId("9f1c7a52-3b64-4d0e-8a71-2e5c6d90b4af")).toBe("9f1c7a52");
137
+ expect(TICK_ID_CHARS).toBe(8);
138
+ });
139
+
140
+ it("shows a shorter id whole rather than padding one chant didn't write", () => {
141
+ expect(shortTickId("tick-fix")).toBe("tick-fix");
142
+ expect(shortTickId("abc")).toBe("abc");
143
+ });
144
+
145
+ it("is null in, null out — an absent id is not an id to shorten", () => {
146
+ expect(shortTickId(null)).toBeNull();
147
+ expect(shortTickId(undefined)).toBeNull();
148
+ expect(shortTickId("")).toBeNull();
149
+ });
99
150
  });
100
151
 
101
152
  describe("leaseText", () => {
@@ -120,10 +171,12 @@ describe("operatorHeadline", () => {
120
171
  });
121
172
 
122
173
  describe("the one-tick caveat", () => {
123
- it("says the strip is not a timeline, and why", () => {
174
+ it("says the strip has one tick, why, and where the rest of it is", () => {
124
175
  expect(ONE_TICK_CAVEAT).toMatch(/only the last one/);
125
176
  expect(ONE_TICK_CAVEAT).toMatch(/chant#2029/);
126
- expect(ONE_TICK_CAVEAT).toMatch(/strip, not a timeline/);
177
+ // The history is reachable now, so "this is not a timeline" without saying
178
+ // where the timeline IS would be the strip keeping a secret.
179
+ expect(ONE_TICK_CAVEAT).toMatch(/open the history below/);
127
180
  });
128
181
  });
129
182
 
@@ -141,14 +194,21 @@ describe("the converge gate card's words", () => {
141
194
  expect(CONVERGE_LOOP_LINE).toMatch(/not the Temporal run gate/);
142
195
  });
143
196
 
144
- it("carries the delegated write it performs, and no URL (chant#2028)", () => {
197
+ it("carries the delegated write it performs", () => {
145
198
  const [g] = convergeGates(state);
146
199
  expect(g.approve).toEqual({
147
200
  method: "POST",
148
201
  path: "/api/operator/approve/fountain-apply/rollout-gate",
149
202
  command: "chant approve fountain-apply rollout-gate",
150
203
  });
151
- expect(JSON.stringify(g)).not.toMatch(/https?:/);
204
+ });
205
+
206
+ it("offers the gate's address under chant's own words, on either half", () => {
207
+ // `chant operator status`'s human render prints `approve at: <url>` under
208
+ // the pending gate; the resolved half is where it DID happen, so the tense
209
+ // changes and nothing else does.
210
+ expect(APPROVE_AT).toBe("approve at:");
211
+ expect(RESOLVED_AT).toBe("resolved at:");
152
212
  });
153
213
 
154
214
  it("has no cards when nothing is pending", () => {
@@ -156,3 +216,127 @@ describe("the converge gate card's words", () => {
156
216
  expect(convergeGates(null)).toEqual([]);
157
217
  });
158
218
  });
219
+
220
+ describe("approvalLink (chant#2028)", () => {
221
+ it("links an absolute http/https address", () => {
222
+ expect(approvalLink("https://github.com/INTENTIUS/chant/pull/2028")).toBe("https://github.com/INTENTIUS/chant/pull/2028");
223
+ expect(approvalLink("http://gitlab.internal/x/-/merge_requests/9")).toBe("http://gitlab.internal/x/-/merge_requests/9");
224
+ });
225
+
226
+ it("reads anything else as no address at all — never a link", () => {
227
+ // Re-checked here rather than only trusted from the server, exactly as
228
+ // APPROVE_SEMANTICS is re-stated in this module: a `javascript:` string that
229
+ // reached a ledger must not become a link because a server was older.
230
+ for (const bad of ["javascript:alert(1)", "org/repo/pull/1", "file:///etc/passwd", "", null, undefined, 12]) {
231
+ expect(approvalLink(bad)).toBeNull();
232
+ }
233
+ });
234
+
235
+ it("means the card renders exactly as it did before the field existed", () => {
236
+ // The absent case is the ordinary one: the url is optional by design, and a
237
+ // placeholder would invent a review surface chant never claimed.
238
+ const [g] = convergeGates(state);
239
+ expect(approvalLink(g.url)).toBeNull();
240
+ });
241
+ });
242
+
243
+ // ── The timeline (chant#2029) ────────────────────────────────────────────────
244
+
245
+ /** src/operator.ts's OperatorHistory, as `/api/operator/log` answers with it.
246
+ * Values trace to chant's own `operator log` test through the committed fixture
247
+ * (src/__fixtures__/operator-status/timeline.log.json). */
248
+ const history = {
249
+ open: true,
250
+ loading: false,
251
+ error: null,
252
+ data: {
253
+ entries: [
254
+ {
255
+ kind: "tick",
256
+ at: "2026-01-01T00:00:00.000Z",
257
+ op: "staging-converge",
258
+ env: "staging",
259
+ id: "t1",
260
+ log: "converge(staging): drifted=0",
261
+ gated: [{ rule: "drift-apply", op: "fountain-apply", gate: "rollout-gate", url: "https://pr.example/1" }],
262
+ },
263
+ {
264
+ kind: "gate-resolution",
265
+ at: "2026-01-02T00:00:00.000Z",
266
+ op: "fountain-apply",
267
+ gate: "rollout-gate",
268
+ resolvedBy: "alex",
269
+ note: null,
270
+ url: "https://pr.example/1",
271
+ },
272
+ ],
273
+ malformed: { converge: 0, gates: 0 },
274
+ window: { limit: 50, since: null },
275
+ more: false,
276
+ readAt: "2026-01-04T00:00:00.000Z",
277
+ },
278
+ };
279
+
280
+ describe("timelineRows", () => {
281
+ it("is empty for a history nobody has opened yet", () => {
282
+ expect(timelineRows(null)).toEqual([]);
283
+ expect(timelineRows({ open: true, loading: true, data: null })).toEqual([]);
284
+ });
285
+
286
+ it("keeps the order the server handed over — chant's own, oldest first", () => {
287
+ expect(timelineRows(history).map((r) => r.at)).toEqual([
288
+ "2026-01-01T00:00:00.000Z",
289
+ "2026-01-02T00:00:00.000Z",
290
+ ]);
291
+ });
292
+ });
293
+
294
+ describe("timelineText", () => {
295
+ it("reads a tick as its loop and its id, with chant's log line kept separate", () => {
296
+ // The log line is rendered verbatim on its own row; nothing here rewords a
297
+ // fact chant wrote.
298
+ expect(timelineText(timelineRows(history)[0])).toBe("staging-converge@staging [t1]");
299
+ });
300
+
301
+ it("reads a resolution as who resolved which gate", () => {
302
+ expect(timelineText(timelineRows(history)[1])).toBe("gate resolved · fountain-apply/rollout-gate by alex");
303
+ });
304
+
305
+ it("prints no id for a tick recorded before chant#2027", () => {
306
+ expect(timelineText({ ...timelineRows(history)[0], id: null })).toBe("staging-converge@staging");
307
+ });
308
+
309
+ it("names a gated outcome the way chant's own log render does", () => {
310
+ expect(gatedText(timelineRows(history)[0].gated[0])).toBe('gated drift-apply → fountain-apply gate "rollout-gate"');
311
+ });
312
+ });
313
+
314
+ describe("historyHeadline", () => {
315
+ it("states the window, so a full one doesn't read as the end of history", () => {
316
+ expect(historyHeadline(history)).toBe("2 entries · newest 50");
317
+ const full = { ...history, data: { ...history.data, more: true } };
318
+ expect(historyHeadline(full)).toMatch(/older ticks before these/);
319
+ });
320
+
321
+ it("names a --since when one was asked for", () => {
322
+ const since = { ...history, data: { ...history.data, window: { limit: 20, since: "2026-01-01T00:00:00.000Z" } } };
323
+ expect(historyHeadline(since)).toBe("2 entries · newest 20 · since 2026-01-01T00:00:00.000Z");
324
+ });
325
+
326
+ it("says nothing about a history that hasn't been read", () => {
327
+ expect(historyHeadline({ open: true, loading: true, data: null })).toBeNull();
328
+ });
329
+ });
330
+
331
+ describe("historyMalformedNote", () => {
332
+ it("surfaces the unreadable lines rather than letting a gap read as a quiet loop", () => {
333
+ const bad = { ...history, data: { ...history.data, malformed: { converge: 3, gates: 1 } } };
334
+ expect(historyMalformedNote(bad)).toMatch(/4 ledger lines were unreadable/);
335
+ expect(historyMalformedNote(bad)).toMatch(/converge: 3, gates: 1/);
336
+ });
337
+
338
+ it("stays silent when every line read", () => {
339
+ expect(historyMalformedNote(history)).toBeNull();
340
+ expect(historyMalformedNote(null)).toBeNull();
341
+ });
342
+ });