@intentius/behold 0.29.3 → 0.29.4
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/dist/cli.js +840 -292
- package/package.json +1 -1
- package/web/app.js +306 -21
- package/web/kiosk-runs.js +61 -6
- package/web/kiosk-runs.test.js +22 -1
- package/web/kiosk.html +21 -3
- package/web/kiosk.js +168 -19
- package/web/kiosk.test.js +160 -3
- package/web/terragucci-change.js +49 -0
- package/web/terragucci-change.test.js +36 -1
- package/web/terragucci-components-live.js +166 -0
- package/web/terragucci-components-live.test.js +145 -0
- package/web/view-url.js +5 -4
- package/web/view-url.test.js +4 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// #571: the live marks on a terragucci repo's components zoom, the roots drawn
|
|
2
|
+
// one card per root in wave boxes titled with the job that applies the wave
|
|
3
|
+
// (behold#597). The same marks the resources zoom carries (#568, #569), for
|
|
4
|
+
// the run the view is pinned to: each card in its wave's state, the waiting
|
|
5
|
+
// wave's plan digest on its box, the box's title with its job's status from
|
|
6
|
+
// the pipeline's runs (`/api/terragucci/runs`, terragucci#1038), and the reads
|
|
7
|
+
// between the roots. app.js draws what these return; nothing here touches
|
|
8
|
+
// the DOM.
|
|
9
|
+
//
|
|
10
|
+
// The marks are the reports' and the runs', read when they were written: a
|
|
11
|
+
// wave that failed says the waves before it stay applied, and nothing here is
|
|
12
|
+
// all-or-nothing.
|
|
13
|
+
|
|
14
|
+
import { pinsCommit, rootStates } from "./terragucci-change.js";
|
|
15
|
+
|
|
16
|
+
/** A job's status as its wave box says it. A job held at a terragucci gate is never a failure. */
|
|
17
|
+
export const JOB_WORDS = {
|
|
18
|
+
queued: "queued",
|
|
19
|
+
running: "running",
|
|
20
|
+
waiting: "held at the gate",
|
|
21
|
+
success: "succeeded",
|
|
22
|
+
failure: "failed",
|
|
23
|
+
cancelled: "cancelled",
|
|
24
|
+
skipped: "skipped",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The tone a job's status is drawn in, on the same channels as a root's state. */
|
|
28
|
+
export const JOB_TONE = { queued: "idle", running: "busy", waiting: "wait", success: "ok", failure: "bad", cancelled: "idle", skipped: "idle" };
|
|
29
|
+
|
|
30
|
+
/** A card's state in one or two words, when the full words don't fit the card. */
|
|
31
|
+
export const SHORT_WORDS = {
|
|
32
|
+
planned: "planned",
|
|
33
|
+
"not-started": "not started",
|
|
34
|
+
waiting: "waiting",
|
|
35
|
+
applying: "applying",
|
|
36
|
+
applied: "applied",
|
|
37
|
+
refused: "refused",
|
|
38
|
+
failed: "failed",
|
|
39
|
+
unchanged: "no changes",
|
|
40
|
+
outside: "not in this run",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const sameSha = (a, b) => !!a && !!b && (a.startsWith(b) || b.startsWith(a));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The commit whose pipeline runs the wave boxes report: the pinned one, or
|
|
47
|
+
* with no pin, the run view's. None when the pin holds the view off every run
|
|
48
|
+
* (`base`, `none`, a pull request's plan): the boxes then carry no job status.
|
|
49
|
+
*/
|
|
50
|
+
export function runsCommit(pin, progress) {
|
|
51
|
+
if (pin) return pinsCommit(pin) ? pin : null;
|
|
52
|
+
const run = progress && !progress.error && progress.run && progress.run.present ? progress.run : null;
|
|
53
|
+
return run && run.commit ? run.commit : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A wave job's status on a commit: of the runs on that commit (the index is
|
|
58
|
+
* newest first), the newest in which the job ran or waits, so a resume's run
|
|
59
|
+
* of `apply-wave-2` follows the merge's run that held it. A job every such
|
|
60
|
+
* run skipped is `skipped`. Null when no run on the commit has the job.
|
|
61
|
+
*/
|
|
62
|
+
export function jobStatus(index, commit, jobName) {
|
|
63
|
+
const runs = index && index.doc && Array.isArray(index.doc.runs) ? index.doc.runs : [];
|
|
64
|
+
let skipped = null;
|
|
65
|
+
for (const run of runs) {
|
|
66
|
+
if (!sameSha(run.commit, commit)) continue;
|
|
67
|
+
const job = (run.jobs || []).find((j) => j.name === jobName);
|
|
68
|
+
if (!job) continue;
|
|
69
|
+
if (job.status !== "skipped") return { status: job.status, words: JOB_WORDS[job.status] || job.status, tone: JOB_TONE[job.status] || "idle", run: run.id };
|
|
70
|
+
skipped = skipped || { status: "skipped", words: JOB_WORDS.skipped, tone: "idle", run: run.id };
|
|
71
|
+
}
|
|
72
|
+
return skipped;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** "apply-wave-2 · held at the gate", or the job alone without a status. */
|
|
76
|
+
export function waveTitle(jobName, status) {
|
|
77
|
+
return status ? `${jobName} · ${status.words}` : jobName;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The wave a `groups.byWave` key names: `wave-2` is 2. Null for any other key. */
|
|
81
|
+
export function waveOfKey(key) {
|
|
82
|
+
const m = /^wave-(\d+)$/.exec(String(key || ""));
|
|
83
|
+
return m ? Number(m[1]) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Each component's marks and each wave box's, for the run the view draws:
|
|
88
|
+
*
|
|
89
|
+
* - `cards[component]`: its root, wave and state (`rootStates`' words and
|
|
90
|
+
* tone; `words` without the "wave N ·" its box already says, `short` for a
|
|
91
|
+
* card the words don't fit), and the facts for its tooltip;
|
|
92
|
+
* - `waves[n]`: the wave's state, its job and the job's status on the run's
|
|
93
|
+
* commit, the box's title, and the facts drawn on the box (a waiting
|
|
94
|
+
* wave's plan digest first).
|
|
95
|
+
*
|
|
96
|
+
* `facets` is the components graph's `meta.terragucciComponents`: its jobs
|
|
97
|
+
* (component -> job and wave) and `roots` (component -> the root it applies;
|
|
98
|
+
* a component without one is taken to be its root's name).
|
|
99
|
+
*/
|
|
100
|
+
export function componentMarks(answer, progress, facets, { runs = null, pin = null } = {}) {
|
|
101
|
+
const jobs = facets && Array.isArray(facets.jobs) ? facets.jobs : [];
|
|
102
|
+
const rootOf = (c) => (facets && facets.roots && facets.roots[c]) || c;
|
|
103
|
+
const states = rootStates(answer, progress);
|
|
104
|
+
const stage = states.stage;
|
|
105
|
+
const commit = runsCommit(pin, progress);
|
|
106
|
+
const cards = {};
|
|
107
|
+
for (const j of jobs) {
|
|
108
|
+
const root = rootOf(j.component);
|
|
109
|
+
const st = states.roots[root];
|
|
110
|
+
if (!st) continue;
|
|
111
|
+
cards[j.component] = {
|
|
112
|
+
root,
|
|
113
|
+
wave: j.wave ?? st.wave,
|
|
114
|
+
state: st.state,
|
|
115
|
+
tone: st.tone,
|
|
116
|
+
words: st.words.replace(/^wave \d+ · /, ""),
|
|
117
|
+
short: SHORT_WORDS[st.state] || st.state,
|
|
118
|
+
facts: st.facts,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const waves = {};
|
|
122
|
+
const waveNumbers = [...new Set(jobs.map((j) => j.wave).filter((n) => typeof n === "number"))].sort((a, b) => a - b);
|
|
123
|
+
for (const n of waveNumbers) {
|
|
124
|
+
const jobName = jobs.find((j) => j.wave === n).jobName;
|
|
125
|
+
const status = commit ? jobStatus(runs, commit, jobName) : null;
|
|
126
|
+
const members = jobs.filter((j) => j.wave === n).map((j) => cards[j.component]).filter(Boolean);
|
|
127
|
+
const w = waveState(answer, progress, stage, n);
|
|
128
|
+
const facts = [];
|
|
129
|
+
for (const m of members) for (const f of m.facts) if (!facts.includes(f)) facts.push(f);
|
|
130
|
+
// The plan a waiting wave is approved for first, as it's what the approval
|
|
131
|
+
// names; then, for a wave that failed, the waves before it that stay applied.
|
|
132
|
+
const rank = (f) => (/^approve plan /.test(f) ? 0 : / stays applied$/.test(f) ? 1 : 2);
|
|
133
|
+
facts.sort((a, b) => rank(a) - rank(b));
|
|
134
|
+
waves[n] = {
|
|
135
|
+
number: n,
|
|
136
|
+
job: jobName,
|
|
137
|
+
state: w,
|
|
138
|
+
tone: w ? toneOf(w) : status ? status.tone : "idle",
|
|
139
|
+
status,
|
|
140
|
+
title: waveTitle(jobName, status),
|
|
141
|
+
facts,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { stage, cards, waves, commit };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const toneOf = (state) => ({ planned: "plan", waiting: "wait", applying: "busy", applied: "ok", failed: "bad", refused: "bad" })[state] || "idle";
|
|
148
|
+
|
|
149
|
+
/** A wave's own state: the run view's once merged, `planned` while a pull request plans it. */
|
|
150
|
+
function waveState(answer, progress, stage, n) {
|
|
151
|
+
if (stage === "apply") {
|
|
152
|
+
const w = progress.states.find((x) => x.number === n);
|
|
153
|
+
return w ? w.state : null;
|
|
154
|
+
}
|
|
155
|
+
if (stage === "pull-request") {
|
|
156
|
+
const w = answer.pull_request.waves.find((x) => x.number === n);
|
|
157
|
+
return w ? w.state || "planned" : null;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The words for a card: the full state when it fits in `room` (a width), else the short form. */
|
|
163
|
+
export function cardWords(mark, fits) {
|
|
164
|
+
return fits(mark.words) ? mark.words : mark.short;
|
|
165
|
+
}
|
|
166
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { JOB_WORDS, componentMarks, jobStatus, runsCommit, waveOfKey, waveTitle } from "./terragucci-components-live.js";
|
|
3
|
+
|
|
4
|
+
// #571: the live marks on a terragucci repo's components zoom. The facets are
|
|
5
|
+
// the components graph's (src/__fixtures__/terragucci-components: network and
|
|
6
|
+
// other in wave 1, app in 2, edge in 3), the run view and the runs shaped as
|
|
7
|
+
// the kiosk's demo fixtures have them.
|
|
8
|
+
const MERGE = "f0e1d2c3b4a5968778695a4b3c2d1e0f12345678";
|
|
9
|
+
const facets = {
|
|
10
|
+
jobs: [
|
|
11
|
+
{ component: "network", jobName: "apply-wave-1", needs: ["check"], wave: 1 },
|
|
12
|
+
{ component: "other", jobName: "apply-wave-1", needs: ["check"], wave: 1 },
|
|
13
|
+
{ component: "app", jobName: "apply-wave-2", needs: ["apply-wave-1"], wave: 2 },
|
|
14
|
+
{ component: "edge", jobName: "apply-wave-3", needs: ["apply-wave-2"], wave: 3 },
|
|
15
|
+
],
|
|
16
|
+
roots: { network: "network", other: "other", app: "app", edge: "edge" },
|
|
17
|
+
};
|
|
18
|
+
const answer = {
|
|
19
|
+
roots: ["app", "edge", "network", "other"].map((name) => ({ member: "demo", name, path: name })),
|
|
20
|
+
pull_request: {
|
|
21
|
+
run: { pull_request: "41", commit: "a1b2c3d4", finished: "2026-10-20T10:01:40.000Z" },
|
|
22
|
+
roots: ["network", "app", "edge"],
|
|
23
|
+
reads: [{ root: "app", upstream: "network", data: "network", outputs: "planned", unknown: ["subnet_id"] }],
|
|
24
|
+
waves: [
|
|
25
|
+
{ number: 1, roots: ["network"], set_digest: "jcs1-sha256:aaaaaaaaaaaaaaaa" },
|
|
26
|
+
{ number: 2, roots: ["app"], replans_after: [1], waits: true },
|
|
27
|
+
{ number: 3, roots: ["edge"], set_digest: "jcs1-sha256:bbbbbbbbbbbbbbbb" },
|
|
28
|
+
],
|
|
29
|
+
blast: { roots: ["network", "app", "edge"], downstream: [] },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const progress = (wave2) => ({
|
|
33
|
+
run: { present: true, commit: MERGE, updated: "2026-10-20T10:05:00.000Z" },
|
|
34
|
+
states: [
|
|
35
|
+
{ number: 1, state: "applied", roots: ["network"], reads: [], digest: "jcs1-sha256:4444", totals: { create: 1 }, changed: ["network"] },
|
|
36
|
+
{ number: 2, state: wave2, roots: ["app"], reads: [1], digest: "jcs1-sha256:555555555555555555", totals: { create: 4 }, changed: ["app"] },
|
|
37
|
+
{ number: 3, state: "not-started", roots: ["edge"], reads: [2] },
|
|
38
|
+
],
|
|
39
|
+
reads: [
|
|
40
|
+
{ root: "app", wave: 2, reads: ["network"] },
|
|
41
|
+
{ root: "edge", wave: 3, reads: ["app"] },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
const job = (name, status) => ({ name, status });
|
|
45
|
+
const runs = (...list) => ({ doc: { runs: list } });
|
|
46
|
+
const run = (id, commit, jobs) => ({ id, commit, jobs });
|
|
47
|
+
|
|
48
|
+
describe("a wave job's status from the pipeline's runs (#571)", () => {
|
|
49
|
+
it("takes the newest run on the commit in which the job ran or waits", () => {
|
|
50
|
+
const index = runs(
|
|
51
|
+
run(12, MERGE, [job("apply-wave-1", "skipped"), job("apply-wave-2", "running")]), // the resume
|
|
52
|
+
run(11, MERGE, [job("apply-wave-1", "success"), job("apply-wave-2", "waiting")]), // the merge, held at the gate
|
|
53
|
+
run(10, "0000000", [job("apply-wave-2", "failure")]),
|
|
54
|
+
);
|
|
55
|
+
expect(jobStatus(index, MERGE, "apply-wave-2")).toEqual({ status: "running", words: "running", tone: "busy", run: 12 });
|
|
56
|
+
// The resume skipped wave 1: the merge's run says how it went.
|
|
57
|
+
expect(jobStatus(index, MERGE, "apply-wave-1")).toEqual({ status: "success", words: "succeeded", tone: "ok", run: 11 });
|
|
58
|
+
// A short sha names the same commit.
|
|
59
|
+
expect(jobStatus(index, MERGE.slice(0, 8), "apply-wave-2").run).toBe(12);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("says skipped when every run on the commit skipped the job, and nothing without one", () => {
|
|
63
|
+
const index = runs(run(12, MERGE, [job("apply-wave-3", "skipped")]));
|
|
64
|
+
expect(jobStatus(index, MERGE, "apply-wave-3")).toMatchObject({ status: "skipped", words: "skipped" });
|
|
65
|
+
expect(jobStatus(index, MERGE, "apply-wave-9")).toBe(null);
|
|
66
|
+
expect(jobStatus(index, "1234567", "apply-wave-3")).toBe(null);
|
|
67
|
+
expect(jobStatus(null, MERGE, "apply-wave-3")).toBe(null);
|
|
68
|
+
expect(jobStatus({ error: "no runs" }, MERGE, "apply-wave-3")).toBe(null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("calls a job held at the gate that, never a failure", () => {
|
|
72
|
+
expect(JOB_WORDS.waiting).toBe("held at the gate");
|
|
73
|
+
expect(waveTitle("apply-wave-2", { words: JOB_WORDS.waiting })).toBe("apply-wave-2 · held at the gate");
|
|
74
|
+
expect(waveTitle("apply-wave-2", null)).toBe("apply-wave-2");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reads the runs of the pinned commit, of the run view's without a pin, and of none under a pin off every run", () => {
|
|
78
|
+
expect(runsCommit(MERGE, null)).toBe(MERGE);
|
|
79
|
+
expect(runsCommit(null, progress("waiting"))).toBe(MERGE);
|
|
80
|
+
expect(runsCommit(null, null)).toBe(null);
|
|
81
|
+
for (const pin of ["base", "none", "pull", "pull-41", `pull-41-${MERGE}`]) expect(runsCommit(pin, progress("waiting"))).toBe(null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("reads a wave from chant's box key", () => {
|
|
85
|
+
expect(waveOfKey("wave-2")).toBe(2);
|
|
86
|
+
expect(waveOfKey("wave-12")).toBe(12);
|
|
87
|
+
expect(waveOfKey("network")).toBe(null);
|
|
88
|
+
expect(waveOfKey(undefined)).toBe(null);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("the component marks (#571)", () => {
|
|
93
|
+
it("colours each card by its wave's state, and puts the waiting wave's digest and its job's status on its box", () => {
|
|
94
|
+
const index = runs(run(11, MERGE, [job("apply-wave-1", "success"), job("apply-wave-2", "waiting"), job("apply-wave-3", "queued")]));
|
|
95
|
+
const m = componentMarks(answer, progress("waiting"), facets, { runs: index, pin: MERGE });
|
|
96
|
+
expect(m.stage).toBe("apply");
|
|
97
|
+
expect(Object.fromEntries(Object.entries(m.cards).map(([c, x]) => [c, [x.state, x.tone, x.words]]))).toEqual({
|
|
98
|
+
network: ["applied", "ok", "applied"],
|
|
99
|
+
other: ["outside", "idle", "not in this run"],
|
|
100
|
+
app: ["waiting", "wait", "waiting for an approval"],
|
|
101
|
+
edge: ["not-started", "idle", "not started"],
|
|
102
|
+
});
|
|
103
|
+
expect(m.cards.app.short).toBe("waiting");
|
|
104
|
+
expect(m.waves[2]).toMatchObject({ job: "apply-wave-2", state: "waiting", tone: "wait", title: "apply-wave-2 · held at the gate" });
|
|
105
|
+
expect(m.waves[2].facts[0]).toBe("approve plan 555555555555");
|
|
106
|
+
expect(m.waves[1].title).toBe("apply-wave-1 · succeeded");
|
|
107
|
+
expect(m.waves[3]).toMatchObject({ state: "not-started", title: "apply-wave-3 · queued", facts: [] });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("says first on a failed wave's box that the waves before it stay applied", () => {
|
|
111
|
+
const index = runs(run(11, MERGE, [job("apply-wave-1", "success"), job("apply-wave-2", "failure"), job("apply-wave-3", "skipped")]));
|
|
112
|
+
const m = componentMarks(answer, progress("failed"), facets, { runs: index, pin: MERGE });
|
|
113
|
+
expect(m.cards.app).toMatchObject({ state: "failed", tone: "bad" });
|
|
114
|
+
expect(m.waves[2].facts[0]).toBe("wave 1 stays applied");
|
|
115
|
+
expect(m.waves[2].title).toBe("apply-wave-2 · failed");
|
|
116
|
+
expect(m.waves[3].title).toBe("apply-wave-3 · skipped");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("draws the pull request's plan before the merge, with no job status", () => {
|
|
120
|
+
const m = componentMarks(answer, null, facets, { runs: runs(run(11, MERGE, [job("apply-wave-2", "waiting")])), pin: `pull-41-a1b2c3d4` });
|
|
121
|
+
expect(m.stage).toBe("pull-request");
|
|
122
|
+
expect(m.cards.network).toMatchObject({ state: "planned", words: "planned" });
|
|
123
|
+
expect(m.cards.other).toMatchObject({ state: "outside", words: "not in this change" });
|
|
124
|
+
expect(m.waves[2]).toMatchObject({ state: "planned", status: null, title: "apply-wave-2" });
|
|
125
|
+
expect(m.waves[1].facts).toEqual(["plan aaaaaaaaaaaa"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("marks nothing at rest", () => {
|
|
129
|
+
const m = componentMarks({ ...answer, pull_request: null }, null, facets, { runs: null, pin: "base" });
|
|
130
|
+
expect(m.stage).toBe("none");
|
|
131
|
+
expect(m.cards).toEqual({});
|
|
132
|
+
expect(Object.values(m.waves).map((w) => [w.title, w.state])).toEqual([
|
|
133
|
+
["apply-wave-1", null],
|
|
134
|
+
["apply-wave-2", null],
|
|
135
|
+
["apply-wave-3", null],
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("joins a component to the root it applies, and to its own name without one", () => {
|
|
140
|
+
const renamed = { jobs: [{ component: "net", jobName: "apply-wave-1", wave: 1 }, { component: "app", jobName: "apply-wave-2", wave: 2 }], roots: { net: "network" } };
|
|
141
|
+
const m = componentMarks(answer, progress("applying"), renamed, { runs: null, pin: MERGE });
|
|
142
|
+
expect(m.cards.net).toMatchObject({ root: "network", state: "applied" });
|
|
143
|
+
expect(m.cards.app).toMatchObject({ root: "app", state: "applying", tone: "busy" });
|
|
144
|
+
});
|
|
145
|
+
});
|
package/web/view-url.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* `focus` (#569) names the boxes the view brings forward, comma-separated
|
|
15
15
|
* (a terragucci repo's roots): the rest are drawn dimmed, nothing is hidden.
|
|
16
16
|
* `run` (#569) pins which terragucci run the view draws: a commit's run view,
|
|
17
|
-
* `none` (no run view: the pull request's plan)
|
|
18
|
-
*
|
|
17
|
+
* `none` (no run view: the newest pull request's plan), `pull-<n>[-<sha>]`
|
|
18
|
+
* (no run view, and only that pull request's plan, of that commit) or `base`
|
|
19
|
+
* (nothing in flight); absent, the newest. */
|
|
19
20
|
export const VIEW_KEYS = ["member", "zoom", "env", "tier", "lens", "node", "radial", "gates", "focus", "run"];
|
|
20
21
|
|
|
21
22
|
/** An env name as the server takes one (src/workspace-gates.ts isEnvName). */
|
|
@@ -131,8 +132,8 @@ export function settleView(want, offer) {
|
|
|
131
132
|
}
|
|
132
133
|
if ("run" in want) {
|
|
133
134
|
// null: the newest run, as without the key.
|
|
134
|
-
if (want.run === null || /^([0-9a-f]{7,40}|none|base)$/.test(String(want.run))) apply.run = want.run;
|
|
135
|
-
else refused.push({ key: "run", value: want.run, reason: "run is a commit sha, none or
|
|
135
|
+
if (want.run === null || /^([0-9a-f]{7,40}|none|base|pull(-\d+(-[0-9a-f]{7,40})?)?)$/.test(String(want.run))) apply.run = want.run;
|
|
136
|
+
else refused.push({ key: "run", value: want.run, reason: "run is a commit sha, none, base or pull-<number>[-<sha>]" });
|
|
136
137
|
}
|
|
137
138
|
if (want.member) apply.member = want.member;
|
|
138
139
|
if (want.node) apply.node = want.node;
|
package/web/view-url.test.js
CHANGED
|
@@ -127,6 +127,10 @@ describe("run (#569)", () => {
|
|
|
127
127
|
expect(settleView({ run: "base" }, {}).apply).toEqual({ run: "base" });
|
|
128
128
|
expect(settleView({ run: null }, {}).apply).toEqual({ run: null });
|
|
129
129
|
expect(settleView({ run: "main" }, {}).refused[0].key).toBe("run");
|
|
130
|
+
expect(settleView({ run: "pull-989-659dee43" }, {}).apply).toEqual({ run: "pull-989-659dee43" });
|
|
131
|
+
expect(settleView({ run: "pull-989" }, {}).apply).toEqual({ run: "pull-989" });
|
|
132
|
+
expect(settleView({ run: "pull" }, {}).apply).toEqual({ run: "pull" });
|
|
133
|
+
expect(settleView({ run: "pull-x" }, {}).refused[0].key).toBe("run");
|
|
130
134
|
expect(writeViewQuery("", { run: "none" })).toBe("?run=none");
|
|
131
135
|
});
|
|
132
136
|
});
|