File size: 7,367 Bytes
31b3c47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import ts from "typescript";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
// CRLF-normalized so the line-anchored checks hold on a Windows checkout.
const CI = readFileSync(join(ROOT, ".github/workflows/ci.yml"), "utf8").replace(/\r\n/g, "\n");
const PACKAGE = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const LOCK = JSON.parse(readFileSync(join(ROOT, "package-lock.json"), "utf8"));

// The SHAs szl-holdings/.github already runs, so the org has one pin per action.
const APPROVED_PINS = {
  "actions/checkout": "3d3c42e5aac5ba805825da76410c181273ba90b1", // v7.0.1
  "actions/setup-node": "820762786026740c76f36085b0efc47a31fe5020", // v7.0.0
  "actions/setup-python": "5fda3b95a4ea91299a34e894583c3862153e4b97", // v7.0.0
  "step-security/harden-runner": "e14015d583714f6e62063499dc959a02595150a1", // v2.21.1
};

function onBlock() {
  const match = CI.match(/^on:\n([\s\S]*?)^(?=\S)/m);
  assert.ok(match, "missing on: block");
  return match[1];
}

function jobSteps(job) {
  const match = CI.match(
    new RegExp(String.raw`^  ${job}:\n([\s\S]*?)(?=^  [A-Za-z_][\w-]*:\n|(?![\s\S]))`, "m"),
  );
  assert.ok(match, `missing job ${job}`);
  return match[1];
}

test("CI runs on every pull request and every push to main, with no path filter", () => {
  const on = onBlock();
  assert.match(on, /^ {2}push:\n {4}branches: \[main\]$/m);
  assert.match(on, /^ {2}pull_request:$/m);
  assert.doesNotMatch(on, /paths(?:-ignore)?:/);
  assert.doesNotMatch(on, /branches-ignore:/);
});

test("CI is read-only", () => {
  assert.match(CI, /^permissions:\n {2}contents: read\n/m);
  assert.doesNotMatch(CI, /:\s*write\b/);
  assert.doesNotMatch(CI, /secrets\./);
  assert.doesNotMatch(CI, /persist-credentials: true/);
});

test("every action is pinned to the SHA the org already uses", () => {
  const uses = [...CI.matchAll(/^\s*uses:\s*(\S+)/gm)].map((match) => match[1]);
  assert.ok(uses.length >= 6);
  for (const value of uses) {
    const [action, ref] = value.split("@");
    assert.ok(action in APPROVED_PINS, `unapproved action: ${value}`);
    assert.equal(ref, APPROVED_PINS[action], `wrong pin for ${action}`);
  }
});

test("each job hardens the runner before anything else runs", () => {
  for (const job of ["node", "python", "space"]) {
    const steps = jobSteps(job);
    const firstUses = steps.match(/^\s*uses:\s*(\S+)/m);
    assert.ok(firstUses, `${job}: no steps`);
    assert.match(firstUses[1], /^step-security\/harden-runner@/, `${job}: first step`);
  }
});

test("the node job runs the full npm gate", () => {
  const steps = jobSteps("node");
  const order = ["npm ci", "npm run typecheck", "npm run lint", "npm test", "npm run build"];
  let last = -1;
  for (const command of order) {
    const at = steps.indexOf(`run: ${command}\n`);
    assert.notEqual(at, -1, `node job does not run: ${command}`);
    assert.ok(at > last, `${command} is out of order`);
    last = at;
  }
  for (const script of ["typecheck", "lint", "test", "build"]) {
    assert.ok(PACKAGE.scripts[script], `package.json has no "${script}" script`);
  }
});

test("TypeScript stays on the supported 6.0 patch line with an exact matching lock", () => {
  // typescript-eslint 8.70.1 and 8.71.0 admit >=4.8.4 <6.1.0, not TS 7.
  // A future minor/major migration must qualify its lint peer contract too.
  assert.match(PACKAGE.devDependencies.typescript, /^~6\.0\.\d+$/);
  assert.equal(LOCK.packages[""].devDependencies.typescript, PACKAGE.devDependencies.typescript);
  assert.match(LOCK.packages["node_modules/typescript"].version, /^6\.0\.\d+$/);
  assert.equal(
    LOCK.packages["node_modules/typescript-eslint"].peerDependencies.typescript,
    ">=4.8.4 <6.1.0",
  );
});

