@wenathlan/stealthhead 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 +15 -0
- package/docs/.gitkeep +0 -0
- package/package.json +23 -3
- package/tests/.gitkeep +0 -0
- package/tests/design.test.ts +160 -0
- package/tests/family.test.ts +81 -0
- package/tests/familyurl.test.ts +0 -0
- package/tests/hud.test.ts +0 -0
- package/tests/matchrules.test.ts +0 -0
- package/tests/netcode.test.ts +0 -0
- package/tests/onboardingstate.test.ts +62 -0
- package/tests/physics.test.ts +0 -0
- package/tests/rankingladder.test.ts +0 -0
- package/tests/spawner.test.ts +0 -0
- package/tests/weaponstats.test.ts +0 -0
package/README.md
CHANGED
|
@@ -15,3 +15,18 @@ pnpm install
|
|
|
15
15
|
pnpm dev
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## The family contract (wave 7-a)
|
|
19
|
+
|
|
20
|
+
- `family.ts` — the one table of the spec §12 accents (argan jade, cadria rosa, debonair latão, forge lime-brasa, foundry têmpera, getry violeta, saddle ember, vault gelo) and the sibling slugs in rail order; the chrome rail foot and the landing footer both render it, and `tests/family.test.ts` pins it (8 cases).
|
|
21
|
+
- `familyurl.ts` — the relative sibling-link contract (`"../" + slug + "/"`, verbatim body); `familyurl(slug)` survives any mount point without trusting a build constant.
|
|
22
|
+
- `tests/design.test.ts` — 12 design-contract cases: the coral `#f4694f` as the one signal (stylesheet alias and mark story alike), the windows accent `#4cc2ff` on focus and the chrome, zero banned hue (`#3B82F6` / `#2563EB` / `#F97316`), zero pill (no `999px`, no `--r-pill`), zero `transition: all`, zero `scale(0)` entries, the trylle font pair (Inter Variable reading, Geist Mono detail), the source-face entry, the reduced-motion hard stop, tabular numerals, the landing logo discipline (`data-landing`), the sticky-footer law and the honest page set of `devthink.toml`.
|
|
23
|
+
|
|
24
|
+
## Entry face and `restore:entry`
|
|
25
|
+
|
|
26
|
+
`index.html` is kept permanently on the SOURCE face: `./App.tsx` script, `./favicon.svg`, `./manifest.webmanifest` and the font triad `<link>`. The Vite build emits hashed bundles (`index-*.js`, `index-*.css`, `favicon-*.svg`, `manifest-*.webmanifest`…) beside it for the pages workflow — every family is barred by `.gitignore`, so the tree keeps the source face only. After a local build, run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run restore:entry
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The script is a GUARD, not a blind checkout: if the working tree already rides the source face it is a no-op; if `HEAD` still carries a pre-triad (hashed or triad-less) face it REFUSES with exit 1 — it never revives a stale face over the fix. Land the source-face commit first, and from then on the guard restores the canonical face after any local build.
|
package/docs/.gitkeep
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/stealthhead",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.117",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/wenathlan/devthink"
|
|
7
7
|
},
|
|
8
|
-
"description": "stealthhead is the FPS game platform of the wenathlan family: it imports versawase from cadria and audio from debonair via the published library, with no engine file of its own
|
|
8
|
+
"description": "stealthhead is the FPS game platform of the wenathlan family: it imports versawase from cadria and audio from debonair via the published library, with no engine file of its own \u2014 only game logics (match, ranking, weapons, world). The envelope distributes the documentation set and the tests directory as one package.",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"stealthhead",
|
|
11
11
|
"fps",
|
|
@@ -16,6 +16,24 @@
|
|
|
16
16
|
"documentation"
|
|
17
17
|
],
|
|
18
18
|
"license": "GPL-3.0-only",
|
|
19
|
+
"engines": {
|
|
20
|
+
"bare": "^1.18.0",
|
|
21
|
+
"bun": "^1.4.2",
|
|
22
|
+
"chrome": "^16.0.912",
|
|
23
|
+
"cnpm": "^9.4.0",
|
|
24
|
+
"corepack": "^0.35.0",
|
|
25
|
+
"deno": "^2.9.5",
|
|
26
|
+
"ecmascript": "^5.0.0",
|
|
27
|
+
"firefox": ">=0.8.0",
|
|
28
|
+
"iojs": "^1.0.0",
|
|
29
|
+
"node": "^26.8.2",
|
|
30
|
+
"npm": "^12.1.0",
|
|
31
|
+
"pnpm": "^12.6.0",
|
|
32
|
+
"teleport": ">=0.2.0",
|
|
33
|
+
"typescript": "^5.8.0",
|
|
34
|
+
"vscode": "^1.118.0",
|
|
35
|
+
"yarn": "^4.17.1"
|
|
36
|
+
},
|
|
19
37
|
"type": "module",
|
|
20
38
|
"scripts": {
|
|
21
39
|
"dev": "vite --host",
|
|
@@ -23,7 +41,9 @@
|
|
|
23
41
|
"start": "vite preview --host",
|
|
24
42
|
"preview": "vite preview --host",
|
|
25
43
|
"check": "tsc --noEmit",
|
|
26
|
-
"
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"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)')\""
|
|
27
47
|
},
|
|
28
48
|
"dependencies": {
|
|
29
49
|
"@hono/node-server": "^2.1.3",
|
package/tests/.gitkeep
CHANGED
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// # design.test — the design contract of the stealthhead surface, runnable
|
|
2
|
+
// with the vitest runner:
|
|
3
|
+
// npm test
|
|
4
|
+
// The file reads the shipped sources (stylesheet, entry face, brand mark,
|
|
5
|
+
// app identity, route anchor, icons) and pins the family laws of the reform
|
|
6
|
+
// wave: no banned hue, no pill, no all-properties transition, no zero-scale
|
|
7
|
+
// entry (the windows dtPopIntro bounce excepted), the trylle font stacks,
|
|
8
|
+
// the source-face entry, the reduced-motion stop, tabular numerals, the
|
|
9
|
+
// sticky-footer law, the honest page set of the application identity, the
|
|
10
|
+
// first-run flow contract (the entry gate, the shared onboarding state, the
|
|
11
|
+
// db mirror) and the trylle-canvas/windows-hands foundation.
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
function read(path: string): string {
|
|
16
|
+
return readFileSync(new URL(path, import.meta.url), "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CSS = read("../Sol/sol.css");
|
|
20
|
+
const ENTRY = read("../index.html");
|
|
21
|
+
const SHELL = read("../Sol/shell/Shell.tsx");
|
|
22
|
+
const MARK = read("../Sol/shell/BrandMark.tsx");
|
|
23
|
+
const TOML = read("../devthink.toml");
|
|
24
|
+
const SOL = read("../Sol/Sol.tsx");
|
|
25
|
+
const FAVICON = read("../favicon.svg");
|
|
26
|
+
const ICON = read("../icon.svg");
|
|
27
|
+
const MANIFEST = read("../manifest.webmanifest");
|
|
28
|
+
|
|
29
|
+
const BANNED = [/3b82f6/i, /2563eb/i, /f97316/i];
|
|
30
|
+
|
|
31
|
+
describe("design contract", () => {
|
|
32
|
+
it("ships no banned hue in the stylesheet, the entry, the mark or the icons", () => {
|
|
33
|
+
for (const source of [CSS, ENTRY, MARK, FAVICON, ICON, MANIFEST]) {
|
|
34
|
+
for (const banned of BANNED) {
|
|
35
|
+
expect(banned.test(source), `banned hue ${banned} found`).toBe(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("carries the stealthhead coral #f4694f as the identity accent and the windows accent on the chrome", () => {
|
|
41
|
+
expect(CSS).toMatch(/--sh-accent: #f4694f/);
|
|
42
|
+
expect(MARK).toMatch(/#f4694f/);
|
|
43
|
+
// the windows hands: focus and the chrome speak the windows accent
|
|
44
|
+
expect(CSS).toMatch(/--win-accent: #4cc2ff/);
|
|
45
|
+
expect(CSS).toMatch(/outline: 2px solid var\(--win-accent\)/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps the pill out of the stylesheet (no 999px, no stadium radius)", () => {
|
|
49
|
+
expect(CSS.includes("999px"), "the pill radius is retired").toBe(false);
|
|
50
|
+
expect(CSS.includes("--r-pill"), "no pill token survives").toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("never ships an all-properties transition", () => {
|
|
54
|
+
expect(/transition:\s*all/.test(CSS), "transition: all is a reprovação").toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("never enters an element from zero scale (the windows dtPopIntro bounce excepted)", () => {
|
|
58
|
+
const sansPopIntro = CSS.replace(/@keyframes dtPopIntro \{[\s\S]*?\n\}/g, "");
|
|
59
|
+
expect(
|
|
60
|
+
/scale\(0\)/.test(sansPopIntro),
|
|
61
|
+
"scale(0) entries are a reprovação (the dtPopIntro bounce of the windows recipe excepted)",
|
|
62
|
+
).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("declares the trylle font stacks (Inter reading, Geist Mono detail) and the 13px body", () => {
|
|
66
|
+
expect(CSS).toMatch(/--font-sans: "Inter Variable"/);
|
|
67
|
+
expect(CSS).toMatch(/--font-mono: "Geist Mono Variable"/);
|
|
68
|
+
expect(ENTRY).toMatch(/family=Inter/);
|
|
69
|
+
expect(ENTRY).toMatch(/Geist\+Mono/);
|
|
70
|
+
expect(CSS).toMatch(/font-size: 13px/);
|
|
71
|
+
expect(CSS).toMatch(/font-weight: 450/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("keeps the entry on the source face (./App.tsx, canonical assets, no hashed ghosts)", () => {
|
|
75
|
+
expect(ENTRY).toMatch(/script[^>]*\.\/App\.tsx/);
|
|
76
|
+
expect(/-[0-9a-f]{8}\.(js|css)/.test(ENTRY), "no hashed bundle rides the entry").toBe(false);
|
|
77
|
+
expect(ENTRY).toMatch(/href="\.\/favicon\.svg"/);
|
|
78
|
+
expect(ENTRY).toMatch(/href="\.\/manifest\.webmanifest"/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("guards the reduced-motion stop on every layer (the hard stop last)", () => {
|
|
82
|
+
const blocks = CSS.match(/@media \(prefers-reduced-motion: reduce\)/g) ?? [];
|
|
83
|
+
expect(blocks.length >= 4, `expected the hard stop on every layer, found ${blocks.length}`).toBe(true);
|
|
84
|
+
const hardStopAt = CSS.lastIndexOf("@media (prefers-reduced-motion: reduce)");
|
|
85
|
+
expect(CSS.slice(hardStopAt)).toMatch(/0\.01ms !important/);
|
|
86
|
+
expect(CSS.slice(hardStopAt + 1)).not.toMatch(/@media/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps tabular numerals on the platform numbers", () => {
|
|
90
|
+
expect(CSS).toMatch(/font-variant-numeric:\s*tabular-nums/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rests the title-bar mark on the landing (logo discipline)", () => {
|
|
94
|
+
expect(CSS).toMatch(/data-landing/);
|
|
95
|
+
expect(SHELL).toMatch(/data-landing/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("pins the footer to the window floor (the sticky-footer law)", () => {
|
|
99
|
+
expect(CSS).toMatch(/\.winapp__stage > main\.shell \{[^}]*flex-direction: column/);
|
|
100
|
+
expect(CSS).toMatch(/\.shquad \{[^}]*margin-top: auto/s);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("declares the honest page set (home, match, ranking, weapons, world, notfound)", () => {
|
|
104
|
+
expect(TOML).toMatch(/pages = \["home", "match", "ranking", "weapons", "world", "notfound"\]/);
|
|
105
|
+
for (const route of ['"/match"', '"/ranking"', '"/weapons"', '"/world"', '"/404"']) {
|
|
106
|
+
expect(SOL.includes(`path=${route}`), `route ${route} must mount`).toBe(true);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps the entry flow mounted (intro, onboarding) before the fundamentals", () => {
|
|
111
|
+
expect(SOL).toMatch(/path="\/intro"/);
|
|
112
|
+
expect(SOL).toMatch(/path="\/onboarding"/);
|
|
113
|
+
expect(SOL.indexOf('"/intro"')).toBeLessThan(SOL.indexOf('"/"'));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("carries the flow contract sources (the entry gate behind the home anchor, the shared state module)", () => {
|
|
117
|
+
const GATE = read("../Sol/home/entrygate.tsx");
|
|
118
|
+
const STATE = read("../onboardingstate.ts");
|
|
119
|
+
const HOME = read("../Sol/home/home.tsx");
|
|
120
|
+
const ONBOARDING = read("../Sol/onboarding/onboarding.tsx");
|
|
121
|
+
expect(GATE).toMatch(/navigate\("\/intro"/);
|
|
122
|
+
expect(GATE).toMatch(/navigate\("\/onboarding"/);
|
|
123
|
+
expect(HOME).toMatch(/EntryGate/);
|
|
124
|
+
expect(ONBOARDING).toMatch(/markOnboardingStage/);
|
|
125
|
+
expect(STATE).toMatch(/stealthhead\.onboarding\.seen/);
|
|
126
|
+
expect(STATE).toMatch(/\/api\/v1\/onboarding/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("catalogues the first-run state in the db layer (prisma model + sqlite mirror)", () => {
|
|
130
|
+
const PRISMA = read("../schema.prisma");
|
|
131
|
+
const DB = read("../db.ts");
|
|
132
|
+
expect(PRISMA).toMatch(/model OnboardingState \{/);
|
|
133
|
+
expect(PRISMA).toMatch(/introseen\s+Boolean\s+@default\(false\)/);
|
|
134
|
+
expect(PRISMA).toMatch(/onboardingseen\s+Boolean\s+@default\(false\)/);
|
|
135
|
+
expect(DB).toMatch(/create table if not exists onboardingstate/);
|
|
136
|
+
expect(DB).toMatch(/getonboardingstate/);
|
|
137
|
+
expect(DB).toMatch(/setonboardingstage/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("wears the windows hands on the chrome (resize handles, inactive law, red close, dtPopIntro)", () => {
|
|
141
|
+
expect(CSS).toMatch(/winapp__rz\[data-edge="se"\]/);
|
|
142
|
+
expect(CSS).toMatch(/grayscale\(80%\)/);
|
|
143
|
+
expect(CSS).toMatch(/rgb\(232, 17, 35\)/);
|
|
144
|
+
expect(CSS).toMatch(/rgb\(139, 10, 20\)/);
|
|
145
|
+
expect(CSS).toMatch(/@keyframes dtPopIntro/);
|
|
146
|
+
expect(SHELL).toMatch(/data-inactive/);
|
|
147
|
+
expect(SHELL).toMatch(/winapp__rz/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("rides the trylle foundation (canvas, surfaces, hairlines, linear shadow ladder)", () => {
|
|
151
|
+
expect(CSS).toMatch(/--sol-canvas: #0a0a0b/);
|
|
152
|
+
expect(CSS).toMatch(/--sol-canvas-deep: #060607/);
|
|
153
|
+
expect(CSS).toMatch(/--sol-ink: #fafafa/);
|
|
154
|
+
expect(CSS).toMatch(/--sol-surface-8: #484848/);
|
|
155
|
+
expect(CSS).toMatch(/color-mix\(in oklab, var\(--sol-ink\) 5%, transparent\)/);
|
|
156
|
+
expect(CSS).toMatch(/0 96px 96px -48px/);
|
|
157
|
+
expect(CSS).toMatch(/cubic-bezier\(0\.22, 1, 0\.36, 1\)/);
|
|
158
|
+
expect(CSS).toMatch(/--sol-micro: 150ms/);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// # family.test — the family table the chrome rides, runnable with the
|
|
2
|
+
// vitest runner:
|
|
3
|
+
// npm test
|
|
4
|
+
// Every test watches the promise the row makes: the eight siblings beside
|
|
5
|
+
// stealthhead are listed once, every slug resolves through the familyurl
|
|
6
|
+
// contract into a climb-one-level url, every member carries the spec §12
|
|
7
|
+
// accent with its own dot ink, and no accent ever collides with the banned
|
|
8
|
+
// palette.
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { describe, it } from "vitest";
|
|
11
|
+
import { FAMILY_ACCENTS, FAMILY_SLUGS, familyrow } from "../family.ts";
|
|
12
|
+
import { familyurl } from "../familyurl.ts";
|
|
13
|
+
|
|
14
|
+
const BANNED = new Set(["#3b82f6", "#2563eb", "#f97316"]);
|
|
15
|
+
|
|
16
|
+
describe("family table", () => {
|
|
17
|
+
it("lists the eight siblings beside stealthhead, never stealthhead itself", () => {
|
|
18
|
+
assert.deepEqual(FAMILY_SLUGS, ["argan", "cadria", "debonair", "forge", "foundry", "getry", "saddle", "vault"]);
|
|
19
|
+
assert.ok(!FAMILY_SLUGS.includes("stealthhead"));
|
|
20
|
+
assert.ok(!FAMILY_SLUGS.includes("devthink"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("answers an accent for every listed sibling", () => {
|
|
24
|
+
for (const slug of FAMILY_SLUGS) {
|
|
25
|
+
const member = FAMILY_ACCENTS[slug];
|
|
26
|
+
assert.ok(member, `${slug} must carry an accent`);
|
|
27
|
+
assert.equal(member.slug, slug);
|
|
28
|
+
assert.match(member.accent, /^#[0-9a-f]{6}$/);
|
|
29
|
+
assert.match(member.dotInk, /^#[0-9a-f]{6}$/);
|
|
30
|
+
}
|
|
31
|
+
// the table holds exactly the siblings — no orphan accents
|
|
32
|
+
assert.deepEqual(Object.keys(FAMILY_ACCENTS).sort(), [...FAMILY_SLUGS].sort());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("resolves every member through the familyurl contract", () => {
|
|
36
|
+
for (const slug of FAMILY_SLUGS) {
|
|
37
|
+
assert.equal(familyurl(slug), `../${slug}/`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("strips decorative slashes and normalizes the slug", () => {
|
|
42
|
+
assert.equal(familyurl("/cadria/"), "../cadria/");
|
|
43
|
+
assert.equal(familyurl(" getry "), "../ getry /");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the spec §12 accents stable", () => {
|
|
47
|
+
assert.equal(FAMILY_ACCENTS.argan.accent, "#34d8a8");
|
|
48
|
+
assert.equal(FAMILY_ACCENTS.cadria.accent, "#f472b6");
|
|
49
|
+
assert.equal(FAMILY_ACCENTS.debonair.accent, "#d9962e");
|
|
50
|
+
assert.equal(FAMILY_ACCENTS.forge.accent, "#a2cb3a");
|
|
51
|
+
assert.equal(FAMILY_ACCENTS.foundry.accent, "#2fb8b5");
|
|
52
|
+
assert.equal(FAMILY_ACCENTS.getry.accent, "#8b5cf6");
|
|
53
|
+
assert.equal(FAMILY_ACCENTS.saddle.accent, "#e86f2d");
|
|
54
|
+
assert.equal(FAMILY_ACCENTS.vault.accent, "#a3d7e6");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("never ships a banned hue in the row", () => {
|
|
58
|
+
for (const member of familyrow()) {
|
|
59
|
+
assert.ok(!BANNED.has(member.accent), `${member.slug} rides a banned accent`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("builds the rail row in slug order without gaps", () => {
|
|
64
|
+
const row = familyrow();
|
|
65
|
+
assert.equal(row.length, FAMILY_SLUGS.length);
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
row.map((member) => member.slug),
|
|
68
|
+
[...FAMILY_SLUGS],
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("keeps every dot ink readable on its own accent (the ink is darker)", () => {
|
|
73
|
+
for (const member of familyrow()) {
|
|
74
|
+
const lum = (hex: string): number => {
|
|
75
|
+
const channel = (offset: number) => Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
|
|
76
|
+
return 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5);
|
|
77
|
+
};
|
|
78
|
+
assert.ok(lum(member.dotInk) < lum(member.accent), `${member.slug} dot ink must be darker than its accent`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
package/tests/familyurl.test.ts
CHANGED
|
File without changes
|
package/tests/hud.test.ts
CHANGED
|
File without changes
|
package/tests/matchrules.test.ts
CHANGED
|
File without changes
|
package/tests/netcode.test.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// # onboardingstate.test — the pure contract of the seen flags: the host
|
|
2
|
+
// validation of the platform base (https only, public hosts only) and the
|
|
3
|
+
// seen-wins stage merge the accessor and the pages share. runnable with
|
|
4
|
+
// vitest: pnpm vitest run tests/onboardingstate.test.ts
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
|
+
import { hostallowed, mergeStage } from "../onboardingstate.ts";
|
|
8
|
+
|
|
9
|
+
describe("onboarding state", () => {
|
|
10
|
+
it("accepts public https bases", () => {
|
|
11
|
+
assert.equal(hostallowed("https://gateway.wenathlan.dev"), true);
|
|
12
|
+
assert.equal(hostallowed("https://gateway.wenathlan.dev/"), true);
|
|
13
|
+
assert.equal(hostallowed("https://stealthhead.example.com:8443"), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("refuses plaintext, localhost, loopback and every private or reserved range", () => {
|
|
17
|
+
const refused = [
|
|
18
|
+
"http://gateway.wenathlan.dev",
|
|
19
|
+
"https://localhost",
|
|
20
|
+
"https://localhost:8787",
|
|
21
|
+
"https://app.localhost",
|
|
22
|
+
"https://box.local",
|
|
23
|
+
"https://service.internal",
|
|
24
|
+
"https://127.0.0.1",
|
|
25
|
+
"https://0.0.0.0",
|
|
26
|
+
"https://10.0.0.9",
|
|
27
|
+
"https://172.16.0.1",
|
|
28
|
+
"https://172.31.255.255",
|
|
29
|
+
"https://192.168.1.4",
|
|
30
|
+
"https://169.254.10.10",
|
|
31
|
+
"https://100.64.0.1",
|
|
32
|
+
"https://192.0.2.7",
|
|
33
|
+
"https://198.51.100.7",
|
|
34
|
+
"https://203.0.113.9",
|
|
35
|
+
"https://[::1]",
|
|
36
|
+
"https://[::ffff:127.0.0.1]",
|
|
37
|
+
"https://[fd00::1]",
|
|
38
|
+
"https://[fe80::1]",
|
|
39
|
+
"not a url",
|
|
40
|
+
"",
|
|
41
|
+
];
|
|
42
|
+
for (const base of refused) assert.equal(hostallowed(base), false, base);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("merges stages seen-wins — a stage never unsees", () => {
|
|
46
|
+
const fresh = { introseen: false, onboardingseen: false };
|
|
47
|
+
assert.deepEqual(mergeStage(fresh, "intro"), { introseen: true, onboardingseen: false });
|
|
48
|
+
assert.deepEqual(mergeStage(fresh, "onboarding"), { introseen: false, onboardingseen: true });
|
|
49
|
+
const half = { introseen: true, onboardingseen: false };
|
|
50
|
+
assert.deepEqual(mergeStage(half, "onboarding"), { introseen: true, onboardingseen: true });
|
|
51
|
+
assert.deepEqual(mergeStage(mergeStage(half, "onboarding"), "intro"), {
|
|
52
|
+
introseen: true,
|
|
53
|
+
onboardingseen: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("never mutates the state it merges", () => {
|
|
58
|
+
const state = { introseen: false, onboardingseen: false };
|
|
59
|
+
mergeStage(state, "intro");
|
|
60
|
+
assert.deepEqual(state, { introseen: false, onboardingseen: false });
|
|
61
|
+
});
|
|
62
|
+
});
|
package/tests/physics.test.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/tests/spawner.test.ts
CHANGED
|
File without changes
|
|
File without changes
|