@geminilight/mindos 0.6.71 → 0.6.73
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/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +27 -27
- package/_standalone/.next/build-manifest.json +3 -3
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/react-loadable-manifest.json +4 -4
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/channels/verify/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/connect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/embedding/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/test/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook/feishu/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/clip/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/lint/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/direct-tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/space-overview/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +1 -1
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +2 -2
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app-paths-manifest.json +27 -27
- package/_standalone/.next/server/chunks/{3311.js → 2449.js} +2 -2
- package/_standalone/.next/server/chunks/5299.js +1 -1
- package/_standalone/.next/server/chunks/6022.js +34 -34
- package/_standalone/.next/server/middleware-build-manifest.js +1 -1
- package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/{7143.879daa87569c5b02.js → 4094.09364c01df411380.js} +1 -1
- package/_standalone/.next/static/chunks/{5795.d9099a1afecd6047.js → 5331.c89084fd7f67887d.js} +2 -2
- package/_standalone/.next/static/chunks/app/{layout-a344709b8447be75.js → layout-fcbde5bee626d21a.js} +63 -63
- package/_standalone/.next/static/chunks/app/trash/page-e623ff0ab35de002.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/page-49c4eff6ffdb5168.js +12 -0
- package/_standalone/.next/static/chunks/{webpack-2f2787d3469d3df1.js → webpack-dc486b68118d1328.js} +1 -1
- package/_standalone/.next/trace +72 -72
- package/_standalone/package-lock.json +2 -2
- package/_standalone/package.json +1 -1
- package/app/package.json +1 -1
- package/package.json +1 -1
- package/_standalone/.next/static/chunks/app/trash/page-0907fdd06a4467de.js +0 -1
- package/_standalone/.next/static/chunks/app/view/[...path]/page-f53ce199b4a4bbb5.js +0 -12
- package/browser-extension/README.md +0 -160
- package/browser-extension/build.mjs +0 -63
- package/browser-extension/extension/background/service-worker.js +0 -1
- package/browser-extension/extension/content/extractor.js +0 -2
- package/browser-extension/extension/icons/icon-128.png +0 -0
- package/browser-extension/extension/icons/icon-128.svg +0 -4
- package/browser-extension/extension/icons/icon-16.png +0 -0
- package/browser-extension/extension/icons/icon-16.svg +0 -4
- package/browser-extension/extension/icons/icon-32.png +0 -0
- package/browser-extension/extension/icons/icon-32.svg +0 -4
- package/browser-extension/extension/icons/icon-48.png +0 -0
- package/browser-extension/extension/icons/icon-48.svg +0 -4
- package/browser-extension/extension/manifest.json +0 -47
- package/browser-extension/extension/popup/popup.css +0 -510
- package/browser-extension/extension/popup/popup.html +0 -128
- package/browser-extension/extension/popup/popup.js +0 -73
- package/browser-extension/package-lock.json +0 -617
- package/browser-extension/package.json +0 -21
- package/browser-extension/scripts/gen-icons.sh +0 -38
- package/browser-extension/src/background/service-worker.ts +0 -27
- package/browser-extension/src/content/extractor.ts +0 -44
- package/browser-extension/src/icons/icon-128.png +0 -0
- package/browser-extension/src/icons/icon-128.svg +0 -4
- package/browser-extension/src/icons/icon-16.png +0 -0
- package/browser-extension/src/icons/icon-16.svg +0 -4
- package/browser-extension/src/icons/icon-32.png +0 -0
- package/browser-extension/src/icons/icon-32.svg +0 -4
- package/browser-extension/src/icons/icon-48.png +0 -0
- package/browser-extension/src/icons/icon-48.svg +0 -4
- package/browser-extension/src/lib/api.ts +0 -146
- package/browser-extension/src/lib/markdown.ts +0 -68
- package/browser-extension/src/lib/storage.ts +0 -37
- package/browser-extension/src/lib/types.ts +0 -42
- package/browser-extension/src/manifest.json +0 -47
- package/browser-extension/src/popup/popup.css +0 -510
- package/browser-extension/src/popup/popup.html +0 -128
- package/browser-extension/src/popup/popup.ts +0 -416
- package/browser-extension/tsconfig.json +0 -16
- package/tests/e2e/README.md +0 -25
- package/tests/e2e/navigation.spec.ts +0 -14
- package/tests/e2e/playwright.config.ts +0 -14
- package/tests/integration/README.md +0 -25
- package/tests/integration/mcp-contract.test.ts +0 -57
- package/tests/integration/mcp-transport.test.ts +0 -361
- package/tests/integration/package-lock.json +0 -1463
- package/tests/integration/package.json +0 -8
- package/tests/integration/vitest.config.ts +0 -11
- package/tests/security-hardening.test.ts +0 -456
- package/tests/unit/build-integrity.test.ts +0 -137
- package/tests/unit/cli-build.test.ts +0 -180
- package/tests/unit/cli-config.test.ts +0 -257
- package/tests/unit/cli-mcp-install-toml.test.ts +0 -586
- package/tests/unit/cli-mcp-install.test.ts +0 -123
- package/tests/unit/cli-mcp-stdio-default.test.ts +0 -180
- package/tests/unit/cli-modules-load.test.ts +0 -64
- package/tests/unit/cli-port.test.ts +0 -87
- package/tests/unit/cli-skill-auto-copy.test.ts +0 -260
- package/tests/unit/cli-smoke.test.ts +0 -88
- package/tests/unit/cli-uninstall.test.ts +0 -218
- package/tests/unit/cli-update-root.test.ts +0 -89
- package/tests/unit/cli-user-flow-sim.test.ts +0 -506
- package/tests/unit/cli-wait-hint.test.ts +0 -86
- package/tests/unit/custom-agents.test.ts +0 -478
- package/tests/unit/dep-safety.test.ts +0 -126
- package/tests/unit/detect-system-lang.test.ts +0 -122
- package/tests/unit/mcp-build.test.ts +0 -162
- package/tests/unit/setup-needs-restart.test.ts +0 -139
- package/tests/unit/stop-restart.test.ts +0 -393
- package/tests/unit/vitest.config.ts +0 -8
- /package/_standalone/.next/static/{w5bqzZbd2_vdoPRB0JQ_I → Dn8EHqUedSzanCfrM8WWS}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{w5bqzZbd2_vdoPRB0JQ_I → Dn8EHqUedSzanCfrM8WWS}/_ssgManifest.js +0 -0
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Tests for app/lib/custom-agents.ts — slugify, inferDefaults, toAgentDef,
|
|
8
|
-
* generateUniqueKey, validateCustomAgentInput, detectBaseDir.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// We need to mock mcp-agents to avoid importing the full agent registry
|
|
12
|
-
vi.mock('../../app/lib/mcp-agents', () => ({
|
|
13
|
-
expandHome: (p: string) => p.replace(/^~/, os.homedir()),
|
|
14
|
-
MCP_AGENTS: {
|
|
15
|
-
cursor: { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
16
|
-
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
17
|
-
},
|
|
18
|
-
SKILL_AGENT_REGISTRY: {},
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Mock settings to avoid file system dependency
|
|
22
|
-
let mockCustomAgents: unknown[] = [];
|
|
23
|
-
vi.mock('../../app/lib/settings', () => ({
|
|
24
|
-
readSettings: () => ({ customAgents: mockCustomAgents, ai: {}, mindRoot: '' }),
|
|
25
|
-
writeSettings: vi.fn((settings: Record<string, unknown>) => {
|
|
26
|
-
mockCustomAgents = (settings.customAgents as unknown[]) ?? [];
|
|
27
|
-
}),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
// Dynamic import after mocks are set up
|
|
31
|
-
let slugify: typeof import('../../app/lib/custom-agents').slugify;
|
|
32
|
-
let generateUniqueKey: typeof import('../../app/lib/custom-agents').generateUniqueKey;
|
|
33
|
-
let inferDefaults: typeof import('../../app/lib/custom-agents').inferDefaults;
|
|
34
|
-
let toAgentDef: typeof import('../../app/lib/custom-agents').toAgentDef;
|
|
35
|
-
let validateCustomAgentInput: typeof import('../../app/lib/custom-agents').validateCustomAgentInput;
|
|
36
|
-
let detectBaseDir: typeof import('../../app/lib/custom-agents').detectBaseDir;
|
|
37
|
-
let loadCustomAgents: typeof import('../../app/lib/custom-agents').loadCustomAgents;
|
|
38
|
-
let saveCustomAgents: typeof import('../../app/lib/custom-agents').saveCustomAgents;
|
|
39
|
-
let getAllAgents: typeof import('../../app/lib/custom-agents').getAllAgents;
|
|
40
|
-
let scanCustomAgentSkills: typeof import('../../app/lib/custom-agents').scanCustomAgentSkills;
|
|
41
|
-
|
|
42
|
-
beforeEach(async () => {
|
|
43
|
-
mockCustomAgents = [];
|
|
44
|
-
const mod = await import('../../app/lib/custom-agents');
|
|
45
|
-
slugify = mod.slugify;
|
|
46
|
-
generateUniqueKey = mod.generateUniqueKey;
|
|
47
|
-
inferDefaults = mod.inferDefaults;
|
|
48
|
-
toAgentDef = mod.toAgentDef;
|
|
49
|
-
validateCustomAgentInput = mod.validateCustomAgentInput;
|
|
50
|
-
detectBaseDir = mod.detectBaseDir;
|
|
51
|
-
loadCustomAgents = mod.loadCustomAgents;
|
|
52
|
-
saveCustomAgents = mod.saveCustomAgents;
|
|
53
|
-
getAllAgents = mod.getAllAgents;
|
|
54
|
-
scanCustomAgentSkills = mod.scanCustomAgentSkills;
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
/* ─── slugify ─── */
|
|
58
|
-
|
|
59
|
-
describe('slugify', () => {
|
|
60
|
-
it('converts normal names to lowercase kebab-case', () => {
|
|
61
|
-
expect(slugify('QC Law Pro 3.0')).toBe('qc-law-pro-30');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('handles underscores and spaces', () => {
|
|
65
|
-
expect(slugify('Work_Buddy Test')).toBe('work-buddy-test');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('strips non-ASCII characters (CJK)', () => {
|
|
69
|
-
expect(slugify('工作助手')).toBe('');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('strips emoji', () => {
|
|
73
|
-
expect(slugify('My🚀Agent')).toBe('myagent');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('collapses multiple hyphens', () => {
|
|
77
|
-
expect(slugify('a - - b')).toBe('a-b');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('trims leading/trailing hyphens', () => {
|
|
81
|
-
expect(slugify(' -hello- ')).toBe('hello');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('handles empty string', () => {
|
|
85
|
-
expect(slugify('')).toBe('');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('handles string with only special characters', () => {
|
|
89
|
-
expect(slugify('!@#$%^')).toBe('');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('preserves numbers', () => {
|
|
93
|
-
expect(slugify('Agent 42')).toBe('agent-42');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/* ─── generateUniqueKey ─── */
|
|
98
|
-
|
|
99
|
-
describe('generateUniqueKey', () => {
|
|
100
|
-
it('returns slug when no conflict', () => {
|
|
101
|
-
expect(generateUniqueKey('QCLaw', new Set())).toBe('qclaw');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('appends suffix on collision', () => {
|
|
105
|
-
expect(generateUniqueKey('QCLaw', new Set(['qclaw']))).toBe('qclaw-2');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('increments suffix on multiple collisions', () => {
|
|
109
|
-
expect(generateUniqueKey('QCLaw', new Set(['qclaw', 'qclaw-2']))).toBe('qclaw-3');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('falls back to custom-N for empty slug (CJK name)', () => {
|
|
113
|
-
expect(generateUniqueKey('工作助手', new Set())).toBe('custom-1');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('increments custom-N on collision', () => {
|
|
117
|
-
expect(generateUniqueKey('工作', new Set(['custom-1']))).toBe('custom-2');
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
/* ─── inferDefaults ─── */
|
|
122
|
-
|
|
123
|
-
describe('inferDefaults', () => {
|
|
124
|
-
it('generates correct defaults from name and baseDir', () => {
|
|
125
|
-
const result = inferDefaults('QCLaw', '~/.qclaw');
|
|
126
|
-
expect(result.name).toBe('QCLaw');
|
|
127
|
-
expect(result.baseDir).toBe('~/.qclaw/');
|
|
128
|
-
expect(result.global).toBe('~/.qclaw/mcp.json');
|
|
129
|
-
expect(result.project).toBeNull();
|
|
130
|
-
expect(result.configKey).toBe('mcpServers');
|
|
131
|
-
expect(result.format).toBe('json');
|
|
132
|
-
expect(result.preferredTransport).toBe('stdio');
|
|
133
|
-
expect(result.presenceDirs).toEqual(['~/.qclaw/']);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('preserves trailing slash in baseDir', () => {
|
|
137
|
-
const result = inferDefaults('Test', '~/.test/');
|
|
138
|
-
expect(result.baseDir).toBe('~/.test/');
|
|
139
|
-
expect(result.global).toBe('~/.test/mcp.json');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('sets skillDir to baseDir + skills/', () => {
|
|
143
|
-
const result = inferDefaults('QCLaw', '~/.qclaw');
|
|
144
|
-
expect(result.skillDir).toBe('~/.qclaw/skills/');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('preserves trailing slash for skillDir', () => {
|
|
148
|
-
const result = inferDefaults('Test', '~/.test/');
|
|
149
|
-
expect(result.skillDir).toBe('~/.test/skills/');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
/* ─── toAgentDef ─── */
|
|
154
|
-
|
|
155
|
-
describe('toAgentDef', () => {
|
|
156
|
-
it('converts CustomAgentDef to AgentDef correctly', () => {
|
|
157
|
-
const custom = {
|
|
158
|
-
name: 'QCLaw',
|
|
159
|
-
key: 'qclaw',
|
|
160
|
-
baseDir: '~/.qclaw/',
|
|
161
|
-
global: '~/.qclaw/mcp.json',
|
|
162
|
-
configKey: 'mcpServers',
|
|
163
|
-
format: 'json' as const,
|
|
164
|
-
preferredTransport: 'stdio' as const,
|
|
165
|
-
presenceDirs: ['~/.qclaw/'],
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const def = toAgentDef(custom);
|
|
169
|
-
expect(def.name).toBe('QCLaw');
|
|
170
|
-
expect(def.key).toBe('mcpServers'); // AgentDef.key = configKey
|
|
171
|
-
expect(def.global).toBe('~/.qclaw/mcp.json');
|
|
172
|
-
expect(def.project).toBeNull();
|
|
173
|
-
expect(def.preferredTransport).toBe('stdio');
|
|
174
|
-
expect(def.format).toBe('json');
|
|
175
|
-
expect(def.presenceDirs).toEqual(['~/.qclaw/']);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('handles optional fields', () => {
|
|
179
|
-
const custom = {
|
|
180
|
-
name: 'Test',
|
|
181
|
-
key: 'test',
|
|
182
|
-
baseDir: '~/.test/',
|
|
183
|
-
global: '~/.test/config.toml',
|
|
184
|
-
configKey: 'mcp_servers',
|
|
185
|
-
format: 'toml' as const,
|
|
186
|
-
preferredTransport: 'http' as const,
|
|
187
|
-
presenceDirs: ['~/.test/'],
|
|
188
|
-
presenceCli: 'test-cli',
|
|
189
|
-
globalNestedKey: 'mcp.servers',
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const def = toAgentDef(custom);
|
|
193
|
-
expect(def.format).toBe('toml');
|
|
194
|
-
expect(def.preferredTransport).toBe('http');
|
|
195
|
-
expect(def.presenceCli).toBe('test-cli');
|
|
196
|
-
expect(def.globalNestedKey).toBe('mcp.servers');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
/* ─── validateCustomAgentInput ─── */
|
|
201
|
-
|
|
202
|
-
describe('validateCustomAgentInput', () => {
|
|
203
|
-
it('returns null for valid input', () => {
|
|
204
|
-
const err = validateCustomAgentInput(
|
|
205
|
-
{ name: 'QCLaw', baseDir: '~/.qclaw/' },
|
|
206
|
-
new Set(),
|
|
207
|
-
);
|
|
208
|
-
expect(err).toBeNull();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('rejects empty name', () => {
|
|
212
|
-
const err = validateCustomAgentInput(
|
|
213
|
-
{ name: '', baseDir: '~/.qclaw/' },
|
|
214
|
-
new Set(),
|
|
215
|
-
);
|
|
216
|
-
expect(err).toBe('Agent name is required');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('rejects empty baseDir', () => {
|
|
220
|
-
const err = validateCustomAgentInput(
|
|
221
|
-
{ name: 'Test', baseDir: '' },
|
|
222
|
-
new Set(),
|
|
223
|
-
);
|
|
224
|
-
expect(err).toBe('Config directory is required');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('rejects relative path', () => {
|
|
228
|
-
const err = validateCustomAgentInput(
|
|
229
|
-
{ name: 'Test', baseDir: 'relative/path' },
|
|
230
|
-
new Set(),
|
|
231
|
-
);
|
|
232
|
-
expect(err).toContain('absolute path');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('rejects key conflict with built-in agent', () => {
|
|
236
|
-
const err = validateCustomAgentInput(
|
|
237
|
-
{ name: 'Cursor', baseDir: '~/.my-cursor/' },
|
|
238
|
-
new Set(),
|
|
239
|
-
);
|
|
240
|
-
expect(err).toContain('Conflicts with built-in agent');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('rejects key conflict with existing custom agent', () => {
|
|
244
|
-
const err = validateCustomAgentInput(
|
|
245
|
-
{ name: 'QCLaw', baseDir: '~/.qclaw/' },
|
|
246
|
-
new Set(['qclaw']),
|
|
247
|
-
);
|
|
248
|
-
expect(err).toContain('already exists');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('skips key conflict check in edit mode', () => {
|
|
252
|
-
const err = validateCustomAgentInput(
|
|
253
|
-
{ name: 'QCLaw', baseDir: '~/.qclaw/' },
|
|
254
|
-
new Set(['qclaw']),
|
|
255
|
-
true,
|
|
256
|
-
);
|
|
257
|
-
expect(err).toBeNull();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('accepts absolute path starting with /', () => {
|
|
261
|
-
const err = validateCustomAgentInput(
|
|
262
|
-
{ name: 'Test', baseDir: '/opt/test/' },
|
|
263
|
-
new Set(),
|
|
264
|
-
);
|
|
265
|
-
expect(err).toBeNull();
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
/* ─── loadCustomAgents / saveCustomAgents ─── */
|
|
270
|
-
|
|
271
|
-
describe('loadCustomAgents', () => {
|
|
272
|
-
it('returns empty array when no customAgents in config', () => {
|
|
273
|
-
mockCustomAgents = [];
|
|
274
|
-
const result = loadCustomAgents();
|
|
275
|
-
expect(result).toEqual([]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('filters out invalid entries', () => {
|
|
279
|
-
mockCustomAgents = [
|
|
280
|
-
{ name: 'Valid', key: 'valid', baseDir: '~/.valid/' },
|
|
281
|
-
{ invalid: true },
|
|
282
|
-
null,
|
|
283
|
-
'string',
|
|
284
|
-
];
|
|
285
|
-
const result = loadCustomAgents();
|
|
286
|
-
expect(result).toHaveLength(1);
|
|
287
|
-
expect(result[0].name).toBe('Valid');
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
/* ─── getAllAgents ─── */
|
|
292
|
-
|
|
293
|
-
describe('getAllAgents', () => {
|
|
294
|
-
it('returns built-in agents when no custom agents exist', () => {
|
|
295
|
-
mockCustomAgents = [];
|
|
296
|
-
const all = getAllAgents();
|
|
297
|
-
expect('cursor' in all).toBe(true);
|
|
298
|
-
expect('claude-code' in all).toBe(true);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('merges custom agents with built-in ones', () => {
|
|
302
|
-
mockCustomAgents = [
|
|
303
|
-
{
|
|
304
|
-
name: 'QCLaw',
|
|
305
|
-
key: 'qclaw',
|
|
306
|
-
baseDir: '~/.qclaw/',
|
|
307
|
-
global: '~/.qclaw/mcp.json',
|
|
308
|
-
configKey: 'mcpServers',
|
|
309
|
-
format: 'json',
|
|
310
|
-
preferredTransport: 'stdio',
|
|
311
|
-
presenceDirs: ['~/.qclaw/'],
|
|
312
|
-
},
|
|
313
|
-
];
|
|
314
|
-
const all = getAllAgents();
|
|
315
|
-
expect('qclaw' in all).toBe(true);
|
|
316
|
-
expect(all.qclaw.name).toBe('QCLaw');
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('built-in agents take priority on key collision', () => {
|
|
320
|
-
mockCustomAgents = [
|
|
321
|
-
{
|
|
322
|
-
name: 'My Cursor',
|
|
323
|
-
key: 'cursor',
|
|
324
|
-
baseDir: '~/.my-cursor/',
|
|
325
|
-
global: '~/.my-cursor/mcp.json',
|
|
326
|
-
configKey: 'mcpServers',
|
|
327
|
-
format: 'json',
|
|
328
|
-
preferredTransport: 'stdio',
|
|
329
|
-
presenceDirs: ['~/.my-cursor/'],
|
|
330
|
-
},
|
|
331
|
-
];
|
|
332
|
-
const all = getAllAgents();
|
|
333
|
-
expect(all.cursor.name).toBe('Cursor'); // built-in wins
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
/* ─── detectBaseDir ─── */
|
|
338
|
-
|
|
339
|
-
describe('detectBaseDir', () => {
|
|
340
|
-
let tempDir: string;
|
|
341
|
-
|
|
342
|
-
beforeEach(() => {
|
|
343
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mindos-detect-test-'));
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
afterEach(() => {
|
|
347
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('returns exists=false for non-existent directory', () => {
|
|
351
|
-
const result = detectBaseDir('/tmp/nonexistent-dir-' + Date.now());
|
|
352
|
-
expect(result.exists).toBe(false);
|
|
353
|
-
expect(result.hasSkillsDir).toBe(false);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('returns exists=true for existing directory', () => {
|
|
357
|
-
const result = detectBaseDir(tempDir);
|
|
358
|
-
expect(result.exists).toBe(true);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('detects JSON config with mcpServers key', () => {
|
|
362
|
-
fs.writeFileSync(
|
|
363
|
-
path.join(tempDir, 'mcp.json'),
|
|
364
|
-
JSON.stringify({ mcpServers: {} }),
|
|
365
|
-
);
|
|
366
|
-
const result = detectBaseDir(tempDir);
|
|
367
|
-
expect(result.exists).toBe(true);
|
|
368
|
-
expect(result.detectedFormat).toBe('json');
|
|
369
|
-
expect(result.detectedConfigKey).toBe('mcpServers');
|
|
370
|
-
expect(result.detectedConfig).toContain('mcp.json');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('detects JSON config with servers key', () => {
|
|
374
|
-
fs.writeFileSync(
|
|
375
|
-
path.join(tempDir, 'settings.json'),
|
|
376
|
-
JSON.stringify({ servers: {} }),
|
|
377
|
-
);
|
|
378
|
-
const result = detectBaseDir(tempDir);
|
|
379
|
-
expect(result.detectedFormat).toBe('json');
|
|
380
|
-
expect(result.detectedConfigKey).toBe('servers');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('detects skills/ subdirectory', () => {
|
|
384
|
-
fs.mkdirSync(path.join(tempDir, 'skills'));
|
|
385
|
-
const result = detectBaseDir(tempDir);
|
|
386
|
-
expect(result.hasSkillsDir).toBe(true);
|
|
387
|
-
expect(result.detectedSkillDir).toBeDefined();
|
|
388
|
-
expect(result.skillCount).toBe(0);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('counts skills inside skills/ directory', () => {
|
|
392
|
-
const skillsDir = path.join(tempDir, 'skills');
|
|
393
|
-
fs.mkdirSync(skillsDir);
|
|
394
|
-
fs.mkdirSync(path.join(skillsDir, 'my-skill-a'));
|
|
395
|
-
fs.mkdirSync(path.join(skillsDir, 'my-skill-b'));
|
|
396
|
-
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), '');
|
|
397
|
-
const result = detectBaseDir(tempDir);
|
|
398
|
-
expect(result.hasSkillsDir).toBe(true);
|
|
399
|
-
expect(result.skillCount).toBe(2);
|
|
400
|
-
expect(result.skillNames).toEqual(['my-skill-a', 'my-skill-b']);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('ignores hidden directories in skills/', () => {
|
|
404
|
-
const skillsDir = path.join(tempDir, 'skills');
|
|
405
|
-
fs.mkdirSync(skillsDir);
|
|
406
|
-
fs.mkdirSync(path.join(skillsDir, 'visible-skill'));
|
|
407
|
-
fs.mkdirSync(path.join(skillsDir, '.hidden-skill'));
|
|
408
|
-
const result = detectBaseDir(tempDir);
|
|
409
|
-
expect(result.skillCount).toBe(1);
|
|
410
|
-
expect(result.skillNames).toEqual(['visible-skill']);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('detects TOML config with mcp_servers section', () => {
|
|
414
|
-
fs.writeFileSync(
|
|
415
|
-
path.join(tempDir, 'config.toml'),
|
|
416
|
-
'[mcp_servers]\nmindos = { command = "npx" }\n',
|
|
417
|
-
);
|
|
418
|
-
const result = detectBaseDir(tempDir);
|
|
419
|
-
expect(result.detectedFormat).toBe('toml');
|
|
420
|
-
expect(result.detectedConfigKey).toBe('mcp_servers');
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('handles empty directory', () => {
|
|
424
|
-
const result = detectBaseDir(tempDir);
|
|
425
|
-
expect(result.exists).toBe(true);
|
|
426
|
-
expect(result.detectedConfig).toBeUndefined();
|
|
427
|
-
expect(result.hasSkillsDir).toBe(false);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it('suggests name from directory name', () => {
|
|
431
|
-
const namedDir = path.join(tempDir, 'qclaw');
|
|
432
|
-
fs.mkdirSync(namedDir);
|
|
433
|
-
const result = detectBaseDir(namedDir);
|
|
434
|
-
expect(result.suggestedName).toBe('Qclaw');
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
/* ─── scanCustomAgentSkills ─── */
|
|
439
|
-
|
|
440
|
-
describe('scanCustomAgentSkills', () => {
|
|
441
|
-
let tempDir: string;
|
|
442
|
-
|
|
443
|
-
beforeEach(() => {
|
|
444
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mindos-skill-scan-'));
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
afterEach(() => {
|
|
448
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it('returns empty when skillDir does not exist', () => {
|
|
452
|
-
const custom = { ...inferDefaults('Test', tempDir), key: 'test', skillDir: tempDir + '/skills/' };
|
|
453
|
-
fs.rmSync(path.join(tempDir), { recursive: true, force: true });
|
|
454
|
-
const result = scanCustomAgentSkills(custom);
|
|
455
|
-
expect(result.skills).toEqual([]);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('scans skills from skillDir', () => {
|
|
459
|
-
const skillsDir = path.join(tempDir, 'skills');
|
|
460
|
-
fs.mkdirSync(skillsDir);
|
|
461
|
-
fs.mkdirSync(path.join(skillsDir, 'alpha'));
|
|
462
|
-
fs.mkdirSync(path.join(skillsDir, 'beta'));
|
|
463
|
-
const custom = { ...inferDefaults('Test', tempDir + '/'), key: 'test', skillDir: tempDir + '/skills/' };
|
|
464
|
-
const result = scanCustomAgentSkills(custom);
|
|
465
|
-
expect(result.skills).toEqual(['alpha', 'beta']);
|
|
466
|
-
expect(result.sourcePath).toContain('skills');
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('uses baseDir + skills/ when skillDir is not set', () => {
|
|
470
|
-
const skillsDir = path.join(tempDir, 'skills');
|
|
471
|
-
fs.mkdirSync(skillsDir);
|
|
472
|
-
fs.mkdirSync(path.join(skillsDir, 'gamma'));
|
|
473
|
-
const custom = { ...inferDefaults('Test', tempDir + '/'), key: 'test' };
|
|
474
|
-
delete (custom as Record<string, unknown>).skillDir;
|
|
475
|
-
const result = scanCustomAgentSkills(custom);
|
|
476
|
-
expect(result.skills).toEqual(['gamma']);
|
|
477
|
-
});
|
|
478
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Dependency safety tests — catch version-range vs actual-import mismatches
|
|
7
|
-
* before they reach users.
|
|
8
|
-
*
|
|
9
|
-
* Motivation: @modelcontextprotocol/sdk declared ^1.6.1 but code imported
|
|
10
|
-
* server/express.js which only exists since 1.25.0. The lockfile masked the
|
|
11
|
-
* bug locally; fresh installs on new machines crashed at runtime.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
15
|
-
const MCP_DIR = path.join(ROOT, 'mcp');
|
|
16
|
-
const MCP_SRC = path.join(MCP_DIR, 'src', 'index.ts');
|
|
17
|
-
const MCP_NODE_MODULES = path.join(MCP_DIR, 'node_modules');
|
|
18
|
-
|
|
19
|
-
const hasMcpSrc = fs.existsSync(MCP_SRC);
|
|
20
|
-
const hasMcpModules = fs.existsSync(MCP_NODE_MODULES);
|
|
21
|
-
|
|
22
|
-
describe.skipIf(!hasMcpSrc)('MCP dependency safety', () => {
|
|
23
|
-
/** Extract all bare-specifier imports from a TS/JS file */
|
|
24
|
-
function extractImports(filePath: string): string[] {
|
|
25
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
-
const imports: string[] = [];
|
|
27
|
-
// Match: import ... from "pkg" / import ... from "pkg/sub/path.js"
|
|
28
|
-
const re = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
29
|
-
let m: RegExpExecArray | null;
|
|
30
|
-
while ((m = re.exec(content)) !== null) {
|
|
31
|
-
// Skip node: built-ins and relative imports
|
|
32
|
-
if (!m[1].startsWith('.') && !m[1].startsWith('node:')) {
|
|
33
|
-
imports.push(m[1]);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return imports;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
it('all MCP source imports resolve to existing files in node_modules', () => {
|
|
40
|
-
if (!hasMcpModules) return; // skip if deps not installed
|
|
41
|
-
|
|
42
|
-
const imports = extractImports(MCP_SRC);
|
|
43
|
-
expect(imports.length).toBeGreaterThan(0);
|
|
44
|
-
|
|
45
|
-
const missing: string[] = [];
|
|
46
|
-
for (const specifier of imports) {
|
|
47
|
-
// e.g. "@modelcontextprotocol/sdk/server/express.js" or "zod"
|
|
48
|
-
try {
|
|
49
|
-
require.resolve(specifier, { paths: [MCP_DIR] });
|
|
50
|
-
} catch {
|
|
51
|
-
missing.push(specifier);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
expect(missing, `Imports not resolvable in node_modules: ${missing.join(', ')}`).toEqual([]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('package.json version range lower bound has required subpath exports', () => {
|
|
59
|
-
// Read package.json to get declared deps
|
|
60
|
-
const pkgPath = path.join(MCP_DIR, 'package.json');
|
|
61
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
62
|
-
const sdkRange = pkg.dependencies?.['@modelcontextprotocol/sdk'];
|
|
63
|
-
if (!sdkRange) return;
|
|
64
|
-
|
|
65
|
-
// Extract lower bound from semver range (^1.25.0 → 1.25.0)
|
|
66
|
-
const lowerMatch = sdkRange.match(/(\d+\.\d+\.\d+)/);
|
|
67
|
-
if (!lowerMatch) return;
|
|
68
|
-
const lowerBound = lowerMatch[1];
|
|
69
|
-
const [major, minor] = lowerBound.split('.').map(Number);
|
|
70
|
-
|
|
71
|
-
// The express.js subpath was added in 1.25.0 — ensure our lower bound is >= 1.25.0
|
|
72
|
-
// This is a specific guard for the known breaking change
|
|
73
|
-
const imports = extractImports(MCP_SRC);
|
|
74
|
-
const usesExpress = imports.some(i => i.includes('express'));
|
|
75
|
-
if (usesExpress) {
|
|
76
|
-
expect(
|
|
77
|
-
major > 1 || (major === 1 && minor >= 25),
|
|
78
|
-
`SDK version range "${sdkRange}" allows <1.25.0 which lacks server/express.js`,
|
|
79
|
-
).toBe(true);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('npm install patterns', () => {
|
|
85
|
-
it('no raw --prefer-offline without fallback in CLI scripts', () => {
|
|
86
|
-
// Scan bin/ for direct --prefer-offline usage that bypasses npmInstall()
|
|
87
|
-
const binDir = path.join(ROOT, 'bin');
|
|
88
|
-
const files = collectJsFiles(binDir);
|
|
89
|
-
|
|
90
|
-
const violations: string[] = [];
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
// utils.js and shell.js define npmInstall() which legitimately uses --prefer-offline
|
|
93
|
-
if (path.basename(file) === 'utils.js' || path.basename(file) === 'shell.js') continue;
|
|
94
|
-
|
|
95
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
96
|
-
const lines = content.split('\n');
|
|
97
|
-
for (let i = 0; i < lines.length; i++) {
|
|
98
|
-
const line = lines[i];
|
|
99
|
-
// Flag: execSync/run with --prefer-offline (should use npmInstall instead)
|
|
100
|
-
if (line.includes('--prefer-offline') && !line.trimStart().startsWith('//') && !line.trimStart().startsWith('*')) {
|
|
101
|
-
violations.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim()}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
expect(
|
|
107
|
-
violations,
|
|
108
|
-
`Found raw --prefer-offline usage (use npmInstall() from utils.js instead):\n${violations.join('\n')}`,
|
|
109
|
-
).toEqual([]);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
/** Collect .js files recursively */
|
|
114
|
-
function collectJsFiles(dir: string): string[] {
|
|
115
|
-
const results: string[] = [];
|
|
116
|
-
if (!fs.existsSync(dir)) return results;
|
|
117
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
-
const full = path.join(dir, entry.name);
|
|
119
|
-
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
120
|
-
results.push(...collectJsFiles(full));
|
|
121
|
-
} else if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
122
|
-
results.push(full);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return results;
|
|
126
|
-
}
|