@nemus-cli/nemus 0.2.1
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/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/assets/README.md +53 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +66 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon-64.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon.svg +23 -0
- package/assets/logo-256.png +0 -0
- package/assets/logo-512.png +0 -0
- package/assets/logo-wordmark.svg +35 -0
- package/assets/logo.svg +32 -0
- package/bin/workspace.js +130 -0
- package/dist/cli/ai-prompt.js +421 -0
- package/dist/cli/dashboard/AgentList.js +61 -0
- package/dist/cli/dashboard/DashboardSidebar.js +280 -0
- package/dist/cli/dashboard/HelpFooter.js +73 -0
- package/dist/cli/dashboard/StatusBadge.js +50 -0
- package/dist/cli/dashboard/index.js +11 -0
- package/dist/cli/dashboard/types.js +2 -0
- package/dist/cli/tui.js +48 -0
- package/dist/commands/ai-prompt.js +22 -0
- package/dist/commands/analyze-deps.js +138 -0
- package/dist/commands/archive.js +113 -0
- package/dist/commands/branch/create.js +119 -0
- package/dist/commands/branch/index.js +104 -0
- package/dist/commands/branch/merge.js +90 -0
- package/dist/commands/branch/rebase.js +84 -0
- package/dist/commands/branch/switch.js +165 -0
- package/dist/commands/cache/index.js +80 -0
- package/dist/commands/cache/manager.js +198 -0
- package/dist/commands/cleanup.js +153 -0
- package/dist/commands/configure-claude.js +55 -0
- package/dist/commands/configure.js +231 -0
- package/dist/commands/create.js +204 -0
- package/dist/commands/dashboard/hook-handler.js +162 -0
- package/dist/commands/dashboard/index.js +258 -0
- package/dist/commands/dashboard/launcher.js +40 -0
- package/dist/commands/dashboard/session-picker.js +106 -0
- package/dist/commands/dashboard/sidebar.js +39 -0
- package/dist/commands/dashboard/workspace-picker.js +95 -0
- package/dist/commands/delete.js +194 -0
- package/dist/commands/deprecated-aliases.js +44 -0
- package/dist/commands/diff.js +129 -0
- package/dist/commands/doctor.js +141 -0
- package/dist/commands/generate-docs.js +77 -0
- package/dist/commands/ghq-status.js +72 -0
- package/dist/commands/go.js +154 -0
- package/dist/commands/history.js +124 -0
- package/dist/commands/list.js +191 -0
- package/dist/commands/mcp/index.js +69 -0
- package/dist/commands/migrate.js +296 -0
- package/dist/commands/remove-repo.js +217 -0
- package/dist/commands/report-bug.js +112 -0
- package/dist/commands/run.js +147 -0
- package/dist/commands/save-context.js +171 -0
- package/dist/commands/sessions.js +129 -0
- package/dist/commands/status.js +138 -0
- package/dist/commands/suite/create.js +212 -0
- package/dist/commands/suite/delete.js +64 -0
- package/dist/commands/suite/export.js +126 -0
- package/dist/commands/suite/import.js +128 -0
- package/dist/commands/suite/index.js +94 -0
- package/dist/commands/suite/list.js +49 -0
- package/dist/commands/suite/use.js +227 -0
- package/dist/commands/sync.js +117 -0
- package/dist/commands/update.js +203 -0
- package/dist/mcp/install.js +550 -0
- package/dist/mcp/server.js +398 -0
- package/dist/mcp/tools.js +922 -0
- package/dist/pi-extensions/permission-sync.ts.template +41 -0
- package/dist/pi-extensions/permissions-gate.ts.template +69 -0
- package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/dist/pi-extensions/review-reminder.ts.template +58 -0
- package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
- package/dist/program.js +136 -0
- package/dist/scripts/workspace-table.py +394 -0
- package/dist/types/dashboard.js +11 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/agent-config.js +263 -0
- package/dist/utils/agent-state.js +178 -0
- package/dist/utils/banner.js +24 -0
- package/dist/utils/branch-operations.js +232 -0
- package/dist/utils/bug-report.js +355 -0
- package/dist/utils/cache.js +168 -0
- package/dist/utils/claude-integration.js +617 -0
- package/dist/utils/claude-sessions.js +260 -0
- package/dist/utils/cleanup-operations.js +120 -0
- package/dist/utils/colors.js +26 -0
- package/dist/utils/command-helpers.js +44 -0
- package/dist/utils/config.js +150 -0
- package/dist/utils/context-file.js +33 -0
- package/dist/utils/dashboard-hooks.js +172 -0
- package/dist/utils/dependency-analyzer.js +234 -0
- package/dist/utils/diff-operations.js +80 -0
- package/dist/utils/doc-generator.js +184 -0
- package/dist/utils/ghq-integration.js +332 -0
- package/dist/utils/git-operations.js +237 -0
- package/dist/utils/git-status.js +187 -0
- package/dist/utils/github.js +68 -0
- package/dist/utils/health-checks.js +345 -0
- package/dist/utils/history.js +150 -0
- package/dist/utils/hooks.js +100 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/permission-sync.js +681 -0
- package/dist/utils/pi-extensions.js +224 -0
- package/dist/utils/progress.js +92 -0
- package/dist/utils/prompts.js +279 -0
- package/dist/utils/repo-resolver.js +260 -0
- package/dist/utils/retry.js +65 -0
- package/dist/utils/run-operations.js +30 -0
- package/dist/utils/suite.js +159 -0
- package/dist/utils/sync-operations.js +107 -0
- package/dist/utils/tmux-dashboard.js +184 -0
- package/dist/utils/validation.js +153 -0
- package/dist/utils/version-check.js +121 -0
- package/dist/utils/workspace-meta.js +185 -0
- package/install-shell-integration.sh +532 -0
- package/install.sh +401 -0
- package/package.json +90 -0
- package/remove-shell-block.awk +44 -0
- package/scripts/postinstall.js +85 -0
- package/skills/analyze-deps.md +23 -0
- package/skills/archive-workspace.md +26 -0
- package/skills/branch-create.md +18 -0
- package/skills/create-workspace.md +39 -0
- package/skills/delete-workspace.md +37 -0
- package/skills/list-org-repos.md +26 -0
- package/skills/list-suites.md +24 -0
- package/skills/list-workspaces.md +27 -0
- package/skills/refresh-workspace-docs.md +89 -0
- package/skills/remove-repo.md +25 -0
- package/skills/run-command.md +23 -0
- package/skills/save-context.md +68 -0
- package/skills/search-repos.md +31 -0
- package/skills/snapshot-list.md +18 -0
- package/skills/snapshot-save.md +12 -0
- package/skills/switch-branch.md +18 -0
- package/skills/update-cache.md +21 -0
- package/skills/update-workspace.md +66 -0
- package/skills/workspace-cleanup.md +18 -0
- package/skills/workspace-diff.md +23 -0
- package/skills/workspace-doctor.md +23 -0
- package/skills/workspace-info.md +45 -0
- package/skills/workspace-manager/SKILL.md +201 -0
- package/skills/workspace-manager/references/ai-prompt.md +44 -0
- package/skills/workspace-manager/references/analyze-deps.md +45 -0
- package/skills/workspace-manager/references/archive-workspace.md +38 -0
- package/skills/workspace-manager/references/branch-create.md +31 -0
- package/skills/workspace-manager/references/branch-merge.md +22 -0
- package/skills/workspace-manager/references/branch-rebase.md +22 -0
- package/skills/workspace-manager/references/cache-clear.md +18 -0
- package/skills/workspace-manager/references/cache-info.md +22 -0
- package/skills/workspace-manager/references/configure-claude.md +31 -0
- package/skills/workspace-manager/references/configure.md +35 -0
- package/skills/workspace-manager/references/create-workspace.md +45 -0
- package/skills/workspace-manager/references/dashboard.md +61 -0
- package/skills/workspace-manager/references/delete-workspace.md +33 -0
- package/skills/workspace-manager/references/generate-docs.md +36 -0
- package/skills/workspace-manager/references/ghq-status.md +28 -0
- package/skills/workspace-manager/references/go.md +33 -0
- package/skills/workspace-manager/references/history.md +41 -0
- package/skills/workspace-manager/references/list-org-repos.md +26 -0
- package/skills/workspace-manager/references/list-suites.md +21 -0
- package/skills/workspace-manager/references/list-workspaces.md +27 -0
- package/skills/workspace-manager/references/mcp.md +49 -0
- package/skills/workspace-manager/references/remove-repo.md +26 -0
- package/skills/workspace-manager/references/run-command.md +44 -0
- package/skills/workspace-manager/references/search-repos.md +24 -0
- package/skills/workspace-manager/references/sessions.md +27 -0
- package/skills/workspace-manager/references/suite-create.md +22 -0
- package/skills/workspace-manager/references/suite-delete.md +21 -0
- package/skills/workspace-manager/references/suite-export.md +19 -0
- package/skills/workspace-manager/references/suite-import.md +19 -0
- package/skills/workspace-manager/references/suite-use.md +27 -0
- package/skills/workspace-manager/references/switch-branch.md +25 -0
- package/skills/workspace-manager/references/update-cache.md +22 -0
- package/skills/workspace-manager/references/update-workspace.md +50 -0
- package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
- package/skills/workspace-manager/references/workspace-diff.md +27 -0
- package/skills/workspace-manager/references/workspace-doctor.md +27 -0
- package/skills/workspace-manager/references/workspace-info.md +33 -0
- package/skills/workspace-manager/references/workspace-status.md +43 -0
- package/skills/workspace-manager/references/workspace-sync.md +26 -0
- package/skills/workspace-status.md +26 -0
- package/skills/workspace-sync.md +23 -0
- package/src/cli/ai-prompt.test.ts +667 -0
- package/src/cli/ai-prompt.ts +421 -0
- package/src/cli/dashboard/AgentList.tsx +44 -0
- package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
- package/src/cli/dashboard/HelpFooter.tsx +22 -0
- package/src/cli/dashboard/StatusBadge.tsx +23 -0
- package/src/cli/dashboard/index.ts +5 -0
- package/src/cli/dashboard/types.ts +16 -0
- package/src/cli/tui.tsx +51 -0
- package/src/cli/workspace-cli.test.ts +30 -0
- package/src/commands/ai-prompt.ts +21 -0
- package/src/commands/analyze-deps.ts +119 -0
- package/src/commands/archive.ts +114 -0
- package/src/commands/branch/create.ts +94 -0
- package/src/commands/branch/index.ts +74 -0
- package/src/commands/branch/merge.ts +61 -0
- package/src/commands/branch/rebase.ts +54 -0
- package/src/commands/branch/switch.ts +150 -0
- package/src/commands/cache/index.ts +51 -0
- package/src/commands/cache/manager.ts +168 -0
- package/src/commands/cleanup.ts +132 -0
- package/src/commands/configure-claude.ts +55 -0
- package/src/commands/configure.ts +204 -0
- package/src/commands/create.ts +196 -0
- package/src/commands/dashboard/hook-handler.ts +138 -0
- package/src/commands/dashboard/index.ts +248 -0
- package/src/commands/dashboard/launcher.ts +42 -0
- package/src/commands/dashboard/session-picker.ts +73 -0
- package/src/commands/dashboard/sidebar.js +39 -0
- package/src/commands/dashboard/workspace-picker.ts +62 -0
- package/src/commands/delete.test.ts +250 -0
- package/src/commands/delete.ts +171 -0
- package/src/commands/deprecated-aliases.ts +55 -0
- package/src/commands/diff.ts +106 -0
- package/src/commands/doctor.ts +110 -0
- package/src/commands/generate-docs.ts +45 -0
- package/src/commands/ghq-status.ts +70 -0
- package/src/commands/go.ts +125 -0
- package/src/commands/history.ts +136 -0
- package/src/commands/list.test.ts +219 -0
- package/src/commands/list.ts +176 -0
- package/src/commands/mcp/index.ts +39 -0
- package/src/commands/migrate.ts +277 -0
- package/src/commands/remove-repo.ts +194 -0
- package/src/commands/report-bug.ts +88 -0
- package/src/commands/run.ts +125 -0
- package/src/commands/save-context.test.ts +113 -0
- package/src/commands/save-context.ts +141 -0
- package/src/commands/sessions.ts +99 -0
- package/src/commands/status.ts +111 -0
- package/src/commands/suite/create.ts +240 -0
- package/src/commands/suite/delete.ts +65 -0
- package/src/commands/suite/export.ts +97 -0
- package/src/commands/suite/import.ts +100 -0
- package/src/commands/suite/index.ts +66 -0
- package/src/commands/suite/list.ts +54 -0
- package/src/commands/suite/use.ts +232 -0
- package/src/commands/sync.ts +97 -0
- package/src/commands/update.ts +197 -0
- package/src/mcp/install.ts +529 -0
- package/src/mcp/server.ts +579 -0
- package/src/mcp/tools.test.ts +545 -0
- package/src/mcp/tools.ts +1019 -0
- package/src/mcp/update-workspace.test.ts +220 -0
- package/src/pi-extensions/permission-sync.ts.template +41 -0
- package/src/pi-extensions/permissions-gate.ts.template +69 -0
- package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/src/pi-extensions/review-reminder.ts.template +58 -0
- package/src/pi-extensions/workspace-input-status.ts.template +919 -0
- package/src/program.ts +112 -0
- package/src/scripts/workspace-table.py +394 -0
- package/src/types/dashboard.ts +29 -0
- package/src/types/index.ts +109 -0
- package/src/types/ink.d.ts +171 -0
- package/src/utils/agent-config.test.ts +303 -0
- package/src/utils/agent-config.ts +264 -0
- package/src/utils/agent-state.test.ts +166 -0
- package/src/utils/agent-state.ts +135 -0
- package/src/utils/banner.ts +24 -0
- package/src/utils/branch-operations.ts +237 -0
- package/src/utils/bug-report.test.ts +174 -0
- package/src/utils/bug-report.ts +355 -0
- package/src/utils/cache.ts +150 -0
- package/src/utils/claude-integration.test.ts +381 -0
- package/src/utils/claude-integration.ts +649 -0
- package/src/utils/claude-sessions.test.ts +262 -0
- package/src/utils/claude-sessions.ts +251 -0
- package/src/utils/cleanup-operations.ts +94 -0
- package/src/utils/colors.ts +25 -0
- package/src/utils/command-helpers.ts +46 -0
- package/src/utils/config.test.ts +203 -0
- package/src/utils/config.ts +133 -0
- package/src/utils/context-file.ts +36 -0
- package/src/utils/dashboard-hooks.test.ts +168 -0
- package/src/utils/dashboard-hooks.ts +163 -0
- package/src/utils/dependency-analyzer.ts +243 -0
- package/src/utils/diff-operations.ts +59 -0
- package/src/utils/doc-generator.ts +187 -0
- package/src/utils/ghq-integration.test.ts +195 -0
- package/src/utils/ghq-integration.ts +317 -0
- package/src/utils/git-operations.test.ts +265 -0
- package/src/utils/git-operations.ts +233 -0
- package/src/utils/git-status.ts +171 -0
- package/src/utils/github.ts +74 -0
- package/src/utils/health-checks.ts +359 -0
- package/src/utils/history.ts +140 -0
- package/src/utils/hooks.ts +92 -0
- package/src/utils/logger.ts +37 -0
- package/src/utils/permission-sync.test.ts +593 -0
- package/src/utils/permission-sync.ts +720 -0
- package/src/utils/pi-extensions.test.ts +182 -0
- package/src/utils/pi-extensions.ts +226 -0
- package/src/utils/progress.ts +73 -0
- package/src/utils/prompts.test.ts +169 -0
- package/src/utils/prompts.ts +296 -0
- package/src/utils/repo-resolver.test.ts +313 -0
- package/src/utils/repo-resolver.ts +294 -0
- package/src/utils/retry.test.ts +124 -0
- package/src/utils/retry.ts +85 -0
- package/src/utils/run-operations.ts +38 -0
- package/src/utils/suite.test.ts +256 -0
- package/src/utils/suite.ts +144 -0
- package/src/utils/sync-operations.ts +87 -0
- package/src/utils/tmux-dashboard.ts +193 -0
- package/src/utils/validation.test.ts +139 -0
- package/src/utils/validation.ts +128 -0
- package/src/utils/version-check.test.ts +108 -0
- package/src/utils/version-check.ts +96 -0
- package/src/utils/workspace-meta.ts +173 -0
- package/sync-permissions.sh +137 -0
- package/tsconfig.json +20 -0
- package/uninstall.sh +162 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
const REPO_META = (name: string, status: 'success' | 'failed' = 'success') => ({
|
|
7
|
+
name,
|
|
8
|
+
directoryName: name,
|
|
9
|
+
owner: 'acme',
|
|
10
|
+
clonedAt: '2024-01-01T00:00:00Z',
|
|
11
|
+
cloneUrl: `git@github.com:acme/${name}.git`,
|
|
12
|
+
status,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const writeMetadata = async (wsPath: string, workspaceName: string, repos: ReturnType<typeof REPO_META>[]) => {
|
|
16
|
+
await fs.writeFile(
|
|
17
|
+
path.join(wsPath, '.workspace-meta.json'),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
workspaceName,
|
|
20
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
21
|
+
repositories: repos,
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('MCP tool handlers', () => {
|
|
27
|
+
let tempDir: string;
|
|
28
|
+
let handleListWorkspaces: typeof import('./tools').handleListWorkspaces;
|
|
29
|
+
let handleWorkspaceStatus: typeof import('./tools').handleWorkspaceStatus;
|
|
30
|
+
let handleWorkspaceDiff: typeof import('./tools').handleWorkspaceDiff;
|
|
31
|
+
let handleWorkspaceInfo: typeof import('./tools').handleWorkspaceInfo;
|
|
32
|
+
let handleListSuites: typeof import('./tools').handleListSuites;
|
|
33
|
+
let handleRunCommand: typeof import('./tools').handleRunCommand;
|
|
34
|
+
let handleDeleteWorkspace: typeof import('./tools').handleDeleteWorkspace;
|
|
35
|
+
let handleRemoveRepo: typeof import('./tools').handleRemoveRepo;
|
|
36
|
+
let handleWorkspaceDoctor: typeof import('./tools').handleWorkspaceDoctor;
|
|
37
|
+
let handleAnalyzeDeps: typeof import('./tools').handleAnalyzeDeps;
|
|
38
|
+
let handleBranchCreate: typeof import('./tools').handleBranchCreate;
|
|
39
|
+
let handleSwitchBranch: typeof import('./tools').handleSwitchBranch;
|
|
40
|
+
let handleWorkspaceCleanup: typeof import('./tools').handleWorkspaceCleanup;
|
|
41
|
+
let handleSearchRepos: typeof import('./tools').handleSearchRepos;
|
|
42
|
+
let handleListOrgRepos: typeof import('./tools').handleListOrgRepos;
|
|
43
|
+
let handleUpdateCache: typeof import('./tools').handleUpdateCache;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-mcp-test-'));
|
|
47
|
+
process.env.WORKSPACE_MANAGER_DIR = tempDir;
|
|
48
|
+
process.env.WORKSPACE_MANAGER_CACHE_DIR = path.join(tempDir, '.cache');
|
|
49
|
+
|
|
50
|
+
vi.resetModules();
|
|
51
|
+
const mod = await import('./tools');
|
|
52
|
+
handleListWorkspaces = mod.handleListWorkspaces;
|
|
53
|
+
handleWorkspaceStatus = mod.handleWorkspaceStatus;
|
|
54
|
+
handleWorkspaceDiff = mod.handleWorkspaceDiff;
|
|
55
|
+
handleWorkspaceInfo = mod.handleWorkspaceInfo;
|
|
56
|
+
handleListSuites = mod.handleListSuites;
|
|
57
|
+
handleRunCommand = mod.handleRunCommand;
|
|
58
|
+
handleDeleteWorkspace = mod.handleDeleteWorkspace;
|
|
59
|
+
handleRemoveRepo = mod.handleRemoveRepo;
|
|
60
|
+
handleWorkspaceDoctor = mod.handleWorkspaceDoctor;
|
|
61
|
+
handleAnalyzeDeps = mod.handleAnalyzeDeps;
|
|
62
|
+
handleBranchCreate = mod.handleBranchCreate;
|
|
63
|
+
handleSwitchBranch = mod.handleSwitchBranch;
|
|
64
|
+
handleWorkspaceCleanup = mod.handleWorkspaceCleanup;
|
|
65
|
+
handleSearchRepos = mod.handleSearchRepos;
|
|
66
|
+
handleListOrgRepos = mod.handleListOrgRepos;
|
|
67
|
+
handleUpdateCache = mod.handleUpdateCache;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
delete process.env.WORKSPACE_MANAGER_DIR;
|
|
72
|
+
delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
|
|
73
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('handleListWorkspaces', () => {
|
|
77
|
+
it('returns empty array when no workspaces exist', async () => {
|
|
78
|
+
const result = await handleListWorkspaces();
|
|
79
|
+
expect(result).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns workspaces with metadata', async () => {
|
|
83
|
+
const wsPath = path.join(tempDir, 'test-workspace');
|
|
84
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
85
|
+
await writeMetadata(wsPath, 'test-workspace', [
|
|
86
|
+
REPO_META('repo1'),
|
|
87
|
+
REPO_META('repo2', 'failed'),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const result = await handleListWorkspaces();
|
|
91
|
+
expect(result).toHaveLength(1);
|
|
92
|
+
expect(result[0].name).toBe('test-workspace');
|
|
93
|
+
expect(result[0].repoCount).toBe(1); // only successful repos
|
|
94
|
+
expect(result[0].createdAt).toBe('2024-01-01T00:00:00Z');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('handleWorkspaceInfo', () => {
|
|
99
|
+
it('throws for non-existent workspace', async () => {
|
|
100
|
+
await expect(handleWorkspaceInfo('non-existent')).rejects.toThrow('Workspace not found');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns full metadata for existing workspace', async () => {
|
|
104
|
+
const wsPath = path.join(tempDir, 'my-ws');
|
|
105
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
106
|
+
await writeMetadata(wsPath, 'my-ws', [REPO_META('repo1')]);
|
|
107
|
+
|
|
108
|
+
const result = await handleWorkspaceInfo('my-ws');
|
|
109
|
+
expect(result.workspaceName).toBe('my-ws');
|
|
110
|
+
expect(result.repositories).toHaveLength(1);
|
|
111
|
+
expect(result.path).toBe(wsPath);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('handleWorkspaceStatus', () => {
|
|
116
|
+
it('throws for non-existent workspace', async () => {
|
|
117
|
+
await expect(handleWorkspaceStatus('non-existent')).rejects.toThrow('Workspace not found');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('handleWorkspaceDiff', () => {
|
|
122
|
+
it('throws for non-existent workspace', async () => {
|
|
123
|
+
await expect(handleWorkspaceDiff('non-existent')).rejects.toThrow('Workspace not found');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns diff summary for workspace with repos', async () => {
|
|
127
|
+
const wsPath = path.join(tempDir, 'diff-ws');
|
|
128
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
129
|
+
|
|
130
|
+
// Create a repo directory (not a real git repo, so diffs will be empty/error)
|
|
131
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
132
|
+
|
|
133
|
+
await writeMetadata(wsPath, 'diff-ws', [REPO_META('repo1')]);
|
|
134
|
+
|
|
135
|
+
const result = await handleWorkspaceDiff('diff-ws');
|
|
136
|
+
expect(result.workspace).toBe('diff-ws');
|
|
137
|
+
expect(result.diffs).toHaveLength(1);
|
|
138
|
+
expect(result.summary).toBeDefined();
|
|
139
|
+
expect(result.summary.totalStaged).toBe(0);
|
|
140
|
+
expect(result.summary.totalUnstaged).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('handleRunCommand', () => {
|
|
145
|
+
it('throws for non-existent workspace', async () => {
|
|
146
|
+
await expect(handleRunCommand('non-existent', 'echo hello')).rejects.toThrow('Workspace not found');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('runs command across repos', async () => {
|
|
150
|
+
const wsPath = path.join(tempDir, 'run-ws');
|
|
151
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
152
|
+
|
|
153
|
+
await writeMetadata(wsPath, 'run-ws', [REPO_META('repo1')]);
|
|
154
|
+
|
|
155
|
+
const result = await handleRunCommand('run-ws', 'echo hello');
|
|
156
|
+
expect(result.workspace).toBe('run-ws');
|
|
157
|
+
expect(result.command).toBe('echo hello');
|
|
158
|
+
expect(result.results).toHaveLength(1);
|
|
159
|
+
expect(result.results[0].status).toBe('success');
|
|
160
|
+
expect(result.results[0].stdout).toBe('hello');
|
|
161
|
+
expect(result.summary.successful).toBe(1);
|
|
162
|
+
expect(result.summary.failed).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('handleUpdateCache', () => {
|
|
167
|
+
it('returns repo count and confirmation message', async () => {
|
|
168
|
+
const mockRepos = [
|
|
169
|
+
{ name: 'repo1', owner: { login: 'acme' }, sshUrl: 'git@github.com:acme/repo1.git' },
|
|
170
|
+
{ name: 'repo2', owner: { login: 'acme' }, sshUrl: 'git@github.com:acme/repo2.git' },
|
|
171
|
+
{ name: 'repo3', owner: { login: 'acme' }, sshUrl: 'git@github.com:acme/repo3.git' },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
vi.doMock('../utils/github', () => ({
|
|
175
|
+
fetchOrgRepos: vi.fn().mockResolvedValue(mockRepos),
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
vi.resetModules();
|
|
179
|
+
const mod = await import('./tools');
|
|
180
|
+
const result = await mod.handleUpdateCache();
|
|
181
|
+
|
|
182
|
+
expect(result.repos).toBe(3);
|
|
183
|
+
expect(result.message).toBe('Cache updated with 3 repositories');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('calls fetchOrgRepos with forceRefresh true', async () => {
|
|
187
|
+
const mockFetch = vi.fn().mockResolvedValue([]);
|
|
188
|
+
|
|
189
|
+
vi.doMock('../utils/github', () => ({
|
|
190
|
+
fetchOrgRepos: mockFetch,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
vi.resetModules();
|
|
194
|
+
const mod = await import('./tools');
|
|
195
|
+
await mod.handleUpdateCache();
|
|
196
|
+
|
|
197
|
+
expect(mockFetch).toHaveBeenCalledWith({ forceRefresh: true });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('handleListSuites', () => {
|
|
202
|
+
it('returns empty array when no suites exist', async () => {
|
|
203
|
+
const result = await handleListSuites();
|
|
204
|
+
expect(result).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns suites with summary info', async () => {
|
|
208
|
+
const cacheDir = path.join(tempDir, '.cache');
|
|
209
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
210
|
+
await fs.writeFile(
|
|
211
|
+
path.join(cacheDir, 'suites.json'),
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
version: 1,
|
|
214
|
+
suites: [
|
|
215
|
+
{
|
|
216
|
+
name: 'test-suite',
|
|
217
|
+
description: 'A test suite',
|
|
218
|
+
entries: [
|
|
219
|
+
{ repoName: 'repo1', directoryName: 'repo1' },
|
|
220
|
+
{ repoName: 'repo2', directoryName: 'repo2' },
|
|
221
|
+
],
|
|
222
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
223
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
224
|
+
postCloneHooks: [{ commands: ['npm install'] }],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const result = await handleListSuites();
|
|
231
|
+
expect(result).toHaveLength(1);
|
|
232
|
+
expect(result[0].name).toBe('test-suite');
|
|
233
|
+
expect(result[0].repoCount).toBe(2);
|
|
234
|
+
expect(result[0].hasHooks).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('handleDeleteWorkspace', () => {
|
|
239
|
+
it('throws for non-existent workspace (single)', async () => {
|
|
240
|
+
await expect(handleDeleteWorkspace('non-existent')).rejects.toThrow('Workspace directory not found');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('deletes an existing workspace with metadata', async () => {
|
|
244
|
+
const wsPath = path.join(tempDir, 'delete-me');
|
|
245
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
246
|
+
await writeMetadata(wsPath, 'delete-me', [REPO_META('repo1')]);
|
|
247
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
248
|
+
|
|
249
|
+
const result = await handleDeleteWorkspace('delete-me');
|
|
250
|
+
expect(result.deleted).toBe('delete-me');
|
|
251
|
+
expect(result.path).toBe(wsPath);
|
|
252
|
+
|
|
253
|
+
await expect(fs.access(wsPath)).rejects.toThrow();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('deletes a workspace without metadata (empty workspace)', async () => {
|
|
257
|
+
const wsPath = path.join(tempDir, 'empty-ws');
|
|
258
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
259
|
+
|
|
260
|
+
const result = await handleDeleteWorkspace('empty-ws');
|
|
261
|
+
expect(result.deleted).toBe('empty-ws');
|
|
262
|
+
|
|
263
|
+
await expect(fs.access(wsPath)).rejects.toThrow();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('deletes multiple workspaces at once', async () => {
|
|
267
|
+
const ws1 = path.join(tempDir, 'ws-a');
|
|
268
|
+
const ws2 = path.join(tempDir, 'ws-b');
|
|
269
|
+
await fs.mkdir(ws1, { recursive: true });
|
|
270
|
+
await fs.mkdir(ws2, { recursive: true });
|
|
271
|
+
|
|
272
|
+
const result = await handleDeleteWorkspace(['ws-a', 'ws-b']) as any;
|
|
273
|
+
expect(result.results).toHaveLength(2);
|
|
274
|
+
expect(result.results[0].deleted).toBe(true);
|
|
275
|
+
expect(result.results[1].deleted).toBe(true);
|
|
276
|
+
|
|
277
|
+
await expect(fs.access(ws1)).rejects.toThrow();
|
|
278
|
+
await expect(fs.access(ws2)).rejects.toThrow();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('reports errors per workspace in multi-delete', async () => {
|
|
282
|
+
const ws1 = path.join(tempDir, 'ws-exists');
|
|
283
|
+
await fs.mkdir(ws1, { recursive: true });
|
|
284
|
+
|
|
285
|
+
const result = await handleDeleteWorkspace(['ws-exists', 'ws-missing']) as any;
|
|
286
|
+
expect(result.results).toHaveLength(2);
|
|
287
|
+
expect(result.results[0].deleted).toBe(true);
|
|
288
|
+
expect(result.results[1].deleted).toBe(false);
|
|
289
|
+
expect(result.results[1].error).toContain('not found');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('handleRemoveRepo', () => {
|
|
294
|
+
it('throws for non-existent workspace', async () => {
|
|
295
|
+
await expect(handleRemoveRepo('non-existent', 'repo1')).rejects.toThrow('Workspace not found');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('throws for non-existent repo in workspace', async () => {
|
|
299
|
+
const wsPath = path.join(tempDir, 'remove-ws');
|
|
300
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
301
|
+
await writeMetadata(wsPath, 'remove-ws', [REPO_META('repo1')]);
|
|
302
|
+
|
|
303
|
+
await expect(handleRemoveRepo('remove-ws', 'nonexistent')).rejects.toThrow('Repository not found');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('removes a repo from workspace', async () => {
|
|
307
|
+
const wsPath = path.join(tempDir, 'remove-ws');
|
|
308
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
309
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
310
|
+
await fs.mkdir(path.join(wsPath, 'repo2'), { recursive: true });
|
|
311
|
+
await writeMetadata(wsPath, 'remove-ws', [REPO_META('repo1'), REPO_META('repo2')]);
|
|
312
|
+
|
|
313
|
+
const result = await handleRemoveRepo('remove-ws', 'repo1');
|
|
314
|
+
expect(result.removed).toBe('repo1');
|
|
315
|
+
expect(result.workspace).toBe('remove-ws');
|
|
316
|
+
expect(result.remainingRepos).toBe(1);
|
|
317
|
+
|
|
318
|
+
// Verify repo directory is removed
|
|
319
|
+
await expect(fs.access(path.join(wsPath, 'repo1'))).rejects.toThrow();
|
|
320
|
+
|
|
321
|
+
// Verify metadata is updated
|
|
322
|
+
const metadata = JSON.parse(await fs.readFile(path.join(wsPath, '.workspace-meta.json'), 'utf-8'));
|
|
323
|
+
expect(metadata.repositories).toHaveLength(1);
|
|
324
|
+
expect(metadata.repositories[0].name).toBe('repo2');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('handleWorkspaceDoctor', () => {
|
|
329
|
+
it('throws for non-existent workspace', async () => {
|
|
330
|
+
await expect(handleWorkspaceDoctor('non-existent')).rejects.toThrow('Workspace not found');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('returns health checks and score', async () => {
|
|
334
|
+
const wsPath = path.join(tempDir, 'doctor-ws');
|
|
335
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
336
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
337
|
+
await writeMetadata(wsPath, 'doctor-ws', [REPO_META('repo1')]);
|
|
338
|
+
|
|
339
|
+
const result = await handleWorkspaceDoctor('doctor-ws');
|
|
340
|
+
expect(result.workspace).toBe('doctor-ws');
|
|
341
|
+
expect(typeof result.score).toBe('number');
|
|
342
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
343
|
+
expect(result.score).toBeLessThanOrEqual(100);
|
|
344
|
+
expect(Array.isArray(result.checks)).toBe(true);
|
|
345
|
+
expect(result.checks.length).toBeGreaterThan(0);
|
|
346
|
+
for (const check of result.checks) {
|
|
347
|
+
expect(check).toHaveProperty('category');
|
|
348
|
+
expect(check).toHaveProperty('status');
|
|
349
|
+
expect(check).toHaveProperty('message');
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('handleAnalyzeDeps', () => {
|
|
355
|
+
it('throws for non-existent workspace', async () => {
|
|
356
|
+
await expect(handleAnalyzeDeps('non-existent')).rejects.toThrow('Workspace not found');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('returns dependency analysis', async () => {
|
|
360
|
+
const wsPath = path.join(tempDir, 'deps-ws');
|
|
361
|
+
await fs.mkdir(wsPath, { recursive: true });
|
|
362
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
363
|
+
await fs.mkdir(path.join(wsPath, 'repo2'), { recursive: true });
|
|
364
|
+
|
|
365
|
+
// Create a package.json with a dependency on repo2
|
|
366
|
+
await fs.writeFile(
|
|
367
|
+
path.join(wsPath, 'repo1', 'package.json'),
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
name: 'repo1',
|
|
370
|
+
dependencies: { '@acme/repo2': '^1.0.0' },
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
await fs.writeFile(
|
|
374
|
+
path.join(wsPath, 'repo2', 'package.json'),
|
|
375
|
+
JSON.stringify({ name: 'repo2' })
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
await writeMetadata(wsPath, 'deps-ws', [REPO_META('repo1'), REPO_META('repo2')]);
|
|
379
|
+
|
|
380
|
+
const result = await handleAnalyzeDeps('deps-ws');
|
|
381
|
+
expect(result.workspace).toBe('deps-ws');
|
|
382
|
+
expect(result.analyses).toBeDefined();
|
|
383
|
+
expect(result.analyses.repo1).toBeDefined();
|
|
384
|
+
expect(result.analyses.repo2).toBeDefined();
|
|
385
|
+
expect(Array.isArray(result.circularDependencies)).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('handleBranchCreate', () => {
|
|
390
|
+
it('throws for non-existent workspace', async () => {
|
|
391
|
+
await expect(handleBranchCreate('non-existent', 'feature/test')).rejects.toThrow('Workspace not found');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('attempts to create branch across repos', async () => {
|
|
395
|
+
const wsPath = path.join(tempDir, 'branch-ws');
|
|
396
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
397
|
+
await writeMetadata(wsPath, 'branch-ws', [REPO_META('repo1')]);
|
|
398
|
+
|
|
399
|
+
// repo1 is not a git repo, so branch creation will fail
|
|
400
|
+
const result = await handleBranchCreate('branch-ws', 'feature/test');
|
|
401
|
+
expect(result.workspace).toBe('branch-ws');
|
|
402
|
+
expect(result.branchName).toBe('feature/test');
|
|
403
|
+
expect(result.results).toHaveLength(1);
|
|
404
|
+
expect(result.summary).toBeDefined();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('handleSwitchBranch', () => {
|
|
409
|
+
it('throws for non-existent workspace', async () => {
|
|
410
|
+
await expect(handleSwitchBranch('non-existent', 'main')).rejects.toThrow('Workspace not found');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('attempts to switch branches across repos', async () => {
|
|
414
|
+
const wsPath = path.join(tempDir, 'switch-ws');
|
|
415
|
+
await fs.mkdir(path.join(wsPath, 'repo1'), { recursive: true });
|
|
416
|
+
await writeMetadata(wsPath, 'switch-ws', [REPO_META('repo1')]);
|
|
417
|
+
|
|
418
|
+
// repo1 is not a git repo, so switch will be skipped
|
|
419
|
+
const result = await handleSwitchBranch('switch-ws', 'main');
|
|
420
|
+
expect(result.workspace).toBe('switch-ws');
|
|
421
|
+
expect(result.branch).toBe('main');
|
|
422
|
+
expect(result.results).toHaveLength(1);
|
|
423
|
+
expect(result.results[0].status).toBe('skipped');
|
|
424
|
+
expect(result.summary.skipped).toBe(1);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('handleWorkspaceCleanup', () => {
|
|
429
|
+
it('throws for non-existent workspace', async () => {
|
|
430
|
+
await expect(handleWorkspaceCleanup('non-existent')).rejects.toThrow('Workspace not found');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('cleans up workspace repos', async () => {
|
|
434
|
+
const wsPath = path.join(tempDir, 'cleanup-ws');
|
|
435
|
+
const repoPath = path.join(wsPath, 'repo1');
|
|
436
|
+
await fs.mkdir(repoPath, { recursive: true });
|
|
437
|
+
|
|
438
|
+
// Create node_modules and dist directories
|
|
439
|
+
await fs.mkdir(path.join(repoPath, 'node_modules'), { recursive: true });
|
|
440
|
+
await fs.writeFile(path.join(repoPath, 'node_modules', 'dummy.js'), 'module.exports = {}');
|
|
441
|
+
await fs.mkdir(path.join(repoPath, 'dist'), { recursive: true });
|
|
442
|
+
await fs.writeFile(path.join(repoPath, 'dist', 'index.js'), 'console.log("built")');
|
|
443
|
+
|
|
444
|
+
await writeMetadata(wsPath, 'cleanup-ws', [REPO_META('repo1')]);
|
|
445
|
+
|
|
446
|
+
const result = await handleWorkspaceCleanup('cleanup-ws');
|
|
447
|
+
expect(result.workspace).toBe('cleanup-ws');
|
|
448
|
+
expect(result.results).toHaveLength(1);
|
|
449
|
+
expect(result.results[0].repo).toBe('repo1');
|
|
450
|
+
expect(result.results[0].nodeModules).toBeDefined();
|
|
451
|
+
expect(result.results[0].buildArtifacts).toBeDefined();
|
|
452
|
+
expect(result.totalSpaceFreed).toBeDefined();
|
|
453
|
+
|
|
454
|
+
// Verify node_modules and dist are removed
|
|
455
|
+
await expect(fs.access(path.join(repoPath, 'node_modules'))).rejects.toThrow();
|
|
456
|
+
await expect(fs.access(path.join(repoPath, 'dist'))).rejects.toThrow();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('respects includeNodeModules=false flag', async () => {
|
|
460
|
+
const wsPath = path.join(tempDir, 'cleanup-ws2');
|
|
461
|
+
const repoPath = path.join(wsPath, 'repo1');
|
|
462
|
+
await fs.mkdir(path.join(repoPath, 'node_modules'), { recursive: true });
|
|
463
|
+
await writeMetadata(wsPath, 'cleanup-ws2', [REPO_META('repo1')]);
|
|
464
|
+
|
|
465
|
+
const result = await handleWorkspaceCleanup('cleanup-ws2', false, true);
|
|
466
|
+
expect(result.results[0].nodeModules).toBeUndefined();
|
|
467
|
+
expect(result.results[0].buildArtifacts).toBeDefined();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe('handleSearchRepos', () => {
|
|
472
|
+
it('returns search results from GitHub repos', async () => {
|
|
473
|
+
// Mock fetchOrgRepos to avoid real API calls
|
|
474
|
+
vi.resetModules();
|
|
475
|
+
vi.doMock('../utils/github', () => ({
|
|
476
|
+
fetchOrgRepos: vi.fn().mockResolvedValue([
|
|
477
|
+
{ name: 'partnerships-api', description: 'API for partnerships', url: 'https://github.com/acme/partnerships-api', sshUrl: 'git@github.com:acme/partnerships-api.git', owner: { login: 'acme' }, isPrivate: true },
|
|
478
|
+
{ name: 'acme-db', description: 'Database layer', url: 'https://github.com/acme/acme-db', sshUrl: 'git@github.com:acme/acme-db.git', owner: { login: 'acme' }, isPrivate: true },
|
|
479
|
+
{ name: 'web-app', description: 'Main web application', url: 'https://github.com/acme/web-app', sshUrl: 'git@github.com:acme/web-app.git', owner: { login: 'acme' }, isPrivate: false },
|
|
480
|
+
]),
|
|
481
|
+
}));
|
|
482
|
+
|
|
483
|
+
const mod = await import('./tools');
|
|
484
|
+
const result = await mod.handleSearchRepos('partner');
|
|
485
|
+
expect(Array.isArray(result)).toBe(true);
|
|
486
|
+
expect(result.length).toBeGreaterThan(0);
|
|
487
|
+
expect(result[0].name).toBe('partnerships-api');
|
|
488
|
+
expect(result[0]).toHaveProperty('description');
|
|
489
|
+
expect(result[0]).toHaveProperty('url');
|
|
490
|
+
expect(result[0]).toHaveProperty('isPrivate');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('respects limit parameter', async () => {
|
|
494
|
+
vi.resetModules();
|
|
495
|
+
vi.doMock('../utils/github', () => ({
|
|
496
|
+
fetchOrgRepos: vi.fn().mockResolvedValue([
|
|
497
|
+
{ name: 'repo-a', description: 'A', url: 'u1', sshUrl: 's1', owner: { login: 'acme' }, isPrivate: false },
|
|
498
|
+
{ name: 'repo-ab', description: 'AB', url: 'u2', sshUrl: 's2', owner: { login: 'acme' }, isPrivate: false },
|
|
499
|
+
{ name: 'repo-abc', description: 'ABC', url: 'u3', sshUrl: 's3', owner: { login: 'acme' }, isPrivate: false },
|
|
500
|
+
]),
|
|
501
|
+
}));
|
|
502
|
+
|
|
503
|
+
const mod = await import('./tools');
|
|
504
|
+
const result = await mod.handleSearchRepos('repo', 1);
|
|
505
|
+
expect(result).toHaveLength(1);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('handleListOrgRepos', () => {
|
|
510
|
+
it('returns all org repos', async () => {
|
|
511
|
+
vi.resetModules();
|
|
512
|
+
vi.doMock('../utils/github', () => ({
|
|
513
|
+
fetchOrgRepos: vi.fn().mockResolvedValue([
|
|
514
|
+
{ name: 'repo1', description: 'First', url: 'u1', sshUrl: 's1', owner: { login: 'acme' }, isPrivate: true },
|
|
515
|
+
{ name: 'repo2', description: 'Second', url: 'u2', sshUrl: 's2', owner: { login: 'acme' }, isPrivate: false },
|
|
516
|
+
]),
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
const mod = await import('./tools');
|
|
520
|
+
const result = await mod.handleListOrgRepos();
|
|
521
|
+
expect(result.repos).toHaveLength(2);
|
|
522
|
+
expect(result.total).toBe(2);
|
|
523
|
+
expect(result.repos[0]).toHaveProperty('name');
|
|
524
|
+
expect(result.repos[0]).toHaveProperty('description');
|
|
525
|
+
expect(result.repos[0]).toHaveProperty('url');
|
|
526
|
+
expect(result.repos[0]).toHaveProperty('isPrivate');
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("path traversal is rejected (security)", () => {
|
|
531
|
+
it("handleWorkspaceStatus refuses a traversal workspace name", async () => {
|
|
532
|
+
await expect(handleWorkspaceStatus("../../etc")).rejects.toThrow(/Invalid workspace name/);
|
|
533
|
+
});
|
|
534
|
+
it("handleRunCommand refuses a traversal name (no command runs outside the sandbox)", async () => {
|
|
535
|
+
await expect(handleRunCommand("../../tmp", "echo pwned")).rejects.toThrow(/Invalid workspace name/);
|
|
536
|
+
});
|
|
537
|
+
it("handleDeleteWorkspace refuses a traversal name (no rm outside the sandbox)", async () => {
|
|
538
|
+
await expect(handleDeleteWorkspace("../../Documents")).rejects.toThrow(/Invalid workspace name/);
|
|
539
|
+
await expect(handleDeleteWorkspace(["ok-name", "../../Documents"])).rejects.toThrow(/Invalid workspace name/);
|
|
540
|
+
});
|
|
541
|
+
it("handleRemoveRepo refuses a traversal workspace name", async () => {
|
|
542
|
+
await expect(handleRemoveRepo("../../etc", "repo")).rejects.toThrow(/Invalid workspace name/);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|