jishushell 0.4.30 → 0.5.22
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/Dockerfile.hermes-slim +2 -5
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +18 -6
- package/apps/filebrowser-container.yaml +164 -0
- package/apps/ollama-binary.yaml +44 -0
- package/apps/ollama-with-hollama-binary.yaml +45 -1
- package/apps/openclaw-binary.yaml +8 -0
- package/apps/openclaw-container.yaml +9 -1
- package/apps/openclaw-with-searxng-container.yaml +4 -0
- package/apps/searxng-container.yaml +5 -4
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/doctor.js +144 -16
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/install.js +4 -4
- package/dist/install.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +87 -12
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +29 -0
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +9 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.js +358 -6
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +3 -0
- package/dist/services/agent-apps/catalog.js +40 -13
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +19 -2
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +3 -0
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +9 -0
- package/dist/services/app/app-manager.js +248 -43
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +1 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +37 -5
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +220 -8
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.js +26 -7
- package/dist/services/capability-endpoint-validator.js.map +1 -1
- package/dist/services/connection-apply.d.ts +2 -0
- package/dist/services/connection-apply.js +55 -1
- package/dist/services/connection-apply.js.map +1 -1
- package/dist/services/connection-resolver.js +1 -1
- package/dist/services/connection-resolver.js.map +1 -1
- package/dist/services/connection-transactor.d.ts +2 -0
- package/dist/services/connection-transactor.js +12 -2
- package/dist/services/connection-transactor.js.map +1 -1
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.js +90 -32
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +28 -0
- package/dist/services/llm-proxy/index.js +76 -3
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +6 -2
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/llm-proxy/validate-key.d.ts +41 -0
- package/dist/services/llm-proxy/validate-key.js +672 -0
- package/dist/services/llm-proxy/validate-key.js.map +1 -0
- package/dist/services/macos-launchd.d.ts +89 -0
- package/dist/services/macos-launchd.js +273 -0
- package/dist/services/macos-launchd.js.map +1 -0
- package/dist/services/nomad-manager.d.ts +11 -0
- package/dist/services/nomad-manager.js +343 -98
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +40 -11
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +3 -2
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.js +56 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +4 -3
- package/dist/services/runtime/adapters/hermes.js +166 -64
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +118 -0
- package/dist/services/runtime/adapters/openclaw.js +1459 -49
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/types.d.ts +31 -0
- package/dist/services/setup-manager.js +190 -68
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.js.map +1 -1
- package/dist/services/update-manager.js +32 -14
- package/dist/services/update-manager.js.map +1 -1
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +247 -35
- package/install/jishu-uninstall.sh +45 -5
- package/package.json +20 -2
- package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
- package/public/assets/Dashboard-AuJESBlJ.js +1 -0
- package/public/assets/{HermesChatPanel-_GHoklgo.js → HermesChatPanel-CByPREwb.js} +1 -1
- package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
- package/public/assets/InitPassword-ka4wNpM5.js +1 -0
- package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
- package/public/assets/Login-aPajuQzf.js +1 -0
- package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
- package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
- package/public/assets/Settings-BYQnbLYL.js +1 -0
- package/public/assets/Setup-D05lwDOV.js +1 -0
- package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
- package/public/assets/index-HSXCsceK.css +1 -0
- package/public/assets/index-bnBu0nlQ.js +19 -0
- package/public/assets/registry-C_qeFTkZ.js +2 -0
- package/public/assets/usePolling-Bn93fe7M.js +1 -0
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-flxcMVeP.js} +2 -2
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-ZC5T_huj.js} +7 -7
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +18 -4
- package/scripts/check-colima-launchd.mjs +230 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-rkWp-CXd.js +0 -1
- package/public/assets/HermesConfigForm-anDnwUp_.js +0 -4
- package/public/assets/InitPassword-ZU9_-hDr.js +0 -1
- package/public/assets/InstanceDetail-CN0FH1aw.js +0 -92
- package/public/assets/Login-BItXqYAJ.js +0 -1
- package/public/assets/NewInstance-BousE6kY.js +0 -1
- package/public/assets/ProviderRecommendations-DFYj7Fb6.js +0 -1
- package/public/assets/Settings-Bttc6QmM.js +0 -1
- package/public/assets/Setup-Bsxx1zgj.js +0 -1
- package/public/assets/WeixinLoginPanel-DPZpAKgO.js +0 -9
- package/public/assets/index-8xZy1z5k.css +0 -1
- package/public/assets/index-Dw3HhUYE.js +0 -19
- package/public/assets/input-paste-CrNVAyOy.js +0 -1
- package/public/assets/providers-DtNXh9JD.js +0 -1
- package/public/assets/registry-5s2UB6is.js +0 -2
- package/public/assets/usePolling-Do5Erqm_.js +0 -1
package/public/index.html
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
from the instance cards, making the panel look like Hermes. -->
|
|
10
10
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
11
11
|
<title>JishuShell</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-react-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-i18n-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-bnBu0nlQ.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-ZC5T_huj.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-i18n-flxcMVeP.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-HSXCsceK.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-white min-h-screen">
|
|
18
18
|
<div id="root"></div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* provide (severity gated by `required` — error for required:true, warning
|
|
22
22
|
* otherwise).
|
|
23
23
|
*/
|
|
24
|
-
import { readFileSync, readdirSync,
|
|
24
|
+
import { readFileSync, readdirSync, } from "fs";
|
|
25
25
|
import { join, dirname } from "path";
|
|
26
26
|
import { fileURLToPath } from "url";
|
|
27
27
|
import { parse as parseYaml } from "yaml";
|
|
@@ -31,10 +31,16 @@ const APPS_DIR = process.env.APPS_DIR_OVERRIDE ?? join(__dirname, "..", "apps");
|
|
|
31
31
|
|
|
32
32
|
// ── Allowlists (kept here so check-app-spec stays mechanically driven) ──
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Keep in sync with CAPABILITY_CATEGORIES in
|
|
35
|
+
// src/services/connection-resolver.ts. `files` (W1-W2) and `knowledge`
|
|
36
|
+
// (W2, AnythingLLM) are full categories with adapter hooks and slot UX.
|
|
37
|
+
const CAPABILITY_CATEGORIES = new Set([
|
|
38
|
+
"llm", "search", "browser", "mcp", "files", "knowledge",
|
|
39
|
+
]);
|
|
35
40
|
|
|
36
41
|
const LEGACY_BINDABLE_EXACT_ALLOWLIST = new Set([
|
|
37
42
|
"ollama-api",
|
|
43
|
+
"browserless-api",
|
|
38
44
|
]);
|
|
39
45
|
|
|
40
46
|
const UI_LEGACY_ALLOWLIST = new Set([
|
|
@@ -47,6 +53,8 @@ const UI_LEGACY_ALLOWLIST = new Set([
|
|
|
47
53
|
"web-searxng",
|
|
48
54
|
"web-openwebui",
|
|
49
55
|
"web-hermes",
|
|
56
|
+
"browserless-debugger",
|
|
57
|
+
"browserless-docs",
|
|
50
58
|
]);
|
|
51
59
|
|
|
52
60
|
const PROTOCOL_WHITELIST = {
|
|
@@ -54,6 +62,12 @@ const PROTOCOL_WHITELIST = {
|
|
|
54
62
|
search: new Set(["http", "https"]),
|
|
55
63
|
browser: new Set(["ws", "wss", "http", "https"]),
|
|
56
64
|
mcp: new Set(["http", "https", "ws", "sse"]),
|
|
65
|
+
// Filebrowser exposes an HTTP Web UI under /apps/filebrowser; webdav adds
|
|
66
|
+
// PROPFIND/PUT over the same HTTP origin. Both fit `http(s)`.
|
|
67
|
+
files: new Set(["http", "https"]),
|
|
68
|
+
// AnythingLLM exposes an OpenAI-compatible HTTP API for kb_search; baseUrl
|
|
69
|
+
// is always http(s) regardless of LAN vs container host topology.
|
|
70
|
+
knowledge: new Set(["http", "https"]),
|
|
57
71
|
default: new Set(["http", "https", "ws", "wss", "tcp"]),
|
|
58
72
|
};
|
|
59
73
|
|
|
@@ -230,7 +244,7 @@ function hasProviderForRequire(req) {
|
|
|
230
244
|
|
|
231
245
|
// Second pass: validate each spec.
|
|
232
246
|
for (const { file, spec } of specs) {
|
|
233
|
-
const
|
|
247
|
+
const _id = typeof spec.id === "string" ? spec.id : "<no id>";
|
|
234
248
|
|
|
235
249
|
// ── provides[*] ──────────────────────────────────────────────────────
|
|
236
250
|
for (const provide of spec.provides ?? []) {
|
|
@@ -319,7 +333,7 @@ for (const { file, spec } of specs) {
|
|
|
319
333
|
// Unrecognized — author must explicitly opt into one of A/B/C.
|
|
320
334
|
err(
|
|
321
335
|
file,
|
|
322
|
-
`provide '${cap}' is not recognized as A/B/C; rename with category prefix (
|
|
336
|
+
`provide '${cap}' is not recognized as A/B/C; rename with category prefix (${[...CAPABILITY_CATEGORIES].map((c) => c + "-").join("/")}), add to LEGACY_BINDABLE_EXACT_ALLOWLIST, or add to UI_LEGACY_ALLOWLIST`,
|
|
323
337
|
);
|
|
324
338
|
}
|
|
325
339
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Static validator for the macOS Colima/Nomad/Panel launchd artifacts
|
|
4
|
+
* produced by install/jishu-install.sh and the doctor self-heal checks.
|
|
5
|
+
* Mirrors the text-assertion style of scripts/check-app-spec.mjs — runs
|
|
6
|
+
* in CI without a Mac. Exits non-zero on any failed assertion.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT = join(__dirname, "..");
|
|
14
|
+
const installer = readFileSync(join(ROOT, "install/jishu-install.sh"), "utf8");
|
|
15
|
+
const doctor = readFileSync(join(ROOT, "src/cli/doctor.ts"), "utf8");
|
|
16
|
+
|
|
17
|
+
const failures = [];
|
|
18
|
+
const must = (cond, msg) => { if (!cond) failures.push(msg); };
|
|
19
|
+
|
|
20
|
+
// Task 2 (ARM-only) — macOS is Apple Silicon only: constant arch/vm-type,
|
|
21
|
+
// fail-fast on non-arm64 Darwin, no Intel/qemu derivation.
|
|
22
|
+
must(/COLIMA_ARCH="aarch64"/.test(installer),
|
|
23
|
+
'installer must set COLIMA_ARCH="aarch64" (Apple Silicon only)');
|
|
24
|
+
must(/COLIMA_VM_TYPE="vz"/.test(installer),
|
|
25
|
+
'installer must set COLIMA_VM_TYPE="vz" (Apple Silicon only)');
|
|
26
|
+
must(!/<string>--arch<\/string><string>aarch64<\/string>/.test(installer),
|
|
27
|
+
"colima plist must not hardcode --arch aarch64 (use ${COLIMA_ARCH})");
|
|
28
|
+
must(!/COLIMA_VM_TYPE="?qemu/.test(installer),
|
|
29
|
+
"no qemu COLIMA_VM_TYPE (macOS is Apple Silicon only)");
|
|
30
|
+
must(/Apple Silicon \(arm64\) only/.test(installer),
|
|
31
|
+
"installer must fail fast on non-arm64 macOS with an Apple-Silicon-only message");
|
|
32
|
+
|
|
33
|
+
// Task 3 — colima wait/start wrapper + hardened plist
|
|
34
|
+
must(/_install_colima_wait_start_wrapper\(\)/.test(installer),
|
|
35
|
+
"installer must define _install_colima_wait_start_wrapper()");
|
|
36
|
+
must(/colima-launchd-wrapper\.sh/.test(installer),
|
|
37
|
+
"installer must generate colima-launchd-wrapper.sh");
|
|
38
|
+
must(!/colima"?\s+delete/.test(installer.replace(/#.*$/gm, "")),
|
|
39
|
+
"colima wrapper must never run `colima delete` (destroys the openclaw image)");
|
|
40
|
+
must(/SuccessfulExit<\/key>\s*<false\/>/.test(installer),
|
|
41
|
+
"colima plist must use KeepAlive={SuccessfulExit:false}");
|
|
42
|
+
must(/<key>ThrottleInterval<\/key>\s*<integer>30<\/integer>/.test(installer),
|
|
43
|
+
"colima plist must set ThrottleInterval=30");
|
|
44
|
+
|
|
45
|
+
// Task 4 — panel 60s docker gate
|
|
46
|
+
must(/panel-launchd-wrapper\.sh/.test(installer),
|
|
47
|
+
"installer must generate panel-launchd-wrapper.sh (60s docker gate)");
|
|
48
|
+
must(/PANEL_DOCKER_WAIT_DEADLINE=60\b/.test(installer),
|
|
49
|
+
"panel gate deadline must be 60s");
|
|
50
|
+
|
|
51
|
+
// Task 5 — autologin opt-in + FileVault guard + flags
|
|
52
|
+
must(/_enable_autologin\(\)/.test(installer),
|
|
53
|
+
"installer must define _enable_autologin()");
|
|
54
|
+
must(/ENABLE_AUTOLOGIN="?\$\{ENABLE_AUTOLOGIN:-0\}"?/.test(installer),
|
|
55
|
+
"ENABLE_AUTOLOGIN must default to 0 (opt-in)");
|
|
56
|
+
must(/--enable-autologin/.test(installer) && /--no-autologin/.test(installer)
|
|
57
|
+
&& /--autologin-password/.test(installer),
|
|
58
|
+
"installer must accept --enable-autologin / --no-autologin / --autologin-password");
|
|
59
|
+
must(/fdesetup status/.test(installer),
|
|
60
|
+
"_enable_autologin must check FileVault via `fdesetup status`");
|
|
61
|
+
must(/0x7D 0x89 0x52 0x23 0xD2 0xBC 0xDD 0xEA 0xA3 0xB9 0x1F/.test(installer),
|
|
62
|
+
"kcpassword cipher key bytes must be present");
|
|
63
|
+
|
|
64
|
+
// Task 6 — doctor self-heal checks registered
|
|
65
|
+
for (const id of ["colima-launchd", "colima-wrapper", "macos-autologin"]) {
|
|
66
|
+
must(new RegExp(`id:\\s*"${id}"`).test(doctor),
|
|
67
|
+
`doctor must define a check with id "${id}"`);
|
|
68
|
+
}
|
|
69
|
+
must(/checkColimaLaunchd[,\s]/.test(doctor) && /ALL_CHECKS/.test(doctor),
|
|
70
|
+
"doctor must register the new checks in ALL_CHECKS");
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Follow-up #14 — TypeScript install path parity. `jishushell install`
|
|
74
|
+
// and the Setup wizard go through src/services/setup-manager.ts, which
|
|
75
|
+
// must produce the SAME hardened launchd artifacts as the bash installer
|
|
76
|
+
// (otherwise any reinstall/update reverts the headless-reboot fix).
|
|
77
|
+
// These assertions read the canonical TS builders in macos-launchd.ts.
|
|
78
|
+
const tsLaunchd = readFileSync(join(ROOT, "src/services/macos-launchd.ts"), "utf8");
|
|
79
|
+
const setupMgr = readFileSync(join(ROOT, "src/services/setup-manager.ts"), "utf8");
|
|
80
|
+
const tsLaunchdNoComments = tsLaunchd.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
81
|
+
|
|
82
|
+
// setup-manager must import + use the canonical builders.
|
|
83
|
+
must(/from\s+"\.\/macos-launchd\.js"/.test(setupMgr),
|
|
84
|
+
"setup-manager.ts must import the canonical builders from ./macos-launchd.js");
|
|
85
|
+
must(/buildColimaWrapperScript\(/.test(setupMgr) && /buildColimaPlist\(/.test(setupMgr)
|
|
86
|
+
&& /buildNomadWaitWrapper\(/.test(setupMgr) && /buildNomadPlist\(/.test(setupMgr)
|
|
87
|
+
&& /buildPanelGateWrapper\(/.test(setupMgr) && /buildPanelPlist\(/.test(setupMgr),
|
|
88
|
+
"setup-manager.ts must call all six macos-launchd builders");
|
|
89
|
+
must(/appleSiliconGuard\(/.test(setupMgr),
|
|
90
|
+
"setup-manager.ts must gate the macOS branch with appleSiliconGuard()");
|
|
91
|
+
|
|
92
|
+
// Colima wrapper: atomic mkdir single-flight lock, no flock, never delete.
|
|
93
|
+
must(/mkdir\s+-p\s+"\\\$COLIMA_HOME"/.test(tsLaunchd),
|
|
94
|
+
'TS colima wrapper must mkdir -p "$COLIMA_HOME"');
|
|
95
|
+
must(/mkdir\s+"\\\$LOCKDIR"/.test(tsLaunchd),
|
|
96
|
+
'TS colima wrapper must use an atomic `mkdir "$LOCKDIR"` single-flight lock');
|
|
97
|
+
must(/trap '.*rmdir "\\\$LOCKDIR".*' EXIT/.test(tsLaunchd),
|
|
98
|
+
"TS colima wrapper must rmdir the lockdir on EXIT");
|
|
99
|
+
must(!/\bflock\b/.test(tsLaunchd),
|
|
100
|
+
"TS launchd builders must not use flock (macOS lacks it; mkdir-lock only)");
|
|
101
|
+
must(!/colima"?\s+delete/.test(tsLaunchdNoComments) && !/"delete"/.test(tsLaunchdNoComments),
|
|
102
|
+
"TS colima wrapper must never run `colima delete` (destroys the openclaw image)");
|
|
103
|
+
must(/--vm-type \$\{COLIMA_VM_TYPE\}/.test(tsLaunchd) && /--arch \$\{COLIMA_ARCH\}/.test(tsLaunchd),
|
|
104
|
+
"TS colima wrapper must use the COLIMA_VM_TYPE / COLIMA_ARCH consts for start flags");
|
|
105
|
+
|
|
106
|
+
// Apple-Silicon-only constants — no qemu / x86_64 derivation.
|
|
107
|
+
must(/COLIMA_ARCH\s*=\s*"aarch64"/.test(tsLaunchd),
|
|
108
|
+
'macos-launchd.ts must export COLIMA_ARCH = "aarch64"');
|
|
109
|
+
must(/COLIMA_VM_TYPE\s*=\s*"vz"/.test(tsLaunchd),
|
|
110
|
+
'macos-launchd.ts must export COLIMA_VM_TYPE = "vz"');
|
|
111
|
+
must(!/qemu/.test(tsLaunchdNoComments) && !/x86_64/.test(tsLaunchdNoComments),
|
|
112
|
+
"macos-launchd.ts must not reference qemu / x86_64 (Apple Silicon only)");
|
|
113
|
+
must(/Apple Silicon \(arm64\) only/.test(tsLaunchd) && /appleSiliconGuard/.test(tsLaunchd),
|
|
114
|
+
"macos-launchd.ts must define appleSiliconGuard with an Apple-Silicon-only message");
|
|
115
|
+
|
|
116
|
+
// Colima plist: KeepAlive={SuccessfulExit:false} + ThrottleInterval 30.
|
|
117
|
+
must(/<key>SuccessfulExit<\/key>\s*<false\/>/.test(tsLaunchd),
|
|
118
|
+
"TS colima plist must use KeepAlive={SuccessfulExit:false}");
|
|
119
|
+
must(/<key>ThrottleInterval<\/key>\s*<integer>30<\/integer>/.test(tsLaunchd),
|
|
120
|
+
"TS colima plist must set ThrottleInterval=30");
|
|
121
|
+
|
|
122
|
+
// Panel 60s docker gate; nomad waits for docker before exec.
|
|
123
|
+
must(/PANEL_DOCKER_WAIT_DEADLINE=60\b/.test(tsLaunchd),
|
|
124
|
+
"TS panel gate deadline must be 60s");
|
|
125
|
+
must(/docker info/.test(tsLaunchd) && /exec "\$\{nomadBin\}" agent/.test(tsLaunchd),
|
|
126
|
+
"TS nomad wait wrapper must poll docker then exec nomad");
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
129
|
+
// Follow-up #14 nit — bash↔TS budget cross-link. The retry/timeout budgets
|
|
130
|
+
// (60/180/throttle-30/retry-5/sleep-10) are duplicated between the bash
|
|
131
|
+
// installer and the TS builders. The assertions above only check each side
|
|
132
|
+
// in isolation, so a lockstep edit (60→90 in both) or a bash-only edit
|
|
133
|
+
// would pass vacuously and silently drift the two install paths apart.
|
|
134
|
+
// Extract the SAME budget literal from BOTH sources and assert equality so
|
|
135
|
+
// any divergence fails loudly with both values named.
|
|
136
|
+
const extract = (src, re, label, side) => {
|
|
137
|
+
const m = re.exec(src);
|
|
138
|
+
if (!m) { failures.push(`${side} source missing budget literal: ${label}`); return null; }
|
|
139
|
+
return m[1];
|
|
140
|
+
};
|
|
141
|
+
const crossLink = (label, bashRe, tsRe) => {
|
|
142
|
+
const b = extract(installer, bashRe, label, "bash");
|
|
143
|
+
const t = extract(tsLaunchd, tsRe, label, "TS");
|
|
144
|
+
if (b !== null && t !== null) {
|
|
145
|
+
must(b === t,
|
|
146
|
+
`${label} budget drift: bash=${b} != TS=${t} ` +
|
|
147
|
+
`(install/jishu-install.sh vs src/services/macos-launchd.ts must stay lockstep)`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// panel docker-wait deadline (PANEL_DOCKER_WAIT_DEADLINE=60).
|
|
152
|
+
crossLink("PANEL_DOCKER_WAIT_DEADLINE",
|
|
153
|
+
/PANEL_DOCKER_WAIT_DEADLINE=(\d+)\b/,
|
|
154
|
+
/PANEL_DOCKER_WAIT_DEADLINE=(\d+)\b/);
|
|
155
|
+
|
|
156
|
+
// nomad docker-wait budget (deadline = now + 180, echoed "after 180s").
|
|
157
|
+
crossLink("nomad docker-wait seconds",
|
|
158
|
+
/deadline=\\?\$\(\(\s*\\?\$\(date \+%s\)\s*\+\s*(\d+)\s*\)\)[\s\S]{0,160}?\[nomad-launchd-wrapper\]/,
|
|
159
|
+
/deadline=\\?\$\(\(\s*\\?\$\(date \+%s\)\s*\+\s*(\d+)\s*\)\)[\s\S]{0,160}?\[nomad-launchd-wrapper\]/);
|
|
160
|
+
crossLink("nomad timeout message seconds",
|
|
161
|
+
/\[nomad-launchd-wrapper\][\s\S]{0,80}?after (\d+)s/,
|
|
162
|
+
/\[nomad-launchd-wrapper\][\s\S]{0,80}?after (\d+)s/);
|
|
163
|
+
|
|
164
|
+
// colima network-wait deadline (net_deadline = now + 60).
|
|
165
|
+
crossLink("colima net-wait seconds",
|
|
166
|
+
/net_deadline=\\?\$\(\(\s*\\?\$\(date \+%s\)\s*\+\s*(\d+)\s*\)\)/,
|
|
167
|
+
/net_deadline=\\?\$\(\(\s*\\?\$\(date \+%s\)\s*\+\s*(\d+)\s*\)\)/);
|
|
168
|
+
|
|
169
|
+
// colima plist ThrottleInterval.
|
|
170
|
+
crossLink("colima ThrottleInterval",
|
|
171
|
+
/<key>ThrottleInterval<\/key>\s*<integer>(\d+)<\/integer>/,
|
|
172
|
+
/<key>ThrottleInterval<\/key>\s*<integer>(\d+)<\/integer>/);
|
|
173
|
+
|
|
174
|
+
// colima retry attempt bound ([ $attempt -lt 5 ]).
|
|
175
|
+
crossLink("colima retry attempts",
|
|
176
|
+
/\[\s*\\?\$attempt\s+-lt\s+(\d+)\s*\]/,
|
|
177
|
+
/\[\s*\\?\$attempt\s+-lt\s+(\d+)\s*\]/);
|
|
178
|
+
|
|
179
|
+
// colima per-attempt backoff multiplier (sleep $(( attempt * 10 ))).
|
|
180
|
+
crossLink("colima backoff multiplier",
|
|
181
|
+
/sleep\s+\\?\$\(\(\s*attempt\s*\*\s*(\d+)\s*\)\)/,
|
|
182
|
+
/sleep\s+\\?\$\(\(\s*attempt\s*\*\s*(\d+)\s*\)\)/);
|
|
183
|
+
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
185
|
+
// M1 — stale mkdir-lock reclamation. If the wrapper is SIGKILLed/power-cut
|
|
186
|
+
// before the `trap rmdir EXIT` is installed, $LOCKDIR is orphaned and every
|
|
187
|
+
// subsequent launchd fire dead-ends at "another start in progress" → colima
|
|
188
|
+
// never recovers. The wrapper reclaims a $LOCKDIR older than the staleness
|
|
189
|
+
// budget (rmdir only removes it while still empty — safe). Assert the line is
|
|
190
|
+
// present on BOTH install paths and the budget literal stays lockstep.
|
|
191
|
+
const staleReclaimRe =
|
|
192
|
+
/\[\s*-d\s+"\\?\$LOCKDIR"\s*\]\s*&&\s*find\s+"\\?\$LOCKDIR"\s+-maxdepth\s+0\s+-type\s+d\s+-mmin\s+\+\d+\s+-exec\s+rmdir\s+\{\}\s+\\\\;\s+2>\/dev\/null\s+\|\|\s+true/;
|
|
193
|
+
must(staleReclaimRe.test(installer),
|
|
194
|
+
"bash colima-wrapper heredoc (install/jishu-install.sh) must reclaim a stale mkdir-lock " +
|
|
195
|
+
'before acquiring it ([ -d "$LOCKDIR" ] && find ... -mmin +N -exec rmdir {} \\;)');
|
|
196
|
+
must(staleReclaimRe.test(tsLaunchd),
|
|
197
|
+
"TS buildColimaWrapperScript (src/services/macos-launchd.ts) must reclaim a stale " +
|
|
198
|
+
'mkdir-lock before acquiring it ([ -d "$LOCKDIR" ] && find ... -mmin +N -exec rmdir {} \\;)');
|
|
199
|
+
|
|
200
|
+
// staleness budget literal (-mmin +10) must match bash↔TS.
|
|
201
|
+
crossLink("colima stale-lock mmin budget",
|
|
202
|
+
/find\s+"\\?\$LOCKDIR"\s+-maxdepth\s+0\s+-type\s+d\s+-mmin\s+\+(\d+)\s+-exec\s+rmdir/,
|
|
203
|
+
/find\s+"\\?\$LOCKDIR"\s+-maxdepth\s+0\s+-type\s+d\s+-mmin\s+\+(\d+)\s+-exec\s+rmdir/);
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
206
|
+
// M1+ — per-retry lock-mtime refresh. The mkdir-lock's mtime is stamped
|
|
207
|
+
// ONCE at acquire and never refreshed, so a single legit `colima start`
|
|
208
|
+
// invocation whose total wall-time exceeds the staleness budget (cold VM
|
|
209
|
+
// init + ≤5 attempts + cumulative backoff) is seen as `-mmin +N` stale by
|
|
210
|
+
// the next launchd fire, which then reclaims the still-held lock and starts
|
|
211
|
+
// colima concurrently. The wrapper re-stamps $LOCKDIR at the top of every
|
|
212
|
+
// retry attempt so an actively-progressing start is never reclaimed. Assert
|
|
213
|
+
// the line is present on BOTH install paths (no numeric budget to crossLink
|
|
214
|
+
// — presence-on-both-sides is the guard).
|
|
215
|
+
const lockRefreshRe = /touch\s+"\\?\$LOCKDIR"\s+2>\/dev\/null\s+\|\|\s+true/;
|
|
216
|
+
must(lockRefreshRe.test(installer),
|
|
217
|
+
"bash colima-wrapper heredoc (install/jishu-install.sh) must refresh its own " +
|
|
218
|
+
'lock mtime each retry attempt (touch "$LOCKDIR" 2>/dev/null || true) so a ' +
|
|
219
|
+
"long in-progress start is never reclaimed as stale");
|
|
220
|
+
must(lockRefreshRe.test(tsLaunchd),
|
|
221
|
+
"TS buildColimaWrapperScript (src/services/macos-launchd.ts) must refresh its own " +
|
|
222
|
+
'lock mtime each retry attempt (touch "$LOCKDIR" 2>/dev/null || true) so a ' +
|
|
223
|
+
"long in-progress start is never reclaimed as stale");
|
|
224
|
+
|
|
225
|
+
if (failures.length) {
|
|
226
|
+
console.error("check-colima-launchd FAILED:");
|
|
227
|
+
for (const f of failures) console.error(" ✗ " + f);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
console.log("check-colima-launchd OK");
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CI check: warn when newly added source files have no corresponding test.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/check-new-file-tests.mjs [--base <ref>]
|
|
7
|
+
*
|
|
8
|
+
* Defaults:
|
|
9
|
+
* --base: CI_MERGE_REQUEST_TARGET_BRANCH_NAME (MR) or origin/main (fallback)
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — all new files have tests (or no new files)
|
|
13
|
+
* 1 — some new files are missing tests (warning)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { basename, dirname, join } from "node:path";
|
|
19
|
+
|
|
20
|
+
// ── Configuration ──────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** File basenames exempt from the test requirement. */
|
|
23
|
+
const EXEMPT_BASENAMES = new Set([
|
|
24
|
+
"index.ts",
|
|
25
|
+
"index.tsx",
|
|
26
|
+
"types.ts",
|
|
27
|
+
"types.tsx",
|
|
28
|
+
"constants.ts",
|
|
29
|
+
"constants.tsx",
|
|
30
|
+
"main.tsx",
|
|
31
|
+
"App.tsx",
|
|
32
|
+
"vite-env.d.ts",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** Glob-style suffix patterns to exempt. */
|
|
36
|
+
const EXEMPT_SUFFIXES = [
|
|
37
|
+
".d.ts",
|
|
38
|
+
".test.ts",
|
|
39
|
+
".test.tsx",
|
|
40
|
+
".spec.ts",
|
|
41
|
+
".spec.tsx",
|
|
42
|
+
".visual.spec.ts",
|
|
43
|
+
".browser.test.ts",
|
|
44
|
+
".browser.test.tsx",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Path prefixes to exempt entirely. */
|
|
48
|
+
const EXEMPT_PATH_PREFIXES = [
|
|
49
|
+
"src/test-setup",
|
|
50
|
+
"frontend/src/test-setup",
|
|
51
|
+
"frontend/src/visual-tests/",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Directories where backend tests live (searched for basename match). */
|
|
55
|
+
const BACKEND_TEST_DIRS = ["tests/unit", "tests/integration"];
|
|
56
|
+
|
|
57
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function git(cmd) {
|
|
60
|
+
return execSync(`git ${cmd}`, { encoding: "utf-8" }).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveBase() {
|
|
64
|
+
// CLI --base override
|
|
65
|
+
const baseIdx = process.argv.indexOf("--base");
|
|
66
|
+
if (baseIdx !== -1 && process.argv[baseIdx + 1]) {
|
|
67
|
+
return process.argv[baseIdx + 1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// GitLab MR pipeline
|
|
71
|
+
const mrTarget = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
72
|
+
if (mrTarget) {
|
|
73
|
+
try {
|
|
74
|
+
git(`fetch origin ${mrTarget} --depth=1 2>/dev/null`);
|
|
75
|
+
} catch {
|
|
76
|
+
// already fetched or shallow — proceed
|
|
77
|
+
}
|
|
78
|
+
return `origin/${mrTarget}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// GitLab push pipeline
|
|
82
|
+
const beforeSha = process.env.CI_COMMIT_BEFORE_SHA;
|
|
83
|
+
if (beforeSha && beforeSha !== "0000000000000000000000000000000000000000") {
|
|
84
|
+
return beforeSha;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Local fallback
|
|
88
|
+
return "origin/main";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isExempt(filePath) {
|
|
92
|
+
const base = basename(filePath);
|
|
93
|
+
|
|
94
|
+
if (EXEMPT_BASENAMES.has(base)) return true;
|
|
95
|
+
if (EXEMPT_SUFFIXES.some((s) => filePath.endsWith(s))) return true;
|
|
96
|
+
if (EXEMPT_PATH_PREFIXES.some((p) => filePath.startsWith(p))) return true;
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Search for a test file matching the given source file basename.
|
|
103
|
+
* Accepts exact basename match or partial token match
|
|
104
|
+
* (e.g., `app.ts` matches `app-cli.test.ts`).
|
|
105
|
+
*/
|
|
106
|
+
function findBackendTest(sourceFile) {
|
|
107
|
+
const base = basename(sourceFile, ".ts");
|
|
108
|
+
|
|
109
|
+
for (const dir of BACKEND_TEST_DIRS) {
|
|
110
|
+
try {
|
|
111
|
+
const files = execSync(
|
|
112
|
+
`find ${dir} -name "*.test.ts" -type f 2>/dev/null`,
|
|
113
|
+
{ encoding: "utf-8" },
|
|
114
|
+
).trim();
|
|
115
|
+
|
|
116
|
+
if (!files) continue;
|
|
117
|
+
|
|
118
|
+
for (const testFile of files.split("\n")) {
|
|
119
|
+
const testBase = basename(testFile, ".test.ts");
|
|
120
|
+
// Exact match or token-contains match
|
|
121
|
+
if (testBase === base || testBase.includes(base) || base.includes(testBase)) {
|
|
122
|
+
return testFile;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a co-located frontend test file exists.
|
|
135
|
+
*/
|
|
136
|
+
function findFrontendTest(sourceFile) {
|
|
137
|
+
const dir = dirname(sourceFile);
|
|
138
|
+
const base = basename(sourceFile).replace(/\.(tsx?)$/, "");
|
|
139
|
+
|
|
140
|
+
const candidates = [
|
|
141
|
+
join(dir, `${base}.test.ts`),
|
|
142
|
+
join(dir, `${base}.test.tsx`),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (const candidate of candidates) {
|
|
146
|
+
if (existsSync(candidate)) return candidate;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const base = resolveBase();
|
|
155
|
+
let mergeBase;
|
|
156
|
+
try {
|
|
157
|
+
mergeBase = git(`merge-base HEAD ${base}`);
|
|
158
|
+
} catch {
|
|
159
|
+
// merge-base may fail on shallow clones — fall back to direct base
|
|
160
|
+
mergeBase = base;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let newFiles;
|
|
164
|
+
try {
|
|
165
|
+
const output = git(`diff --name-only --diff-filter=A ${mergeBase}...HEAD`);
|
|
166
|
+
newFiles = output ? output.split("\n") : [];
|
|
167
|
+
} catch {
|
|
168
|
+
console.log("⚠ Could not compute diff — skipping new-file test check.");
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Filter to source files only
|
|
173
|
+
const backendSources = newFiles.filter(
|
|
174
|
+
(f) => f.startsWith("src/") && f.endsWith(".ts") && !isExempt(f),
|
|
175
|
+
);
|
|
176
|
+
const frontendSources = newFiles.filter(
|
|
177
|
+
(f) =>
|
|
178
|
+
f.startsWith("frontend/src/") &&
|
|
179
|
+
(f.endsWith(".ts") || f.endsWith(".tsx")) &&
|
|
180
|
+
!isExempt(f),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const uncovered = [];
|
|
184
|
+
|
|
185
|
+
for (const file of backendSources) {
|
|
186
|
+
const testFile = findBackendTest(file);
|
|
187
|
+
if (!testFile) {
|
|
188
|
+
uncovered.push({ source: file, kind: "backend" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const file of frontendSources) {
|
|
193
|
+
const testFile = findFrontendTest(file);
|
|
194
|
+
if (!testFile) {
|
|
195
|
+
uncovered.push({ source: file, kind: "frontend" });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Report ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
const totalChecked = backendSources.length + frontendSources.length;
|
|
202
|
+
|
|
203
|
+
if (totalChecked === 0) {
|
|
204
|
+
console.log("✔ No new source files to check.");
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`Checked ${totalChecked} new source file(s) against ${mergeBase.slice(0, 8)}...\n`);
|
|
209
|
+
|
|
210
|
+
if (uncovered.length === 0) {
|
|
211
|
+
console.log("✔ All new source files have matching test files.");
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log("⚠ The following new source files have no obvious matching test:\n");
|
|
216
|
+
for (const { source, kind } of uncovered) {
|
|
217
|
+
const hint =
|
|
218
|
+
kind === "backend"
|
|
219
|
+
? "→ Expected: tests/unit/**/<name>.test.ts or tests/integration/**/<name>.test.ts"
|
|
220
|
+
: "→ Expected: co-located <name>.test.{ts,tsx}";
|
|
221
|
+
console.log(` ${source}`);
|
|
222
|
+
console.log(` ${hint}\n`);
|
|
223
|
+
}
|
|
224
|
+
console.log(
|
|
225
|
+
"If these files are covered by existing integration/e2e tests, " +
|
|
226
|
+
"note the covering test in the MR description.\n" +
|
|
227
|
+
"See docs/testing.md for testing conventions.\n",
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
process.exit(1);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Check quarantine expiry — fails CI if any quarantined test has expired.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node scripts/check-quarantine-expiry.mjs
|
|
6
|
+
* Exit codes:
|
|
7
|
+
* 0 — all quarantined tests are within their expiry window (or none exist)
|
|
8
|
+
* 1 — one or more quarantined tests have expired
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { resolve, dirname } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const quarantinePath = resolve(__dirname, "../tests/quarantine.json");
|
|
17
|
+
|
|
18
|
+
const MAX_QUARANTINE_DAYS = 14;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(quarantinePath, "utf-8");
|
|
22
|
+
const data = JSON.parse(raw);
|
|
23
|
+
const tests = data.tests || [];
|
|
24
|
+
|
|
25
|
+
if (tests.length === 0) {
|
|
26
|
+
console.log("✅ No quarantined tests.");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const today = new Date();
|
|
31
|
+
today.setHours(0, 0, 0, 0);
|
|
32
|
+
|
|
33
|
+
const expired = [];
|
|
34
|
+
const active = [];
|
|
35
|
+
const tooLong = [];
|
|
36
|
+
|
|
37
|
+
for (const entry of tests) {
|
|
38
|
+
const expiryDate = new Date(entry.expiry);
|
|
39
|
+
expiryDate.setHours(0, 0, 0, 0);
|
|
40
|
+
const createdDate = new Date(entry.created);
|
|
41
|
+
createdDate.setHours(0, 0, 0, 0);
|
|
42
|
+
|
|
43
|
+
const durationDays = Math.ceil(
|
|
44
|
+
(expiryDate.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (durationDays > MAX_QUARANTINE_DAYS) {
|
|
48
|
+
tooLong.push({
|
|
49
|
+
...entry,
|
|
50
|
+
durationDays,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (expiryDate < today) {
|
|
55
|
+
expired.push(entry);
|
|
56
|
+
} else {
|
|
57
|
+
active.push(entry);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (active.length > 0) {
|
|
62
|
+
console.log(`⏳ ${active.length} quarantined test(s) (active):`);
|
|
63
|
+
for (const t of active) {
|
|
64
|
+
console.log(` - ${t.file} > "${t.testName}" (expires: ${t.expiry}, owner: ${t.owner})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tooLong.length > 0) {
|
|
69
|
+
console.error(`\n❌ ${tooLong.length} quarantine(s) exceed ${MAX_QUARANTINE_DAYS}-day limit:`);
|
|
70
|
+
for (const t of tooLong) {
|
|
71
|
+
console.error(
|
|
72
|
+
` - ${t.file} > "${t.testName}" (${t.durationDays} days, max ${MAX_QUARANTINE_DAYS})`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
console.error("\nShorten expiry to at most 14 days from creation date.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (expired.length > 0) {
|
|
79
|
+
console.error(`\n❌ ${expired.length} quarantined test(s) have EXPIRED:`);
|
|
80
|
+
for (const t of expired) {
|
|
81
|
+
console.error(
|
|
82
|
+
` - ${t.file} > "${t.testName}" (expired: ${t.expiry}, owner: ${t.owner})`,
|
|
83
|
+
);
|
|
84
|
+
console.error(` Reason: ${t.reason}`);
|
|
85
|
+
if (t.issueLink) {
|
|
86
|
+
console.error(` Issue: ${t.issueLink}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
console.error("\nFix or remove expired quarantine entries before merging.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tooLong.length > 0 || expired.length > 0) {
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log("\n✅ All quarantine entries are within their expiry window.");
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.code === "ENOENT") {
|
|
100
|
+
console.log("✅ No quarantine file found (tests/quarantine.json).");
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
console.error("Failed to check quarantine:", err.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|