@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.
- package/AGENTS.md +35 -7
- package/dist/cli.js +4615 -4225
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +144 -52
- package/example-carve/app/package-lock.json +9 -9
- package/example-carve/app/package.json +2 -2
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +144 -52
- package/example-k8s/package-lock.json +18 -18
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +4 -4
- package/web/app.js +92 -4
- package/web/index.html +24 -2
- package/web/operator.js +247 -14
- package/web/operator.test.js +188 -4
package/web/operator.test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|