Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_release.mjs

11.8 KiB, 1 run

created by r2519314175:641, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_release.mjs — which Daimond you are running, and what came before it.
2//
3// The guarantees worth holding, rather than the pixels:
4//
5// * a note can be added to a sealed entry WITHOUT moving its hash, which is
6// the whole reason the changelog can live in the transparency log at all,
7// * the status row names the release, not a hex build id,
8// * every sealed build appears, newest first, exactly one marked "you are here",
9// * the planned entry is never mistakable for history: no build, no seal
10// number, and said in words,
11// * there is no way back to an old version, deliberately.
12//
13// Run with dev/serve.mjs up. No gateway needed. The log is fed in over a data:
14// URL so this does not depend on the network or on what is published today.
15import { open, signInAs, errors, APP } from './harness.mjs';
16import { readFile } from 'node:fs/promises';
17import { parseLog, verifyChain, entryHash, nextEntry } from '../verify/lib.mjs';
18
19const ok = [], bad = [];
20const check = (name, pass, detail) => {
21 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
22 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
23};
24
25// ── The chain tolerates notes, which is what makes this possible ────────
26{
27 const entries = parseLog(await readFile('verify/transparency.jsonl', 'utf8'));
28 const chain = verifyChain(entries);
29 check('the real log still verifies with notes on every entry', chain.ok,
30 chain.error || `${entries.length} entries`);
31 check('every entry carries a note', entries.every(e => e.note && e.note.length),
32 `${entries.filter(e => e.note).length}/${entries.length}`);
33
34 // The load-bearing property: the note is outside the hashed preimage.
35 const e = entries[entries.length - 1];
36 const recomputed = entryHash({ seq: e.seq, ts: e.ts, build: e.build, bundle: e.bundle, prev: e.prev });
37 check('the hash is computed without the note', recomputed === e.entry, e.entry.slice(0, 16) + '…');
38
39 const withNote = nextEntry([], { ts: 't', build: 'b', bundle: 'x', note: 'hello' });
40 const without = nextEntry([], { ts: 't', build: 'b', bundle: 'x' });
41 check('so adding one to a sealed entry cannot invalidate it',
42 withNote.entry === without.entry, withNote.entry.slice(0, 16) + '…');
43 check('and an absent note leaves no empty field behind', !('note' in without));
44}
45
46// ── A log of our own, so this test does not depend on the network ───────
47// The build this tab is running has to appear in the log, because the surface
48// reports what you are RUNNING rather than what has most recently been
49// published -- reporting the latter told a stale tab it was current.
50const running = await (async () => {
51 const r = await fetch(`${APP}/build.json`).catch(() => null);
52 const j = r && r.ok ? await r.json() : null;
53 return (j && j.build) || 'cccccccccccc';
54})();
55
56const mk = (seq, ts, build, note) => ({
57 seq, ts, build, note, bundle: 'x'.repeat(64), prev: '0'.repeat(64), entry: 'e'.repeat(64),
58});
59const LOG = [
60 mk(0, '2026-01-02T00:00:00.000Z', 'aaaaaaaaaaaa', 'The first one'),
61 mk(1, '2026-02-03T00:00:00.000Z', 'bbbbbbbbbbbb', 'The second one'),
62 mk(2, '2026-03-04T00:00:00.000Z', running, 'The newest one'),
63];
64const asUrl = (rows) => 'data:text/plain,' + encodeURIComponent(rows.map(e => JSON.stringify(e)).join('\n'));
65const LOG_URL = asUrl(LOG);
66
67const s = await open({ name: 'release', connect: false });
68const p = s.page;
69await p.waitForTimeout(2500);
70
71await p.evaluate((url) => {
72 const m = document.createElement('meta');
73 m.name = 'daimond-log';
74 m.content = url;
75 document.head.appendChild(m);
76 window.DaimondRelease.reset();
77}, LOG_URL);
78
79// ── Before a release is declared, nothing is announced ──────────────────
80// A deployment is not a release. Daimond seals several builds a day, and none
81// of them is an announcement; until one is declared the row has to say so
82// rather than dress a build up as a version.
83{
84 await p.evaluate((url) => {
85 const m = document.createElement('meta');
86 m.name = 'daimond-releases';
87 m.content = url;
88 document.head.appendChild(m);
89 window.DaimondRelease.reset();
90 }, 'data:application/json,' + encodeURIComponent(JSON.stringify(
91 { milestones: [], planned: { name: 'Albany', blurb: 'The first release.' } })));
92 await p.evaluate(() => window.DaimondRelease.paintRow());
93 await p.waitForTimeout(500);
94 const txt = await p.$eval('#astat-release', e => e.textContent);
95 check('with no release declared, the row says pre-release', /Pre-release/i.test(txt), txt.trim());
96 check('and names the build rather than inventing a version name',
97 txt.includes(running), txt.trim());
98 check('it does not present the planned release as the current one',
99 !/Albany/.test(txt), txt.trim());
100}
101
102// ── Once one IS declared, the row names it ──────────────────────────────
103{
104 await p.evaluate((url) => {
105 document.querySelector('meta[name="daimond-releases"]').content = url;
106 window.DaimondRelease.reset();
107 }, 'data:application/json,' + encodeURIComponent(JSON.stringify({
108 milestones: [{ name: 'Albany', from: 0, blurb: 'The first release.' }],
109 planned: { name: 'Broome', blurb: 'What comes next.' },
110 })));
111 await p.evaluate(() => window.DaimondRelease.paintRow());
112 await p.waitForTimeout(500);
113 const txt = await p.$eval('#astat-release', e => e.textContent);
114 check('the row names the release, not a hex build id', /Albany/.test(txt), txt.trim());
115 check('and says how long you have been on it',
116 /(today|yesterday|days ago|months ago|years ago)/.test(txt), txt.trim());
117 const title = await p.$eval('#astat-release', e => e.title);
118 check('the build id is still available, in the tooltip', title.includes(running), title);
119}
120
121// ── The history ─────────────────────────────────────────────────────────
122{
123 await p.click('#astat-release');
124 await p.waitForTimeout(700);
125 const rows = await p.$$eval('#rel-list .rel-row', els => els.map(e => ({
126 planned: e.classList.contains('rel-planned'),
127 current: e.classList.contains('rel-current'),
128 text: e.textContent,
129 })));
130 // The list is RELEASES, not deployments: one declared release plus what is
131 // planned, however many builds have been sealed underneath.
132 check('the list shows releases rather than every build', rows.length === 2,
133 `${rows.length} rows for ${LOG.length} builds`);
134 check('the planned one is first', rows[0] && rows[0].planned, rows[0] && rows[0].text.slice(0, 30));
135 check('exactly one row says you are here',
136 rows.filter(r => /you are here/i.test(r.text)).length === 1);
137 check('and it is the declared release', rows[1] && rows[1].current && /Albany/.test(rows[1].text));
138
139 // The builds stay reachable, because they are the verifiable part.
140 const builds = await p.$$eval('#rel-list .rel-builds .rel-build', els => els.map(e => e.textContent));
141 check('every sealed build is still there, behind a disclosure',
142 builds.length === LOG.length, `${builds.length} builds`);
143 check('each build carries its own note',
144 builds.some(t => /The second one/.test(t)), builds[1] && builds[1].slice(0, 44));
145 const summary = await p.$eval('#rel-list .rel-builds summary', e => e.textContent);
146 check('the disclosure says how many there are', /3 sealed builds/.test(summary), summary);
147}
148
149// ── A tab running an OLDER build is told so, not flattered ─────────────
150// This is the defect the surface was built with: it reported the newest
151// PUBLISHED release as "you are here", so a tab open since before a deploy was
152// told it was current on the same screen where the update chip said otherwise.
153{
154 const ahead = LOG.concat([mk(3, '2026-04-05T00:00:00.000Z', 'ffffffffffff', 'Published after this tab loaded')]);
155 await p.evaluate((url) => {
156 document.querySelector('meta[name="daimond-log"]').content = url;
157 window.DaimondRelease.reset();
158 }, asUrl(ahead));
159 await p.evaluate(() => window.DaimondRelease.paintRow());
160 await p.waitForTimeout(400);
161 const txt = await p.$eval('#astat-release', e => e.textContent);
162 const tip = await p.$eval('#astat-release', e => e.title);
163 check('a tab behind the newest release says so', /update ready/i.test(txt), txt.trim());
164 check('and the tooltip says a newer build exists', /newer build/i.test(tip), tip);
165
166 await p.evaluate(() => window.DaimondRelease.render(document.getElementById('rel-list')));
167 await p.waitForTimeout(400);
168 const marked = await p.$$eval('#rel-list .rel-row', els =>
169 els.filter(e => /you are here/i.test(e.textContent)).map(e => e.textContent.slice(0, 60)));
170 check('"you are here" marks where the running build sits, not the newest published',
171 marked.length >= 1 && !marked.some(t => t.includes('ffffffffffff')), marked.join(' | '));
172
173 // Put the original log back for the checks below.
174 await p.evaluate((url) => {
175 document.querySelector('meta[name="daimond-log"]').content = url;
176 window.DaimondRelease.reset();
177 }, LOG_URL);
178 await p.evaluate(() => window.DaimondRelease.render(document.getElementById('rel-list')));
179 await p.waitForTimeout(400);
180}
181
182// ── A promise must not read as a record ─────────────────────────────────
183// This is the one honesty requirement of the whole surface: everything below
184// the first row is sealed and checkable, and the first row is neither.
185{
186 const planned = await p.$eval('#rel-list .rel-planned', e => ({
187 text: e.textContent,
188 dashed: getComputedStyle(e).borderStyle,
189 hasCode: !!e.querySelector('code'),
190 }));
191 check('the planned entry names no build id', !planned.hasCode);
192 check('it carries no seal number', !/sealed #/.test(planned.text));
193 check('it is drawn differently from a sealed one', planned.dashed === 'dashed', planned.dashed);
194 check('and it says so in words, not only in styling',
195 /not released yet/i.test(planned.text), planned.text.slice(-46).trim());
196 check('it is labelled planned', /planned/i.test(planned.text));
197}
198
199// ── No way back, on purpose ─────────────────────────────────────────────
200// Reverting fights the gateway's own "too old" refusal, lets a user pin a build
201// with a since-fixed security fault, and risks an old build meeting newer local
202// data. If a control for it ever appears, that was a decision, not a drive-by.
203{
204 const controls = await p.$$eval('#rel-list button, #rel-list a', els =>
205 els.map(e => (e.textContent || '').trim()).filter(Boolean));
206 check('the history offers no way to go back to an old build',
207 !controls.some(t => /revert|roll ?back|downgrade|switch to|install/i.test(t)),
208 controls.length ? controls.join(', ') : 'no controls at all');
209}
210
211// ── It survives a log it cannot read ────────────────────────────────────
212// Not knowing the version is a smaller problem than a blank panel.
213{
214 await p.evaluate(() => {
215 document.querySelector('meta[name="daimond-log"]').content = 'data:text/plain,not%20json%20at%20all';
216 window.DaimondRelease.reset();
217 });
218 await p.evaluate(() => window.DaimondRelease.render(document.getElementById('rel-list')));
219 await p.waitForTimeout(500);
220 const txt = await p.$eval('#rel-list', e => e.textContent);
221 check('an unreadable log says so rather than showing nothing',
222 /no published history/i.test(txt), txt.trim().slice(0, 60));
223 await p.evaluate(() => window.DaimondRelease.paintRow());
224 await p.waitForTimeout(300);
225 check('and the status strip still stands', await p.isVisible('#astat-release'));
226}
227
228{
229 const errs = errors(s).filter(e => !/502|Bad Gateway|Failed to load resource/.test(e));
230 check('no unexpected console errors', errs.length === 0, errs.join(' | ') || 'clean');
231}
232
233await s.close();
234console.log(`\n${ok.length} passed, ${bad.length} failed`);
235if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }