@wenathlan/forge 2.0.94 → 2.0.117
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/README.md +0 -0
- package/docs/.gitkeep +0 -0
- package/package.json +32 -5
- package/tests/.gitkeep +0 -0
- package/tests/design.test.ts +182 -0
- package/tests/family.test.ts +105 -0
- package/tests/fleet.test.ts +209 -0
- package/tests/queue.test.ts +174 -0
- package/tests/runstream.test.ts +126 -0
package/README.md
CHANGED
|
File without changes
|
package/docs/.gitkeep
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/forge",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.117",
|
|
4
4
|
"description": "forge is the sandbox runner surface of the wenathlan family: it runs the sandbox runners on the saddle engine boundary and ships the documentation set and the tests directory as one package",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/wenathlan/devthink"
|
|
9
|
+
},
|
|
7
10
|
"license": "GPL-3.0-only",
|
|
8
11
|
"author": "Wenathlan",
|
|
9
12
|
"sideEffects": false,
|
|
@@ -12,7 +15,12 @@
|
|
|
12
15
|
"build": "vite build",
|
|
13
16
|
"start": "vite preview --host",
|
|
14
17
|
"preview": "vite preview --host",
|
|
15
|
-
"check": "tsc --noEmit"
|
|
18
|
+
"check": "tsc --noEmit",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "node --test \"tests/*.test.ts\"",
|
|
21
|
+
"test:watch": "node --test --watch \"tests/*.test.ts\"",
|
|
22
|
+
"lint": "biome check .",
|
|
23
|
+
"restore:entry": "node -e \"const{readFileSync}=require('node:fs');const{execSync}=require('node:child_process');const script=/script[^>]*App\\.tsx/;const triad=/family=Bricolage\\+Grotesque[\\s\\S]*Outfit[\\s\\S]*JetBrains\\+Mono/;const face=(h)=>script.test(h)&&triad.test(h);if(face(readFileSync('index.html','utf8'))){console.log('index.html already rides the source face');process.exit(0)}if(!face(execSync('git show HEAD:./index.html',{encoding:'utf8'}))){console.error('refusing: HEAD index.html is not the canonical source face yet (script App.tsx + the font triad) - land the source-face fix first; nothing was touched');process.exit(1)}execSync('git checkout -- index.html',{stdio:'inherit'});console.log('index.html restored from HEAD (the source face)')\""
|
|
16
24
|
},
|
|
17
25
|
"dependencies": {
|
|
18
26
|
"@hono/node-server": "^2.1.3",
|
|
@@ -35,7 +43,22 @@
|
|
|
35
43
|
"LICENSE"
|
|
36
44
|
],
|
|
37
45
|
"engines": {
|
|
38
|
-
"
|
|
46
|
+
"bare": "^1.18.0",
|
|
47
|
+
"bun": "^1.4.2",
|
|
48
|
+
"chrome": "^16.0.912",
|
|
49
|
+
"cnpm": "^9.4.0",
|
|
50
|
+
"corepack": "^0.35.0",
|
|
51
|
+
"deno": "^2.9.5",
|
|
52
|
+
"ecmascript": "^5.0.0",
|
|
53
|
+
"firefox": ">=0.8.0",
|
|
54
|
+
"iojs": "^1.0.0",
|
|
55
|
+
"node": "^26.8.2",
|
|
56
|
+
"npm": "^12.1.0",
|
|
57
|
+
"pnpm": "^12.6.0",
|
|
58
|
+
"teleport": ">=0.2.0",
|
|
59
|
+
"typescript": "^5.8.0",
|
|
60
|
+
"vscode": "^1.118.0",
|
|
61
|
+
"yarn": "^4.17.1"
|
|
39
62
|
},
|
|
40
63
|
"publishConfig": {
|
|
41
64
|
"access": "public"
|
|
@@ -46,6 +69,7 @@
|
|
|
46
69
|
"@capacitor/cli": "^8.5.2",
|
|
47
70
|
"@capacitor/core": "^8.5.2",
|
|
48
71
|
"@capacitor/ios": "^8.5.2",
|
|
72
|
+
"@fluentui/react-icons": "^2.0.344",
|
|
49
73
|
"@hookform/resolvers": "^5.9.1",
|
|
50
74
|
"@libsql/client": "^0.18.0",
|
|
51
75
|
"@opencode-ai/sdk": "^1.18.34",
|
|
@@ -144,6 +168,9 @@
|
|
|
144
168
|
"mysql2": "^3.23.1",
|
|
145
169
|
"uuid": "^11.1.1",
|
|
146
170
|
"postcss-selector-parser": "^7.1.6"
|
|
147
|
-
}
|
|
171
|
+
},
|
|
172
|
+
"neverBuiltDependencies": [
|
|
173
|
+
"ssh2"
|
|
174
|
+
]
|
|
148
175
|
}
|
|
149
176
|
}
|
package/tests/.gitkeep
CHANGED
|
File without changes
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// # design.test — the design contract of the forge surface, runnable with
|
|
2
|
+
// the node built-in runner (no dependencies, no install):
|
|
3
|
+
// node --test tests/design.test.ts
|
|
4
|
+
// The file reads the shipped sources (css, entry face, brand mark, app
|
|
5
|
+
// identity, route anchor, flow sources) and pins the family laws of the
|
|
6
|
+
// reform wave: the trylle foundation (canvas, surfaces, Inter + Geist Mono),
|
|
7
|
+
// the windows hands (accent, close red, no pill, no all-properties
|
|
8
|
+
// transition, no zero-scale entry outside the dtPopIntro bounce), the
|
|
9
|
+
// source-face entry, the reduced-motion stop, tabular numerals, the honest
|
|
10
|
+
// page set and the first-run flow contract (the entry gate, the shared
|
|
11
|
+
// onboarding state, the db mirror).
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { describe, it } from "node:test";
|
|
15
|
+
|
|
16
|
+
function read(path: string): string {
|
|
17
|
+
return readFileSync(new URL(path, import.meta.url), "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CSS = read("../Sol/sol.css");
|
|
21
|
+
const ENTRY = read("../index.html");
|
|
22
|
+
const MARK = read("../Sol/shell/BrandMark.tsx");
|
|
23
|
+
const TOML = read("../devthink.toml");
|
|
24
|
+
const SOL = read("../Sol/Sol.tsx");
|
|
25
|
+
const GATE = read("../Sol/home/entrygate.tsx");
|
|
26
|
+
const STATE = read("../onboardingstate.ts");
|
|
27
|
+
const ONBOARDING = read("../Sol/onboarding/onboarding.tsx");
|
|
28
|
+
const INTRO = read("../Sol/intro/intro.tsx");
|
|
29
|
+
const SHELL = read("../Sol/shell/Shell.tsx");
|
|
30
|
+
const PRISMA = read("../schema.prisma");
|
|
31
|
+
const DB = read("../db.ts");
|
|
32
|
+
|
|
33
|
+
const BANNED = [/3b82f6/i, /2563eb/i, /f97316/i, /f59e0b/i];
|
|
34
|
+
|
|
35
|
+
describe("design contract", () => {
|
|
36
|
+
it("ships no banned hue in the stylesheet, the entry or the mark", () => {
|
|
37
|
+
for (const source of [CSS, ENTRY, MARK]) {
|
|
38
|
+
for (const banned of BANNED) {
|
|
39
|
+
assert.ok(!banned.test(source), `banned hue ${banned} found`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("builds on the trylle foundation (canvas, deep floor, ink)", () => {
|
|
45
|
+
assert.match(CSS, /--sol-canvas: #0a0a0b/);
|
|
46
|
+
assert.match(CSS, /--sol-canvas-deep: #060607/);
|
|
47
|
+
assert.match(CSS, /--sol-ink: #fafafa/);
|
|
48
|
+
assert.match(CSS, /--sol-surface-1: #171717/);
|
|
49
|
+
assert.match(CSS, /--sol-surface-8: #484848/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("carries the forge lime #a2cb3a as the one identity signal (brand, CTAs, active ladder)", () => {
|
|
53
|
+
assert.match(CSS, /--fg-lime: #a2cb3a/);
|
|
54
|
+
assert.match(CSS, /--sol-primary: #a2cb3a/);
|
|
55
|
+
assert.match(MARK, /#a2cb3a/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rides the windows accent #4cc2ff for focus (the win10 #0067c0 in the light face)", () => {
|
|
59
|
+
assert.match(CSS, /--win-accent: #4cc2ff/);
|
|
60
|
+
assert.match(CSS, /--win-accent: #0067c0/);
|
|
61
|
+
assert.match(CSS, /--sol-focus: var\(--win-accent\)/);
|
|
62
|
+
const dark = CSS.indexOf("--win-accent: #4cc2ff");
|
|
63
|
+
const light = CSS.indexOf("--win-accent: #0067c0");
|
|
64
|
+
assert.ok(light > dark, "the light accent must be restated after the dark one");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps the pill out of the stylesheet (no 999px, no stadium radius)", () => {
|
|
68
|
+
assert.ok(!CSS.includes("999px"), "the pill radius is retired");
|
|
69
|
+
assert.ok(!CSS.includes("--r-pill"), "no pill token survives");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("never ships transition: all", () => {
|
|
73
|
+
assert.ok(!/transition:\s*all/.test(CSS), "transition: all is a reprovação");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("never enters an element from zero scale (the windows dtPopIntro bounce excepted)", () => {
|
|
77
|
+
const sansPopIntro = CSS.replace(/@keyframes dtPopIntro \{[\s\S]*?\n\}/g, "");
|
|
78
|
+
assert.ok(
|
|
79
|
+
!/scale\(0\)/.test(sansPopIntro),
|
|
80
|
+
"scale(0) entries are a reprovação (the dtPopIntro bounce of the windows recipe excepted)",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("declares the trylle font stacks (Inter reading, Geist Mono detail)", () => {
|
|
85
|
+
assert.match(CSS, /--font-sans: "Inter Variable"/);
|
|
86
|
+
assert.match(CSS, /--font-mono: "Geist Mono Variable"/);
|
|
87
|
+
assert.match(ENTRY, /family=Inter/);
|
|
88
|
+
assert.match(ENTRY, /Geist\+Mono/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("keeps the entry on the source face (./App.tsx, canonical assets, no hashed ghosts)", () => {
|
|
92
|
+
assert.match(ENTRY, /script[^>]*\.\/App\.tsx/);
|
|
93
|
+
assert.ok(!/-[0-9a-f]{8}\.(js|css)/.test(ENTRY), "no hashed bundle rides the entry");
|
|
94
|
+
assert.match(ENTRY, /href="\.\/favicon\.svg"/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("guards the reduced-motion stop on every layer, the hard stop last", () => {
|
|
98
|
+
const blocks = CSS.match(/@media \(prefers-reduced-motion: reduce\)/g) ?? [];
|
|
99
|
+
assert.ok(blocks.length >= 4, `expected the hard stop on every layer, found ${blocks.length}`);
|
|
100
|
+
const hardStop = CSS.indexOf("animation-duration: 0.01ms !important");
|
|
101
|
+
assert.ok(hardStop > 0, "the .01ms hard stop must exist");
|
|
102
|
+
assert.ok(/@media \(prefers-reduced-motion: reduce\)[\s\S]*$/.test(CSS), "the guards close the file");
|
|
103
|
+
const lastGuard = CSS.lastIndexOf("@media (prefers-reduced-motion: reduce)");
|
|
104
|
+
assert.ok(CSS.indexOf("animation-duration: 0.01ms", lastGuard) > lastGuard, "the hard stop is the last word");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("floors the acrylics on reduced transparency (the window values)", () => {
|
|
108
|
+
assert.match(CSS, /@media \(prefers-reduced-transparency: reduce\)/);
|
|
109
|
+
assert.match(CSS, /background-color: #202020/);
|
|
110
|
+
assert.match(CSS, /background-color: #2b2b2b/);
|
|
111
|
+
assert.match(CSS, /background-color: #2c2c2c/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("keeps tabular numerals on the platform numbers", () => {
|
|
115
|
+
assert.match(CSS, /font-variant-numeric:\s*tabular-nums/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rests the title-bar mark on the landing (logo discipline)", () => {
|
|
119
|
+
assert.match(CSS, /data-landing/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("wears the windows hands (press scale .7, dtPopIntro, the state ladder, the red close)", () => {
|
|
123
|
+
assert.match(CSS, /@keyframes dtPopIntro/);
|
|
124
|
+
assert.match(CSS, /transform: scale\(0\.7\)/);
|
|
125
|
+
assert.match(CSS, /--win-close: rgb\(232, 17, 35\)/);
|
|
126
|
+
assert.match(CSS, /--win-close-hot: rgb\(139, 10, 20\)/);
|
|
127
|
+
assert.match(CSS, /grayscale\(80%\)/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("declares the honest page set (home, runs, queue, logs, notfound)", () => {
|
|
131
|
+
assert.match(TOML, /pages = \["home", "runs", "queue", "logs", "notfound"\]/);
|
|
132
|
+
for (const route of ['"/runs"', '"/queue"', '"/logs"', '"/404"']) {
|
|
133
|
+
assert.ok(SOL.includes(`path=${route}`), `route ${route} must mount`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps the entry flow mounted (intro, onboarding) before the gated fundamentals", () => {
|
|
138
|
+
assert.match(SOL, /path="\/intro"/);
|
|
139
|
+
assert.match(SOL, /path="\/onboarding"/);
|
|
140
|
+
assert.ok(SOL.indexOf('"/intro"') < SOL.indexOf('"/"'), "the intro route precedes the home route");
|
|
141
|
+
assert.match(SOL, /EntryGate/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("carries the flow contract sources (the gate beside the home anchor, the shared state module)", () => {
|
|
145
|
+
assert.match(GATE, /navigate\("\/intro", \{ replace: true \}\)/);
|
|
146
|
+
assert.match(GATE, /navigate\("\/onboarding", \{ replace: true \}\)/);
|
|
147
|
+
assert.match(GATE, /loadOnboardingState/);
|
|
148
|
+
assert.match(STATE, /forge\.onboarding\.seen/);
|
|
149
|
+
assert.match(STATE, /\/api\/v1\/onboarding/);
|
|
150
|
+
assert.match(STATE, /VITE_ONBOARDING_URL/);
|
|
151
|
+
assert.match(INTRO, /markIntroSeen/);
|
|
152
|
+
assert.match(ONBOARDING, /markOnboardingSeen/);
|
|
153
|
+
assert.ok(!INTRO.includes("sessionStorage"), "the sessionStorage-only flag is retired");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("hands the intro directly to the onboarding (the auto-advance)", () => {
|
|
157
|
+
assert.match(INTRO, /navigate\("\/onboarding"\)/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("marks the onboarding stage on finish and skip alike", () => {
|
|
161
|
+
const finishes = ONBOARDING.match(/markOnboardingSeen/g) ?? [];
|
|
162
|
+
assert.ok(finishes.length >= 2, "finish and skip both write the stage");
|
|
163
|
+
assert.match(ONBOARDING, /navigate\("\/"\)/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("catalogues the first-run state in the db layer (prisma model + sqlite mirror)", () => {
|
|
167
|
+
assert.match(PRISMA, /model OnboardingState \{/);
|
|
168
|
+
assert.match(PRISMA, /introseen\s+Boolean\s+@default\(false\)/);
|
|
169
|
+
assert.match(PRISMA, /onboardingseen\s+Boolean\s+@default\(false\)/);
|
|
170
|
+
assert.match(DB, /CREATE TABLE IF NOT EXISTS onboarding_state/);
|
|
171
|
+
assert.match(DB, /getonboardingstate/);
|
|
172
|
+
assert.match(DB, /setonboardingstage/);
|
|
173
|
+
assert.match(DB, /MAX\(introseen, excluded\.introseen\)/, "the upsert is seen-wins");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("reforms the navigation to the 48px pin rail with the windows hands", () => {
|
|
177
|
+
assert.match(CSS, /--win-rail-w: 48px/);
|
|
178
|
+
assert.match(SHELL, /data-tip=\{app\.label\}/);
|
|
179
|
+
assert.match(SHELL, /data-dir="se"/, "the eight resize handles ride the frame");
|
|
180
|
+
assert.match(SHELL, /setPointerCapture/);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// # family.test — the family table the chrome rides, runnable with the node
|
|
2
|
+
// built-in runner (no dependencies, no install):
|
|
3
|
+
// node --test tests/family.test.ts
|
|
4
|
+
// Every test watches the promise the row makes: the nine siblings beside
|
|
5
|
+
// forge are listed once, every slug resolves through the familyurl contract
|
|
6
|
+
// into a climb-one-level url, every member carries the spec §12 accent with
|
|
7
|
+
// its own dot ink, and no accent ever collides with the banned palette.
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { describe, it } from "node:test";
|
|
10
|
+
import { FAMILY_ACCENTS, FAMILY_SLUGS, familyAccentOf, familyrow } from "../family.ts";
|
|
11
|
+
import { familyurl } from "../familyurl.ts";
|
|
12
|
+
|
|
13
|
+
const BANNED = new Set(["#3b82f6", "#2563eb", "#f97316"]);
|
|
14
|
+
|
|
15
|
+
describe("family table", () => {
|
|
16
|
+
it("lists the nine siblings beside forge, never forge itself", () => {
|
|
17
|
+
assert.deepEqual(FAMILY_SLUGS, [
|
|
18
|
+
"devthink",
|
|
19
|
+
"cadria",
|
|
20
|
+
"stealthhead",
|
|
21
|
+
"debonair",
|
|
22
|
+
"argan",
|
|
23
|
+
"saddle",
|
|
24
|
+
"foundry",
|
|
25
|
+
"vault",
|
|
26
|
+
"getry",
|
|
27
|
+
]);
|
|
28
|
+
assert.ok(!FAMILY_SLUGS.includes("forge"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("answers an accent for every listed sibling", () => {
|
|
32
|
+
for (const slug of FAMILY_SLUGS) {
|
|
33
|
+
const member = FAMILY_ACCENTS[slug];
|
|
34
|
+
assert.ok(member, `${slug} must carry an accent`);
|
|
35
|
+
assert.equal(member.slug, slug);
|
|
36
|
+
assert.match(member.accent, /^#[0-9a-fA-F]{6}$/);
|
|
37
|
+
assert.match(member.dotInk, /^#[0-9a-fA-F]{6}$/);
|
|
38
|
+
}
|
|
39
|
+
// the table holds exactly the ten deploy units — no orphan accents
|
|
40
|
+
assert.deepEqual(Object.keys(FAMILY_ACCENTS).sort(), [
|
|
41
|
+
"argan",
|
|
42
|
+
"cadria",
|
|
43
|
+
"debonair",
|
|
44
|
+
"devthink",
|
|
45
|
+
"forge",
|
|
46
|
+
"foundry",
|
|
47
|
+
"getry",
|
|
48
|
+
"saddle",
|
|
49
|
+
"stealthhead",
|
|
50
|
+
"vault",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps the spec §12 accents stable, verbatim", () => {
|
|
55
|
+
assert.equal(FAMILY_ACCENTS.devthink.accent, "#F59E0B");
|
|
56
|
+
assert.equal(FAMILY_ACCENTS.argan.accent, "#34d8a8");
|
|
57
|
+
assert.equal(FAMILY_ACCENTS.cadria.accent, "#f472b6");
|
|
58
|
+
assert.equal(FAMILY_ACCENTS.debonair.accent, "#d9962e");
|
|
59
|
+
assert.equal(FAMILY_ACCENTS.forge.accent, "#a2cb3a");
|
|
60
|
+
assert.equal(FAMILY_ACCENTS.foundry.accent, "#2fb8b5");
|
|
61
|
+
assert.equal(FAMILY_ACCENTS.getry.accent, "#8b5cf6");
|
|
62
|
+
assert.equal(FAMILY_ACCENTS.saddle.accent, "#e86f2d");
|
|
63
|
+
assert.equal(FAMILY_ACCENTS.stealthhead.accent, "#e8563f");
|
|
64
|
+
assert.equal(FAMILY_ACCENTS.vault.accent, "#a3d7e6");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps the spec accent-ink column stable", () => {
|
|
68
|
+
assert.equal(FAMILY_ACCENTS.forge.dotInk, "#161d03");
|
|
69
|
+
assert.equal(FAMILY_ACCENTS.foundry.dotInk, "#03211f");
|
|
70
|
+
assert.equal(FAMILY_ACCENTS.vault.dotInk, "#072330");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resolves every member through the familyurl contract", () => {
|
|
74
|
+
for (const slug of FAMILY_SLUGS) {
|
|
75
|
+
assert.equal(familyurl(slug), `../${slug}/`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("never ships a banned hue in the row or the table", () => {
|
|
80
|
+
for (const member of [...familyrow(), ...Object.values(FAMILY_ACCENTS)]) {
|
|
81
|
+
assert.ok(!BANNED.has(member.accent.toLowerCase()), `${member.slug} rides a banned accent`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("builds the rail row in slug order without gaps", () => {
|
|
86
|
+
const row = familyrow();
|
|
87
|
+
assert.equal(row.length, FAMILY_SLUGS.length);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
row.map((member) => member.slug),
|
|
90
|
+
[...FAMILY_SLUGS],
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("answers familyAccentOf for the house and falls back for unknowns", () => {
|
|
95
|
+
assert.equal(familyAccentOf("forge")?.accent, "#a2cb3a");
|
|
96
|
+
assert.equal(familyAccentOf("vault")?.accent, "#a3d7e6");
|
|
97
|
+
assert.equal(familyAccentOf("nope"), undefined);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps the familyurl contract on the climb-one-level shape", () => {
|
|
101
|
+
assert.equal(familyurl("cadria"), "../cadria/");
|
|
102
|
+
assert.equal(familyurl("/cadria/"), "../cadria/");
|
|
103
|
+
assert.equal(familyurl("cadria"), familyurl(" cadria ".trim()));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// # fleet.test — the runner fleet engine of the /runs page, runnable with
|
|
2
|
+
// the node built-in runner (no dependencies, no install):
|
|
3
|
+
// node --test tests/fleet.test.ts
|
|
4
|
+
// The fleet is a seeded rehearsal: the same seed musters the same runners,
|
|
5
|
+
// the health ladder is pure thresholds, the tick advances the floor
|
|
6
|
+
// deterministically and never heals an offline runner by hiding its record.
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import {
|
|
10
|
+
fleetSummary,
|
|
11
|
+
fleetTick,
|
|
12
|
+
HEALTH_LADDER,
|
|
13
|
+
healthRungOf,
|
|
14
|
+
mulberry32,
|
|
15
|
+
pickRunner,
|
|
16
|
+
RUNNER_MODES,
|
|
17
|
+
RUNNER_STATES,
|
|
18
|
+
type Runner,
|
|
19
|
+
seedFleet,
|
|
20
|
+
} from "../fleet.ts";
|
|
21
|
+
|
|
22
|
+
/** one hand-built runner for the threshold tests */
|
|
23
|
+
function runnerOf(patch: Partial<Runner>): Runner {
|
|
24
|
+
return {
|
|
25
|
+
id: "fgr-test-01",
|
|
26
|
+
name: "runner-test-01",
|
|
27
|
+
mode: "runner",
|
|
28
|
+
state: "busy",
|
|
29
|
+
load: 0.4,
|
|
30
|
+
runs: 100,
|
|
31
|
+
failures: 0,
|
|
32
|
+
...patch,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("seeded fleet", () => {
|
|
37
|
+
it("musters the same runners from the same seed, every time", () => {
|
|
38
|
+
const a = seedFleet(20260214, 8);
|
|
39
|
+
const b = seedFleet(20260214, 8);
|
|
40
|
+
assert.deepEqual(a, b);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("musters a fleet of the requested size with unique ids and names", () => {
|
|
44
|
+
const fleet = seedFleet(7, 12);
|
|
45
|
+
assert.equal(fleet.length, 12);
|
|
46
|
+
assert.equal(new Set(fleet.map((r) => r.id)).size, 12);
|
|
47
|
+
assert.equal(new Set(fleet.map((r) => r.name)).size, 12);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("answers only real states, modes and load ranges", () => {
|
|
51
|
+
for (const runner of seedFleet(99, 40)) {
|
|
52
|
+
assert.ok(RUNNER_STATES.includes(runner.state), `state ${runner.state} is on the ladder`);
|
|
53
|
+
assert.ok(RUNNER_MODES.includes(runner.mode), `mode ${runner.mode} is a saddle mode`);
|
|
54
|
+
assert.ok(runner.load >= 0 && runner.load <= 1, `load ${runner.load} rides 0..1`);
|
|
55
|
+
assert.ok(runner.runs >= 0 && runner.failures >= 0);
|
|
56
|
+
assert.ok(runner.failures <= runner.runs, "failures never exceed runs");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps offline runners honest: no load, no fresh runs", () => {
|
|
61
|
+
const fleet = seedFleet(1234, 60);
|
|
62
|
+
for (const runner of fleet.filter((r) => r.state === "offline")) {
|
|
63
|
+
assert.equal(runner.failures, 0);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("prng", () => {
|
|
69
|
+
it("mulberry32 is deterministic across calls", () => {
|
|
70
|
+
const a = mulberry32(42);
|
|
71
|
+
const b = mulberry32(42);
|
|
72
|
+
for (let i = 0; i < 16; i++) assert.equal(a(), b());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("health ladder", () => {
|
|
77
|
+
it("is the three-rung ladder green → amber → red", () => {
|
|
78
|
+
assert.deepEqual(HEALTH_LADDER, ["green", "amber", "red"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("paints an offline runner red with an honest why", () => {
|
|
82
|
+
const rung = healthRungOf(runnerOf({ state: "offline" }));
|
|
83
|
+
assert.equal(rung.rung, 2);
|
|
84
|
+
assert.equal(rung.label, "red");
|
|
85
|
+
assert.match(rung.why, /offline/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("paints a failure rate over 20% red", () => {
|
|
89
|
+
const rung = healthRungOf(runnerOf({ runs: 10, failures: 3 }));
|
|
90
|
+
assert.equal(rung.label, "red");
|
|
91
|
+
assert.match(rung.why, /failure rate/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("paints a draining runner amber", () => {
|
|
95
|
+
const rung = healthRungOf(runnerOf({ state: "draining" }));
|
|
96
|
+
assert.equal(rung.rung, 1);
|
|
97
|
+
assert.equal(rung.label, "amber");
|
|
98
|
+
assert.match(rung.why, /draining/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("paints a saturated runner amber (load over 85%)", () => {
|
|
102
|
+
const rung = healthRungOf(runnerOf({ load: 0.9 }));
|
|
103
|
+
assert.equal(rung.label, "amber");
|
|
104
|
+
assert.match(rung.why, /load/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps a healthy busy runner green", () => {
|
|
108
|
+
const rung = healthRungOf(runnerOf({ load: 0.5, runs: 40, failures: 1 }));
|
|
109
|
+
assert.equal(rung.rung, 0);
|
|
110
|
+
assert.equal(rung.label, "green");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("computes the failure rate honestly (zero runs → zero)", () => {
|
|
114
|
+
assert.equal(healthRungOf(runnerOf({ runs: 0, failures: 0 })).label, "green");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("fleet tick", () => {
|
|
119
|
+
it("advances the fleet deterministically", () => {
|
|
120
|
+
const fleet = seedFleet(20260214, 8);
|
|
121
|
+
const t1 = fleetTick(fleet, 20260214, 1);
|
|
122
|
+
const t1again = fleetTick(fleet, 20260214, 1);
|
|
123
|
+
assert.deepEqual(t1, t1again);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("never mutates the input fleet", () => {
|
|
127
|
+
const fleet = seedFleet(5, 6);
|
|
128
|
+
const frozen = JSON.parse(JSON.stringify(fleet)) as Runner[];
|
|
129
|
+
fleetTick(fleet, 5, 3);
|
|
130
|
+
assert.deepEqual(fleet, frozen);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("keeps runs monotonic and loads inside 0..1", () => {
|
|
134
|
+
let fleet = seedFleet(321, 8);
|
|
135
|
+
for (let tick = 1; tick <= 12; tick++) {
|
|
136
|
+
const next = fleetTick(fleet, 321, tick);
|
|
137
|
+
for (let i = 0; i < next.length; i++) {
|
|
138
|
+
assert.ok(next[i].runs >= fleet[i].runs, "runs never go backwards");
|
|
139
|
+
assert.ok(next[i].load >= 0 && next[i].load <= 1);
|
|
140
|
+
}
|
|
141
|
+
fleet = next;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("never heals an offline runner by hiding its record", () => {
|
|
146
|
+
const fleet = [runnerOf({ id: "fgr-off", name: "runner-off-01", state: "offline", runs: 10, failures: 4 })];
|
|
147
|
+
const next = fleetTick(fleet, 1, 1)[0];
|
|
148
|
+
assert.equal(next.state, "offline");
|
|
149
|
+
assert.equal(next.failures, 4);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("fleet summary", () => {
|
|
154
|
+
it("counts states and rungs exactly", () => {
|
|
155
|
+
const fleet = [
|
|
156
|
+
runnerOf({ id: "a", state: "busy", load: 0.5 }),
|
|
157
|
+
runnerOf({ id: "b", state: "idle", load: 0.1 }),
|
|
158
|
+
runnerOf({ id: "c", state: "offline", load: 0 }),
|
|
159
|
+
];
|
|
160
|
+
const summary = fleetSummary(fleet);
|
|
161
|
+
assert.equal(summary.total, 3);
|
|
162
|
+
assert.equal(summary.byState.busy, 1);
|
|
163
|
+
assert.equal(summary.byState.idle, 1);
|
|
164
|
+
assert.equal(summary.byState.offline, 1);
|
|
165
|
+
assert.equal(summary.byHealth.red, 1);
|
|
166
|
+
assert.equal(summary.byHealth.green, 2);
|
|
167
|
+
assert.equal(summary.healthy, false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("answers a healthy floor when no runner rides red", () => {
|
|
171
|
+
const summary = fleetSummary([
|
|
172
|
+
runnerOf({ id: "a", state: "busy", load: 0.5 }),
|
|
173
|
+
runnerOf({ id: "b", state: "idle", load: 0.2 }),
|
|
174
|
+
]);
|
|
175
|
+
assert.equal(summary.healthy, true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("answers a zero busy load on an empty floor", () => {
|
|
179
|
+
const summary = fleetSummary([]);
|
|
180
|
+
assert.equal(summary.total, 0);
|
|
181
|
+
assert.equal(summary.busyLoad, 0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("pick runner", () => {
|
|
186
|
+
it("never hands work to an offline or red runner", () => {
|
|
187
|
+
const fleet = [
|
|
188
|
+
runnerOf({ id: "a", state: "offline" }),
|
|
189
|
+
runnerOf({ id: "b", runs: 10, failures: 5 }),
|
|
190
|
+
runnerOf({ id: "c", state: "idle", load: 0.1 }),
|
|
191
|
+
];
|
|
192
|
+
const picked = pickRunner(fleet, "runner");
|
|
193
|
+
assert.ok(picked);
|
|
194
|
+
assert.equal(picked.id, "c");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("prefers the greener, lighter runner of the requested mode", () => {
|
|
198
|
+
const fleet = [
|
|
199
|
+
runnerOf({ id: "light", state: "idle", load: 0.1 }),
|
|
200
|
+
runnerOf({ id: "heavy", state: "busy", load: 0.9 }),
|
|
201
|
+
];
|
|
202
|
+
assert.equal(pickRunner(fleet, "runner")?.id, "light");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("refuses honestly when nobody of the mode can take the work", () => {
|
|
206
|
+
const fleet = [runnerOf({ id: "a", mode: "docker", state: "offline" })];
|
|
207
|
+
assert.equal(pickRunner(fleet, "runner"), undefined);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// # queue.test — the execution queue engine of the /queue page, runnable
|
|
2
|
+
// with the node built-in runner (no dependencies, no install):
|
|
3
|
+
// node --test tests/queue.test.ts
|
|
4
|
+
// The queue never lies: storage is append-only, the serving order is
|
|
5
|
+
// priority first and FIFO inside the lane, the depth counts exactly and
|
|
6
|
+
// the rehearsal seed replays the same queue every time.
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import {
|
|
10
|
+
cancel,
|
|
11
|
+
depthByPriority,
|
|
12
|
+
dequeue,
|
|
13
|
+
enqueue,
|
|
14
|
+
nextUp,
|
|
15
|
+
PRIORITY_RANK,
|
|
16
|
+
positionOf,
|
|
17
|
+
QUEUE_PRIORITIES,
|
|
18
|
+
type QueueItem,
|
|
19
|
+
queueDepth,
|
|
20
|
+
queueFromSeed,
|
|
21
|
+
queueOrder,
|
|
22
|
+
} from "../queue.ts";
|
|
23
|
+
|
|
24
|
+
/** a draft without the seq (enqueue stamps it) */
|
|
25
|
+
function draft(id: string, priority: QueueItem["priority"], seqLess: true = true): Omit<QueueItem, "seq"> {
|
|
26
|
+
void seqLess;
|
|
27
|
+
return { id, label: `run ${id}`, mode: "runner", priority, enqueuedAt: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("queue order", () => {
|
|
31
|
+
it("serves express before normal before batch", () => {
|
|
32
|
+
const queue = [draft("b", "batch"), draft("e", "express"), draft("n", "normal")] as QueueItem[];
|
|
33
|
+
const seeded = queue.reduce((acc, item) => enqueue(acc, item), [] as QueueItem[]);
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
queueOrder(seeded).map((item) => item.id),
|
|
36
|
+
["e", "n", "b"],
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps FIFO inside a lane (arrival order breaks the ties)", () => {
|
|
41
|
+
let queue: QueueItem[] = [];
|
|
42
|
+
queue = enqueue(queue, draft("a1", "normal"));
|
|
43
|
+
queue = enqueue(queue, draft("a2", "normal"));
|
|
44
|
+
queue = enqueue(queue, draft("a3", "normal"));
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
queueOrder(queue).map((item) => item.id),
|
|
47
|
+
["a1", "a2", "a3"],
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("ranks the priorities in serve order", () => {
|
|
52
|
+
assert.equal(PRIORITY_RANK.express, 0);
|
|
53
|
+
assert.equal(PRIORITY_RANK.normal, 1);
|
|
54
|
+
assert.equal(PRIORITY_RANK.batch, 2);
|
|
55
|
+
assert.deepEqual(QUEUE_PRIORITIES, ["express", "normal", "batch"]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("enqueue", () => {
|
|
60
|
+
it("stamps monotonic arrival seqs", () => {
|
|
61
|
+
let queue: QueueItem[] = [];
|
|
62
|
+
queue = enqueue(queue, draft("a", "express"));
|
|
63
|
+
queue = enqueue(queue, draft("b", "express"));
|
|
64
|
+
queue = enqueue(queue, draft("c", "express"));
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
queue.map((item) => item.seq),
|
|
67
|
+
[1, 2, 3],
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("appends without mutating the input queue", () => {
|
|
72
|
+
const before: QueueItem[] = [{ id: "x", label: "run x", mode: "docker", priority: "batch", seq: 1, enqueuedAt: 0 }];
|
|
73
|
+
const after = enqueue(before, draft("y", "normal"));
|
|
74
|
+
assert.equal(before.length, 1);
|
|
75
|
+
assert.equal(after.length, 2);
|
|
76
|
+
assert.deepEqual(before[0], { id: "x", label: "run x", mode: "docker", priority: "batch", seq: 1, enqueuedAt: 0 });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("dequeue", () => {
|
|
81
|
+
it("pops the head of the serving order and removes it", () => {
|
|
82
|
+
let queue: QueueItem[] = [];
|
|
83
|
+
queue = enqueue(queue, draft("batch-1", "batch"));
|
|
84
|
+
queue = enqueue(queue, draft("express-1", "express"));
|
|
85
|
+
const answer = dequeue(queue);
|
|
86
|
+
assert.equal(answer.item?.id, "express-1");
|
|
87
|
+
assert.equal(answer.queue.length, 1);
|
|
88
|
+
assert.equal(answer.queue[0].id, "batch-1");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("answers an honest empty when the queue is empty", () => {
|
|
92
|
+
const answer = dequeue([]);
|
|
93
|
+
assert.equal(answer.item, undefined);
|
|
94
|
+
assert.equal(answer.queue.length, 0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("serves FIFO inside the lane across dequeues", () => {
|
|
98
|
+
let queue: QueueItem[] = [];
|
|
99
|
+
queue = enqueue(queue, draft("n1", "normal"));
|
|
100
|
+
queue = enqueue(queue, draft("n2", "normal"));
|
|
101
|
+
const first = dequeue(queue);
|
|
102
|
+
const second = dequeue(first.queue);
|
|
103
|
+
assert.equal(first.item?.id, "n1");
|
|
104
|
+
assert.equal(second.item?.id, "n2");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("cancel", () => {
|
|
109
|
+
it("removes only the cancelled run", () => {
|
|
110
|
+
let queue: QueueItem[] = [];
|
|
111
|
+
queue = enqueue(queue, draft("a", "normal"));
|
|
112
|
+
queue = enqueue(queue, draft("b", "normal"));
|
|
113
|
+
const next = cancel(queue, "a");
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
next.map((item) => item.id),
|
|
116
|
+
["b"],
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("keeps the queue untouched when the id is unknown", () => {
|
|
121
|
+
const queue = enqueue(enqueue([], draft("a", "normal")), draft("b", "normal"));
|
|
122
|
+
const next = cancel(queue, "ghost");
|
|
123
|
+
assert.equal(next.length, 2);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("depth and position", () => {
|
|
128
|
+
it("counts the depth exactly", () => {
|
|
129
|
+
let queue: QueueItem[] = [];
|
|
130
|
+
for (let i = 0; i < 5; i++) queue = enqueue(queue, draft(`r${i}`, "batch"));
|
|
131
|
+
assert.equal(queueDepth(queue), 5);
|
|
132
|
+
assert.equal(queueDepth([]), 0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("counts the lanes per priority", () => {
|
|
136
|
+
let queue: QueueItem[] = [];
|
|
137
|
+
queue = enqueue(queue, draft("e1", "express"));
|
|
138
|
+
queue = enqueue(queue, draft("n1", "normal"));
|
|
139
|
+
queue = enqueue(queue, draft("n2", "normal"));
|
|
140
|
+
queue = enqueue(queue, draft("b1", "batch"));
|
|
141
|
+
const lanes = depthByPriority(queue);
|
|
142
|
+
assert.equal(lanes.express, 1);
|
|
143
|
+
assert.equal(lanes.normal, 2);
|
|
144
|
+
assert.equal(lanes.batch, 1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("answers the position in the serving order", () => {
|
|
148
|
+
let queue: QueueItem[] = [];
|
|
149
|
+
queue = enqueue(queue, draft("express", "express"));
|
|
150
|
+
queue = enqueue(queue, draft("normal", "normal"));
|
|
151
|
+
queue = enqueue(queue, draft("batch", "batch"));
|
|
152
|
+
assert.equal(positionOf(queue, "express"), 0);
|
|
153
|
+
assert.equal(positionOf(queue, "batch"), 2);
|
|
154
|
+
assert.equal(positionOf(queue, "ghost"), -1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("rehearsal queue", () => {
|
|
159
|
+
it("replays the same queue from the same seed", () => {
|
|
160
|
+
assert.deepEqual(queueFromSeed(731004, 7), queueFromSeed(731004, 7));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("stamps unique ids and seqs on every item", () => {
|
|
164
|
+
const queue = queueFromSeed(42, 9);
|
|
165
|
+
assert.equal(queue.length, 9);
|
|
166
|
+
assert.equal(new Set(queue.map((item) => item.id)).size, 9);
|
|
167
|
+
assert.equal(new Set(queue.map((item) => item.seq)).size, 9);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("answers nextUp as the head of the serving order", () => {
|
|
171
|
+
const queue = queueFromSeed(731004, 7);
|
|
172
|
+
assert.equal(nextUp(queue)?.id, queueOrder(queue)[0]?.id);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// # runstream.test — the deterministic log replay of the /logs page,
|
|
2
|
+
// runnable with the node built-in runner (no dependencies, no install):
|
|
3
|
+
// node --test tests/runstream.test.ts
|
|
4
|
+
// One seed unrolls the whole line stream: the same seed answers the same
|
|
5
|
+
// lines forever, the outcome derives from the stream itself, the prefix
|
|
6
|
+
// window and the census are pure.
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import { formatLogTime, linesWithin, logCensus, logDuration, logOutcome, logText, runLogLines } from "../runstream.ts";
|
|
10
|
+
|
|
11
|
+
describe("replay determinism", () => {
|
|
12
|
+
it("answers the same stream from the same seed", () => {
|
|
13
|
+
assert.deepEqual(runLogLines(421337), runLogLines(421337));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("answers the same stream for the same seed at any size", () => {
|
|
17
|
+
const a = runLogLines(7, 9);
|
|
18
|
+
const b = runLogLines(7, 9);
|
|
19
|
+
assert.deepEqual(a, b);
|
|
20
|
+
assert.equal(a.length, 3 + 9 + 2);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("opens every run with the three boot lines in order", () => {
|
|
24
|
+
const lines = runLogLines(1);
|
|
25
|
+
assert.match(lines[0].text, /^floor: accepting run on the /);
|
|
26
|
+
assert.match(lines[1].text, /mount sandbox/);
|
|
27
|
+
assert.match(lines[2].text, /restore working set/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("keeps the stream chronological (t never goes backwards)", () => {
|
|
31
|
+
const lines = runLogLines(90012);
|
|
32
|
+
for (let i = 1; i < lines.length; i++) {
|
|
33
|
+
assert.ok(lines[i].t >= lines[i - 1].t, `line ${i} drifted backwards`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("answers differing streams for differing seeds (over 20 seeds)", () => {
|
|
38
|
+
const firsts = new Set<string>();
|
|
39
|
+
for (let seed = 1; seed <= 20; seed++) {
|
|
40
|
+
const lines = runLogLines(seed);
|
|
41
|
+
firsts.add(JSON.stringify(lines));
|
|
42
|
+
}
|
|
43
|
+
// at most two identical streams across 20 seeds is well within chance
|
|
44
|
+
assert.ok(firsts.size >= 18, `expected near-unique streams, got ${firsts.size}`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("outcome honesty", () => {
|
|
49
|
+
it("derives success when the stream carries no error", () => {
|
|
50
|
+
const lines = runLogLines(421337);
|
|
51
|
+
if (lines.some((line) => line.level === "error")) return;
|
|
52
|
+
assert.equal(logOutcome(lines), "success");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("derives failed from the error line itself", () => {
|
|
56
|
+
const lines = runLogLines(1, 1); // small stream; find a failing seed
|
|
57
|
+
let failing: ReturnType<typeof runLogLines> | undefined;
|
|
58
|
+
for (let seed = 0; seed < 64 && !failing; seed++) {
|
|
59
|
+
const candidate = runLogLines(seed);
|
|
60
|
+
if (candidate.some((line) => line.level === "error")) failing = candidate;
|
|
61
|
+
}
|
|
62
|
+
assert.ok(failing, "some seed in 0..63 must fail");
|
|
63
|
+
void lines;
|
|
64
|
+
assert.equal(logOutcome(failing), "failed");
|
|
65
|
+
const last = failing[failing.length - 1];
|
|
66
|
+
assert.match(last.text, /outcome: failed/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("closes a successful run with the report line", () => {
|
|
70
|
+
for (let seed = 0; seed < 64; seed++) {
|
|
71
|
+
const lines = runLogLines(seed);
|
|
72
|
+
if (!lines.some((line) => line.level === "error")) {
|
|
73
|
+
assert.match(lines[lines.length - 1].text, /outcome: success/);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
assert.fail("no successful seed found in 0..63");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("window and clock", () => {
|
|
82
|
+
it("linesWithin answers the chronological prefix", () => {
|
|
83
|
+
const lines = runLogLines(5124);
|
|
84
|
+
const mid = logDuration(lines) / 2;
|
|
85
|
+
const head = linesWithin(lines, mid);
|
|
86
|
+
assert.ok(head.length > 0);
|
|
87
|
+
assert.ok(head.length < lines.length);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
head,
|
|
90
|
+
lines.filter((line) => line.t <= mid),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("linesWithin at zero answers only the boot head, at full the whole stream", () => {
|
|
95
|
+
const lines = runLogLines(5124);
|
|
96
|
+
assert.ok(linesWithin(lines, 0).length >= 1);
|
|
97
|
+
assert.deepEqual(linesWithin(lines, logDuration(lines)), lines);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("logDuration answers the last timestamp", () => {
|
|
101
|
+
const lines = runLogLines(77031);
|
|
102
|
+
assert.equal(logDuration(lines), Math.max(...lines.map((line) => line.t)));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("formatLogTime renders the tabular mm:ss.mmm clock", () => {
|
|
106
|
+
assert.equal(formatLogTime(0), "00:00.000");
|
|
107
|
+
assert.equal(formatLogTime(1000), "00:01.000");
|
|
108
|
+
assert.equal(formatLogTime(61_234), "01:01.234");
|
|
109
|
+
assert.equal(formatLogTime(600_000), "10:00.000");
|
|
110
|
+
assert.equal(formatLogTime(-5), "00:00.000");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("logCensus counts every level and sums to the total", () => {
|
|
114
|
+
const lines = runLogLines(77031);
|
|
115
|
+
const census = logCensus(lines);
|
|
116
|
+
assert.equal(census.info + census.warn + census.error, lines.length);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("logText renders one line per stream line with the clock first", () => {
|
|
120
|
+
const lines = runLogLines(421337);
|
|
121
|
+
const text = logText(lines);
|
|
122
|
+
const rows = text.split("\n");
|
|
123
|
+
assert.equal(rows.length, lines.length);
|
|
124
|
+
assert.match(rows[0], /^\d{2}:\d{2}\.\d{3} info /);
|
|
125
|
+
});
|
|
126
|
+
});
|