jishushell 0.4.30 → 0.5.15
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/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +18 -6
- package/apps/filebrowser-container.yaml +163 -0
- 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/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 +3 -3
- 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 +45 -7
- 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/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 +341 -3
- package/dist/server.js.map +1 -1
- 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 +1 -0
- package/dist/services/app/app-manager.js +172 -41
- 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 +20 -3
- 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 +177 -4
- package/dist/services/backup-manager.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 +1 -23
- package/dist/services/instance-manager.js.map +1 -1
- 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/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +53 -19
- 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 +20 -1
- 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/hermes.js +1 -1
- 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 +90 -0
- package/dist/services/runtime/adapters/openclaw.js +957 -45
- 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 +93 -18
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.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 +60 -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/package.json +16 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-_GHoklgo.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/{HermesConfigForm-anDnwUp_.js → HermesConfigForm-DVlhg3WV.js} +2 -2
- package/public/assets/{InitPassword-ZU9_-hDr.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BItXqYAJ.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/{ProviderRecommendations-DFYj7Fb6.js → ProviderRecommendations-BuRnvRcI.js} +1 -1
- package/public/assets/{Settings-Bttc6QmM.js → Settings-Cc-tYBil.js} +1 -1
- package/public/assets/{Setup-Bsxx1zgj.js → Setup-lGZEk5jq.js} +1 -1
- package/public/assets/{WeixinLoginPanel-DPZpAKgO.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/{registry-5s2UB6is.js → registry-BWnkJgZ1.js} +2 -2
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +18 -4
- 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/InstanceDetail-CN0FH1aw.js +0 -92
- package/public/assets/NewInstance-BousE6kY.js +0 -1
- package/public/assets/index-8xZy1z5k.css +0 -1
- package/public/assets/index-Dw3HhUYE.js +0 -19
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-BZc5zH7u.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BWrEVJVb.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-i18n-y9V7Sfuu.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-87IJXG-w.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
|
+
* 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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Performance Baseline Tests (k6)
|
|
2
|
+
|
|
3
|
+
Latency baseline scripts for core API endpoints. Run nightly in CI to detect
|
|
4
|
+
performance regressions.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/) installed on the runner
|
|
9
|
+
- JishuShell server running (the CI job starts one automatically)
|
|
10
|
+
|
|
11
|
+
## Scripts
|
|
12
|
+
|
|
13
|
+
| Script | Endpoints covered |
|
|
14
|
+
|--------|-------------------|
|
|
15
|
+
| `auth.js` | `/api/auth/status`, `/api/auth/login`, `/api/auth/logout` |
|
|
16
|
+
| `proxy.js` | `/proxy/v1/models`, `/api/llm/providers` |
|
|
17
|
+
| `instances.js` | `/api/instances` CRUD lifecycle |
|
|
18
|
+
|
|
19
|
+
## Environment Variables
|
|
20
|
+
|
|
21
|
+
| Variable | Default | Description |
|
|
22
|
+
|----------|---------|-------------|
|
|
23
|
+
| `BASE_URL` | `http://127.0.0.1:8090` | Server base URL |
|
|
24
|
+
| `PASSWORD` | `perf-test-password-2026` | Admin password |
|
|
25
|
+
| `SMOKE` | `false` | Set to `true` for quick 10s validation |
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Smoke test (quick validation)
|
|
31
|
+
k6 run -e SMOKE=true scripts/perf/auth.js
|
|
32
|
+
|
|
33
|
+
# Full baseline run
|
|
34
|
+
k6 run scripts/perf/auth.js
|
|
35
|
+
|
|
36
|
+
# All scripts with custom base URL
|
|
37
|
+
for script in scripts/perf/auth.js scripts/perf/proxy.js scripts/perf/instances.js; do
|
|
38
|
+
k6 run -e BASE_URL=http://localhost:9090 "$script"
|
|
39
|
+
done
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Thresholds
|
|
43
|
+
|
|
44
|
+
Default thresholds (see `config.js`):
|
|
45
|
+
- **p(95) < 500ms** for general endpoints
|
|
46
|
+
- **p(99) < 1000ms** tail latency
|
|
47
|
+
- **Error rate < 1%**
|
|
48
|
+
|
|
49
|
+
Per-endpoint overrides are defined in each script's `options.thresholds`.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import http from "k6/http";
|
|
2
|
+
import { check, group, sleep } from "k6";
|
|
3
|
+
import {
|
|
4
|
+
BASE_URL,
|
|
5
|
+
PASSWORD,
|
|
6
|
+
DEFAULT_THRESHOLDS,
|
|
7
|
+
DEFAULT_STAGES,
|
|
8
|
+
SMOKE_STAGES,
|
|
9
|
+
authHeaders,
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
|
|
12
|
+
const isSmoke = __ENV.SMOKE === "true";
|
|
13
|
+
|
|
14
|
+
export const options = {
|
|
15
|
+
stages: isSmoke ? SMOKE_STAGES : DEFAULT_STAGES,
|
|
16
|
+
thresholds: {
|
|
17
|
+
...DEFAULT_THRESHOLDS,
|
|
18
|
+
"http_req_duration{name:login}": ["p(95)<300"],
|
|
19
|
+
"http_req_duration{name:auth_status}": ["p(95)<100"],
|
|
20
|
+
},
|
|
21
|
+
tags: { suite: "auth" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Setup: initialize password and obtain a baseline token.
|
|
26
|
+
* Runs once before VUs start.
|
|
27
|
+
*/
|
|
28
|
+
export function setup() {
|
|
29
|
+
// Check if already initialized
|
|
30
|
+
const statusRes = http.get(`${BASE_URL}/api/auth/status`);
|
|
31
|
+
const initialized = statusRes.json("initialized");
|
|
32
|
+
|
|
33
|
+
let token;
|
|
34
|
+
if (!initialized) {
|
|
35
|
+
const initRes = http.post(
|
|
36
|
+
`${BASE_URL}/api/auth/init`,
|
|
37
|
+
JSON.stringify({ password: PASSWORD }),
|
|
38
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
39
|
+
);
|
|
40
|
+
check(initRes, { "init 200": (r) => r.status === 200 });
|
|
41
|
+
token = initRes.json("token");
|
|
42
|
+
} else {
|
|
43
|
+
const loginRes = http.post(
|
|
44
|
+
`${BASE_URL}/api/auth/login`,
|
|
45
|
+
JSON.stringify({ password: PASSWORD }),
|
|
46
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
47
|
+
);
|
|
48
|
+
check(loginRes, { "login 200": (r) => r.status === 200 });
|
|
49
|
+
token = loginRes.json("token");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!token) {
|
|
53
|
+
throw new Error("Failed to obtain auth token during setup");
|
|
54
|
+
}
|
|
55
|
+
return { token };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default function (data) {
|
|
59
|
+
group("auth-status", () => {
|
|
60
|
+
const res = http.get(`${BASE_URL}/api/auth/status`, {
|
|
61
|
+
tags: { name: "auth_status" },
|
|
62
|
+
});
|
|
63
|
+
check(res, {
|
|
64
|
+
"status 200": (r) => r.status === 200,
|
|
65
|
+
"has initialized field": (r) => r.json("initialized") !== undefined,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
sleep(0.5);
|
|
70
|
+
|
|
71
|
+
group("login", () => {
|
|
72
|
+
const res = http.post(
|
|
73
|
+
`${BASE_URL}/api/auth/login`,
|
|
74
|
+
JSON.stringify({ password: PASSWORD }),
|
|
75
|
+
{
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
tags: { name: "login" },
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
check(res, {
|
|
81
|
+
"login 200": (r) => r.status === 200,
|
|
82
|
+
"returns token": (r) => r.json("token") !== undefined,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
sleep(0.5);
|
|
87
|
+
|
|
88
|
+
group("logout", () => {
|
|
89
|
+
const res = http.post(`${BASE_URL}/api/auth/logout`, null, {
|
|
90
|
+
headers: authHeaders(data.token),
|
|
91
|
+
tags: { name: "logout" },
|
|
92
|
+
});
|
|
93
|
+
check(res, {
|
|
94
|
+
"logout 200": (r) => r.status === 200,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
sleep(1);
|
|
99
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration and helpers for k6 performance tests.
|
|
3
|
+
*
|
|
4
|
+
* Environment variables:
|
|
5
|
+
* BASE_URL — server base URL (default: http://127.0.0.1:8090)
|
|
6
|
+
* PASSWORD — admin password for auth (default: perf-test-password-2026)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const BASE_URL = __ENV.BASE_URL || "http://127.0.0.1:8090";
|
|
10
|
+
export const PASSWORD = __ENV.PASSWORD || "perf-test-password-2026";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Standard k6 thresholds for API endpoints.
|
|
14
|
+
* - p(95) < 500ms: acceptable for most CRUD ops
|
|
15
|
+
* - p(99) < 1000ms: tail latency budget
|
|
16
|
+
* - error rate < 1%
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_THRESHOLDS = {
|
|
19
|
+
http_req_duration: ["p(95)<500", "p(99)<1000"],
|
|
20
|
+
http_req_failed: ["rate<0.01"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Standard load profile for baseline tests.
|
|
25
|
+
* Ramp up to 10 VUs over 30s, hold for 2m, ramp down.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_STAGES = [
|
|
28
|
+
{ duration: "30s", target: 10 },
|
|
29
|
+
{ duration: "2m", target: 10 },
|
|
30
|
+
{ duration: "10s", target: 0 },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Smoke profile for quick validation (CI gate).
|
|
35
|
+
* 1 VU for 10s — just verify the script works.
|
|
36
|
+
*/
|
|
37
|
+
export const SMOKE_STAGES = [
|
|
38
|
+
{ duration: "10s", target: 1 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build standard headers with auth cookie.
|
|
43
|
+
* @param {string} token - JWT token from login/init
|
|
44
|
+
* @returns {object} headers object for k6 http calls
|
|
45
|
+
*/
|
|
46
|
+
export function authHeaders(token) {
|
|
47
|
+
return {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
Cookie: `jishushell_session=${token}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build standard headers with Bearer token (for proxy routes).
|
|
55
|
+
* @param {string} token - JWT token
|
|
56
|
+
* @returns {object} headers object for k6 http calls
|
|
57
|
+
*/
|
|
58
|
+
export function bearerHeaders(token) {
|
|
59
|
+
return {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
};
|
|
63
|
+
}
|