test("CI never overrides incompatible dependency peers to pass installation", () => {
  assert.doesNotMatch(CI, /--legacy-peer-deps|--force\b|strict-peer-deps:\s*false/);
});

test("Nitro's optional cache peer is explicit without overriding Babel's major", () => {
  assert.equal(PACKAGE.devDependencies["lru-cache"], "11.5.3");
  assert.equal(LOCK.packages[""].devDependencies["lru-cache"], "11.5.3");
  assert.equal(LOCK.packages["node_modules/lru-cache"].version, "11.5.3");
  assert.equal(
    LOCK.packages["node_modules/@babel/helper-compilation-targets/node_modules/lru-cache"].version,
    "5.1.1",
  );
});

test("the Space smoke retries health, root and source readback", () => {
  const steps = jobSteps("space");
  assert.match(steps, /fetch_with_retry\(\)/);
  assert.match(
    steps,
    /fetch_with_retry http:\/\/127\.0\.0\.1:7860\/healthz \/tmp\/nexus-health\.json/,
  );
  assert.match(steps, /fetch_with_retry http:\/\/127\.0\.0\.1:7860\/ \/tmp\/nexus-root\.html/);
  assert.match(
    steps,
    /fetch_with_retry http:\/\/127\.0\.0\.1:7860\/api\/build-info \/tmp\/nexus-build\.json/,
  );
  assert.match(steps, /--connect-timeout 2 --max-time 10/);
});

test("the Space smoke waits for Docker health instead of sampling once", () => {
  const steps = jobSteps("space");
  assert.match(steps, /wait_for_docker_health\(\)/);
  assert.match(steps, /for attempt in \$\(seq 1 60\)/);
  assert.match(steps, /if \[ "\$state" = healthy \]/);
  assert.match(steps, /if \[ "\$state" = unhealthy \]/);
  assert.match(steps, /container health did not stabilize/);
  assert.doesNotMatch(
    steps,
    /test "\$\(docker inspect --format '\{\{\.State\.Health\.Status\}\}' "\$name"\)" = healthy/,
  );
});

test("the source alias resolves without deprecated baseUrl or a suppression", () => {
  const { config, error } = ts.readConfigFile(join(ROOT, "tsconfig.json"), ts.sys.readFile);
  assert.equal(error, undefined);
  assert.equal(config.compilerOptions.baseUrl, undefined);
  assert.equal(config.compilerOptions.ignoreDeprecations, undefined);
  const parsed = ts.parseJsonConfigFileContent(config, ts.sys, ROOT);
  assert.equal(parsed.errors.length, 0);
  const resolved = ts.resolveModuleName(
    "@/lib/nexus/types",
    join(ROOT, "src/routes/index.tsx"),
    parsed.options,
    ts.sys,
  ).resolvedModule;
  assert.ok(resolved, "the compiler must retain the @/ source alias");
  assert.equal(
    resolved.resolvedFileName.replaceAll("\\", "/"),
    join(ROOT, "src/lib/nexus/types.ts").replaceAll("\\", "/"),
  );
});

test("the node job runs on both Node LTS lines the repository uses", () => {
  const steps = jobSteps("node");
  assert.match(steps, /^ {8}node: \["22", "24"\]$/m);
  assert.match(steps, /node-version: \$\{\{ matrix\.node \}\}/);
  assert.match(steps, /fail-fast: false/);
});

test("npm test runs the cross-platform runner, not a shell glob", () => {
  assert.equal(PACKAGE.scripts.test, "node --experimental-strip-types scripts/run-tests.mjs");
  assert.doesNotMatch(PACKAGE.scripts.test, /[*']/);
});

test("the python job compiles, self-tests and runs pytest when tests exist", () => {
  const steps = jobSteps("python");
  assert.match(steps, /git ls-files -z -- '\*\.py' \| xargs -0 -r python -m py_compile/);
  assert.match(steps, /run: python server\.py --selftest\n/);
  assert.match(steps, /run: python source_bound_server\.py --selftest\n/);
  assert.match(steps, /python -m pytest -q/);
});