magus-cli 0.0.0-placeholder → 7.0.0
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/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
beforeAll,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
test,
|
|
8
|
+
} from "bun:test";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import {
|
|
14
|
+
clearWorktreeCache,
|
|
15
|
+
inheritablePaths,
|
|
16
|
+
} from "../services/git-worktree.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Exercised against real repositories rather than a mocked `git`, because the
|
|
20
|
+
* whole point of the function is agreeing with git about what a worktree is.
|
|
21
|
+
* A stub would have happily confirmed a wrong parse of `worktree list`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Isolate from the operator's git config: a global `core.excludesFile`,
|
|
25
|
+
// `init.defaultBranch`, or commit signing would otherwise leak into these repos.
|
|
26
|
+
const GIT_CFG = [
|
|
27
|
+
"-c",
|
|
28
|
+
"user.name=test",
|
|
29
|
+
"-c",
|
|
30
|
+
"user.email=test@example.com",
|
|
31
|
+
"-c",
|
|
32
|
+
"commit.gpgsign=false",
|
|
33
|
+
"-c",
|
|
34
|
+
"init.defaultBranch=main",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const git = (cwd: string, ...args: string[]) =>
|
|
38
|
+
execFileSync("git", [...GIT_CFG, ...args], { cwd, encoding: "utf8" }).trim();
|
|
39
|
+
|
|
40
|
+
let base: string;
|
|
41
|
+
let mainTree: string;
|
|
42
|
+
let linked: string;
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
// realpath: on macOS os.tmpdir() is a symlink, and git reports real paths.
|
|
46
|
+
base = await fs.realpath(
|
|
47
|
+
await fs.mkdtemp(path.join(os.tmpdir(), "magus-worktree-")),
|
|
48
|
+
);
|
|
49
|
+
mainTree = path.join(base, "repo");
|
|
50
|
+
linked = path.join(base, "linked");
|
|
51
|
+
|
|
52
|
+
await fs.ensureDir(mainTree);
|
|
53
|
+
git(mainTree, "init", "-q");
|
|
54
|
+
await fs.writeFile(path.join(mainTree, "README.md"), "hello\n");
|
|
55
|
+
git(mainTree, "add", "-A");
|
|
56
|
+
git(mainTree, "commit", "-qm", "initial");
|
|
57
|
+
git(mainTree, "worktree", "add", "-q", "-b", "feature", linked);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
if (base) await fs.remove(base);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
clearWorktreeCache();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("inheritablePaths", () => {
|
|
69
|
+
test("a linked worktree inherits from the main working tree", async () => {
|
|
70
|
+
expect(await inheritablePaths(linked)).toEqual([mainTree]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the main working tree inherits nothing — it holds its own rows", async () => {
|
|
74
|
+
expect(await inheritablePaths(mainTree)).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a subdirectory of a checkout inherits from the repository root", async () => {
|
|
78
|
+
const sub = path.join(mainTree, "packages", "app");
|
|
79
|
+
await fs.ensureDir(sub);
|
|
80
|
+
expect(await inheritablePaths(sub)).toEqual([mainTree]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a subdirectory of a linked worktree inherits both roots, nearest first", async () => {
|
|
84
|
+
const sub = path.join(linked, "packages", "app");
|
|
85
|
+
await fs.ensureDir(sub);
|
|
86
|
+
expect(await inheritablePaths(sub)).toEqual([linked, mainTree]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a directory outside any repository inherits nothing", async () => {
|
|
90
|
+
const loose = path.join(base, "not-a-repo");
|
|
91
|
+
await fs.ensureDir(loose);
|
|
92
|
+
// Guard: a stray repo above the temp dir would silently invalidate this.
|
|
93
|
+
const out = await inheritablePaths(loose);
|
|
94
|
+
expect(out.every((p) => !p.startsWith(base))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a missing directory yields nothing rather than throwing", async () => {
|
|
98
|
+
expect(await inheritablePaths(path.join(base, "gone"))).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("results are cached per path and cleared on demand", async () => {
|
|
102
|
+
const first = await inheritablePaths(linked);
|
|
103
|
+
expect(await inheritablePaths(linked)).toBe(first); // same array identity
|
|
104
|
+
clearWorktreeCache();
|
|
105
|
+
expect(await inheritablePaths(linked)).not.toBe(first);
|
|
106
|
+
expect(await inheritablePaths(linked)).toEqual([mainTree]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
activeCooldowns,
|
|
4
|
+
cooldownFor,
|
|
5
|
+
describeCooldown,
|
|
6
|
+
formatWait,
|
|
7
|
+
hostOf,
|
|
8
|
+
recordRateLimit,
|
|
9
|
+
recordSuccess,
|
|
10
|
+
remainingFromHeaders,
|
|
11
|
+
resetGitHubBudget,
|
|
12
|
+
waitMs,
|
|
13
|
+
} from "../services/github-budget";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The design hinges on one measured asymmetry (2026-08-17):
|
|
17
|
+
*
|
|
18
|
+
* api.github.com 429/403 carries x-ratelimit-reset and retry-after
|
|
19
|
+
* raw.githubusercontent.com 429 carries NEITHER — only a request id and a date
|
|
20
|
+
*
|
|
21
|
+
* So the module must be able to say "resets in 4m" when GitHub said so, and must
|
|
22
|
+
* NOT say it otherwise. Fabricating a reset time would be the same sin as the
|
|
23
|
+
* original bug: reporting a guess as a fact.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NOW = 1_786_980_000_000;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetGitHubBudget();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("hostOf", () => {
|
|
33
|
+
it("recognises the two budgeted hosts and nothing else", () => {
|
|
34
|
+
expect(hostOf("https://api.github.com/repos/x/y")).toBe("api.github.com");
|
|
35
|
+
expect(hostOf("https://raw.githubusercontent.com/x/y/main/f.json")).toBe(
|
|
36
|
+
"raw.githubusercontent.com",
|
|
37
|
+
);
|
|
38
|
+
expect(hostOf("https://github.com/x/y")).toBeNull();
|
|
39
|
+
expect(hostOf("not a url")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("recordRateLimit — exact timing when GitHub provides it", () => {
|
|
44
|
+
it("honours retry-after in seconds", () => {
|
|
45
|
+
const cooldown = recordRateLimit(
|
|
46
|
+
"api.github.com",
|
|
47
|
+
new Headers({ "retry-after": "120" }),
|
|
48
|
+
NOW,
|
|
49
|
+
);
|
|
50
|
+
expect(cooldown.exact).toBe(true);
|
|
51
|
+
expect(cooldown.until).toBe(NOW + 120_000);
|
|
52
|
+
expect(describeCooldown(cooldown, NOW)).toBe(
|
|
53
|
+
"GitHub rate limit resets in 2m",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("honours retry-after as an HTTP date", () => {
|
|
58
|
+
const when = new Date(NOW + 300_000).toUTCString();
|
|
59
|
+
const cooldown = recordRateLimit(
|
|
60
|
+
"api.github.com",
|
|
61
|
+
new Headers({ "retry-after": when }),
|
|
62
|
+
NOW,
|
|
63
|
+
);
|
|
64
|
+
expect(cooldown.exact).toBe(true);
|
|
65
|
+
// UTCString drops sub-second precision, so allow a second of slack.
|
|
66
|
+
expect(Math.abs(cooldown.until - (NOW + 300_000))).toBeLessThanOrEqual(
|
|
67
|
+
1000,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reads x-ratelimit-reset as epoch SECONDS, not milliseconds", () => {
|
|
72
|
+
// Treating it as ms lands in 1970, so the cooldown expires instantly and the
|
|
73
|
+
// UI reports "not rate limited" while GitHub is still refusing every call.
|
|
74
|
+
const resetEpochSeconds = Math.floor(NOW / 1000) + 600;
|
|
75
|
+
const cooldown = recordRateLimit(
|
|
76
|
+
"api.github.com",
|
|
77
|
+
new Headers({ "x-ratelimit-reset": String(resetEpochSeconds) }),
|
|
78
|
+
NOW,
|
|
79
|
+
);
|
|
80
|
+
expect(cooldown.exact).toBe(true);
|
|
81
|
+
expect(cooldown.until).toBe(resetEpochSeconds * 1000);
|
|
82
|
+
expect(waitMs("api.github.com", NOW)).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("caps an implausible server window so a bad header cannot wedge the UI", () => {
|
|
86
|
+
const cooldown = recordRateLimit(
|
|
87
|
+
"api.github.com",
|
|
88
|
+
new Headers({ "retry-after": String(60 * 60 * 24 * 30) }),
|
|
89
|
+
NOW,
|
|
90
|
+
);
|
|
91
|
+
expect(cooldown.until - NOW).toBeLessThanOrEqual(60 * 60_000);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("recordRateLimit — backoff when GitHub provides nothing", () => {
|
|
96
|
+
it("does not claim a reset time it was never told", () => {
|
|
97
|
+
// This is the raw.githubusercontent.com case, measured: a bare 429.
|
|
98
|
+
const cooldown = recordRateLimit(
|
|
99
|
+
"raw.githubusercontent.com",
|
|
100
|
+
new Headers(),
|
|
101
|
+
NOW,
|
|
102
|
+
);
|
|
103
|
+
expect(cooldown.exact).toBe(false);
|
|
104
|
+
expect(describeCooldown(cooldown, NOW)).toContain("next attempt in");
|
|
105
|
+
expect(describeCooldown(cooldown, NOW)).not.toContain("resets in");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("backs off further on each consecutive failure", () => {
|
|
109
|
+
const first = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
|
|
110
|
+
const second = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
|
|
111
|
+
const third = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
|
|
112
|
+
expect(second.until).toBeGreaterThan(first.until);
|
|
113
|
+
expect(third.until).toBeGreaterThan(second.until);
|
|
114
|
+
expect(third.strikes).toBe(3);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("ignores a retry-after that is garbage or already past", () => {
|
|
118
|
+
const garbage = recordRateLimit(
|
|
119
|
+
"raw.githubusercontent.com",
|
|
120
|
+
new Headers({ "retry-after": "soon-ish" }),
|
|
121
|
+
NOW,
|
|
122
|
+
);
|
|
123
|
+
expect(garbage.exact).toBe(false);
|
|
124
|
+
expect(garbage.until).toBeGreaterThan(NOW);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("the gate", () => {
|
|
129
|
+
it("blocks while cooling and clears when the window passes", () => {
|
|
130
|
+
const cooldown = recordRateLimit(
|
|
131
|
+
"raw.githubusercontent.com",
|
|
132
|
+
undefined,
|
|
133
|
+
NOW,
|
|
134
|
+
);
|
|
135
|
+
expect(cooldownFor("raw.githubusercontent.com", NOW)).not.toBeNull();
|
|
136
|
+
expect(
|
|
137
|
+
cooldownFor("raw.githubusercontent.com", cooldown.until + 1),
|
|
138
|
+
).toBeNull();
|
|
139
|
+
expect(waitMs("raw.githubusercontent.com", cooldown.until + 1)).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps the two hosts on separate budgets", () => {
|
|
143
|
+
// They are separate buckets upstream. Blocking raw because the API is
|
|
144
|
+
// exhausted would stop work that would have succeeded.
|
|
145
|
+
recordRateLimit(
|
|
146
|
+
"api.github.com",
|
|
147
|
+
new Headers({ "retry-after": "60" }),
|
|
148
|
+
NOW,
|
|
149
|
+
);
|
|
150
|
+
expect(cooldownFor("api.github.com", NOW)).not.toBeNull();
|
|
151
|
+
expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("clears the cooldown and the strike count on success", () => {
|
|
155
|
+
// Without resetting strikes, one bad afternoon pins the backoff at ten
|
|
156
|
+
// minutes for the rest of the session even though calls are succeeding.
|
|
157
|
+
recordRateLimit("raw.githubusercontent.com", undefined, NOW);
|
|
158
|
+
recordRateLimit("raw.githubusercontent.com", undefined, NOW);
|
|
159
|
+
recordSuccess("raw.githubusercontent.com");
|
|
160
|
+
expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
|
|
161
|
+
|
|
162
|
+
const afterRecovery = recordRateLimit(
|
|
163
|
+
"raw.githubusercontent.com",
|
|
164
|
+
undefined,
|
|
165
|
+
NOW,
|
|
166
|
+
);
|
|
167
|
+
expect(afterRecovery.strikes).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("lists active cooldowns soonest first", () => {
|
|
171
|
+
recordRateLimit(
|
|
172
|
+
"api.github.com",
|
|
173
|
+
new Headers({ "retry-after": "600" }),
|
|
174
|
+
NOW,
|
|
175
|
+
);
|
|
176
|
+
recordRateLimit(
|
|
177
|
+
"raw.githubusercontent.com",
|
|
178
|
+
new Headers({ "retry-after": "30" }),
|
|
179
|
+
NOW,
|
|
180
|
+
);
|
|
181
|
+
const active = activeCooldowns(NOW);
|
|
182
|
+
expect(active.map((c) => c.host)).toEqual([
|
|
183
|
+
"raw.githubusercontent.com",
|
|
184
|
+
"api.github.com",
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("remainingFromHeaders", () => {
|
|
190
|
+
it("reads the budget so a caller can stop before tripping the limit", () => {
|
|
191
|
+
expect(
|
|
192
|
+
remainingFromHeaders(new Headers({ "x-ratelimit-remaining": "51" })),
|
|
193
|
+
).toBe(51);
|
|
194
|
+
expect(remainingFromHeaders(new Headers())).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("formatWait", () => {
|
|
199
|
+
it("reads naturally at every scale", () => {
|
|
200
|
+
expect(formatWait(0)).toBe("0s");
|
|
201
|
+
expect(formatWait(45_000)).toBe("45s");
|
|
202
|
+
expect(formatWait(90_000)).toBe("2m");
|
|
203
|
+
expect(formatWait(3_600_000)).toBe("1h 0m");
|
|
204
|
+
expect(formatWait(4_320_000)).toBe("1h 12m");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults.js";
|
|
7
|
+
import {
|
|
8
|
+
checkGitignore,
|
|
9
|
+
formatGitignoreWarning,
|
|
10
|
+
} from "../services/gitignore-check";
|
|
11
|
+
|
|
12
|
+
function git(cwd: string, ...args: string[]): void {
|
|
13
|
+
spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("checkGitignore", () => {
|
|
17
|
+
let projectDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-check-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns 0 violations for non-git directory", async () => {
|
|
28
|
+
const r = await checkGitignore(projectDir);
|
|
29
|
+
expect(r.violationCount).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("flags missing built-in patterns inside a fresh git repo", async () => {
|
|
33
|
+
git(projectDir, "init", "-q");
|
|
34
|
+
const r = await checkGitignore(projectDir);
|
|
35
|
+
// BUILTIN_DEFAULTS includes ".mnemex/" etc. — none are tracked, none in
|
|
36
|
+
// .gitignore, so each one becomes "missing-from-gitignore".
|
|
37
|
+
expect(r.violationCount).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns 0 when .gitignore covers all built-in patterns and tracked files match track rules", async () => {
|
|
41
|
+
git(projectDir, "init", "-q");
|
|
42
|
+
git(projectDir, "config", "user.email", "t@t");
|
|
43
|
+
git(projectDir, "config", "user.name", "t");
|
|
44
|
+
// Suppress all built-in ignore rules. DERIVED from the constant, not a
|
|
45
|
+
// hand-copied list: a copy silently becomes a different assertion the
|
|
46
|
+
// moment a pattern is added, and then fails for a reason that has nothing
|
|
47
|
+
// to do with what this test is about.
|
|
48
|
+
await writeFile(
|
|
49
|
+
join(projectDir, ".gitignore"),
|
|
50
|
+
`${BUILTIN_DEFAULTS.ignore.join("\n")}\n`,
|
|
51
|
+
);
|
|
52
|
+
// Tracked files don't have to exist for the absent-track-path case to be ignored
|
|
53
|
+
const r = await checkGitignore(projectDir);
|
|
54
|
+
expect(r.violationCount).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("formatGitignoreWarning", () => {
|
|
59
|
+
it("returns null when no violations", () => {
|
|
60
|
+
expect(
|
|
61
|
+
formatGitignoreWarning({ violationCount: 0, warnings: [] }),
|
|
62
|
+
).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns a warning string when violations exist", () => {
|
|
66
|
+
const w = formatGitignoreWarning({ violationCount: 3, warnings: [] });
|
|
67
|
+
expect(w).not.toBeNull();
|
|
68
|
+
expect(w).toContain("3 git state");
|
|
69
|
+
expect(w).toContain("Git State tab");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { detectViolations } from "../services/gitignore-detector";
|
|
7
|
+
import type { ResolvedManifest } from "../types/gitignore";
|
|
8
|
+
|
|
9
|
+
function git(cwd: string, ...args: string[]): string {
|
|
10
|
+
// core.excludesFile=/dev/null bypasses the developer's global gitignore
|
|
11
|
+
// so the test environment doesn't leak into the result.
|
|
12
|
+
//
|
|
13
|
+
// commit.gpgsign=false is the same isolation, one step further: a developer
|
|
14
|
+
// with signed commits makes every `git commit` here call their signer. With
|
|
15
|
+
// 1Password's SSH agent that means a prompt or an agent error, so these tests
|
|
16
|
+
// failed or timed out locally for reasons that have nothing to do with the
|
|
17
|
+
// code under test. CI has no signing config, so it only ever broke locally.
|
|
18
|
+
const r = spawnSync(
|
|
19
|
+
"git",
|
|
20
|
+
[
|
|
21
|
+
"-c",
|
|
22
|
+
"core.excludesFile=/dev/null",
|
|
23
|
+
"-c",
|
|
24
|
+
"commit.gpgsign=false",
|
|
25
|
+
...args,
|
|
26
|
+
],
|
|
27
|
+
{
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
if (r.status !== 0) {
|
|
33
|
+
throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`);
|
|
34
|
+
}
|
|
35
|
+
return r.stdout;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function makeRepo(): Promise<string> {
|
|
39
|
+
const dir = await mkdtemp(join(tmpdir(), "gitig-det-"));
|
|
40
|
+
git(dir, "init", "-q");
|
|
41
|
+
git(dir, "config", "user.email", "test@test");
|
|
42
|
+
git(dir, "config", "user.name", "test");
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function manifest(rules: ResolvedManifest["rules"]): ResolvedManifest {
|
|
47
|
+
return { rules, conflicts: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("detectViolations", () => {
|
|
51
|
+
let repo: string;
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
repo = await makeRepo();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
await rm(repo, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns empty for non-git directory", async () => {
|
|
62
|
+
const tmp = await mkdtemp(join(tmpdir(), "gitig-nogit-"));
|
|
63
|
+
try {
|
|
64
|
+
const r = await detectViolations(
|
|
65
|
+
tmp,
|
|
66
|
+
manifest([{ pattern: ".env", action: "ignore", source: "builtin" }]),
|
|
67
|
+
);
|
|
68
|
+
expect(r).toEqual([]);
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(tmp, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("flags tracked-but-should-ignore for a tracked file", async () => {
|
|
75
|
+
await writeFile(join(repo, "secret.txt"), "x");
|
|
76
|
+
git(repo, "add", "secret.txt");
|
|
77
|
+
git(repo, "commit", "-q", "-m", "add secret");
|
|
78
|
+
|
|
79
|
+
const r = await detectViolations(
|
|
80
|
+
repo,
|
|
81
|
+
manifest([
|
|
82
|
+
{ pattern: "secret.txt", action: "ignore", source: "project" },
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
expect(r.length).toBe(1);
|
|
86
|
+
expect(r[0].kind).toBe("tracked-but-should-ignore");
|
|
87
|
+
expect(r[0].severity).toBe("destructive");
|
|
88
|
+
expect(r[0].path).toBe("secret.txt");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("flags missing-from-gitignore when path neither tracked nor in .gitignore", async () => {
|
|
92
|
+
const r = await detectViolations(
|
|
93
|
+
repo,
|
|
94
|
+
manifest([{ pattern: ".env", action: "ignore", source: "builtin" }]),
|
|
95
|
+
);
|
|
96
|
+
expect(r.length).toBe(1);
|
|
97
|
+
expect(r[0].kind).toBe("missing-from-gitignore");
|
|
98
|
+
expect(r[0].severity).toBe("safe");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does NOT flag missing-from-gitignore when .gitignore already covers the path", async () => {
|
|
102
|
+
await writeFile(join(repo, ".gitignore"), ".env\n");
|
|
103
|
+
const r = await detectViolations(
|
|
104
|
+
repo,
|
|
105
|
+
manifest([{ pattern: ".env", action: "ignore", source: "builtin" }]),
|
|
106
|
+
);
|
|
107
|
+
expect(r).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("flags ignored-but-should-track when track rule is excluded by .gitignore", async () => {
|
|
111
|
+
await writeFile(join(repo, ".gitignore"), ".mcp.json\n");
|
|
112
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
113
|
+
const r = await detectViolations(
|
|
114
|
+
repo,
|
|
115
|
+
manifest([{ pattern: ".mcp.json", action: "track", source: "builtin" }]),
|
|
116
|
+
);
|
|
117
|
+
expect(r.length).toBe(1);
|
|
118
|
+
expect(r[0].kind).toBe("ignored-but-should-track");
|
|
119
|
+
expect(r[0].severity).toBe("destructive");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("flags untracked-and-should-track when path exists but isn't tracked", async () => {
|
|
123
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
124
|
+
const r = await detectViolations(
|
|
125
|
+
repo,
|
|
126
|
+
manifest([{ pattern: ".mcp.json", action: "track", source: "builtin" }]),
|
|
127
|
+
);
|
|
128
|
+
expect(r.length).toBe(1);
|
|
129
|
+
expect(r[0].kind).toBe("untracked-and-should-track");
|
|
130
|
+
expect(r[0].severity).toBe("destructive");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does NOT flag track rule when path is already tracked", async () => {
|
|
134
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
135
|
+
git(repo, "add", ".mcp.json");
|
|
136
|
+
git(repo, "commit", "-q", "-m", "add mcp");
|
|
137
|
+
const r = await detectViolations(
|
|
138
|
+
repo,
|
|
139
|
+
manifest([{ pattern: ".mcp.json", action: "track", source: "builtin" }]),
|
|
140
|
+
);
|
|
141
|
+
expect(r).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("matches directory patterns against tracked subpaths", async () => {
|
|
145
|
+
await mkdir(join(repo, "ai-docs", "sessions"), { recursive: true });
|
|
146
|
+
await writeFile(join(repo, "ai-docs", "sessions", "leak.md"), "x");
|
|
147
|
+
git(repo, "add", "ai-docs/sessions/leak.md");
|
|
148
|
+
git(repo, "commit", "-q", "-m", "leak");
|
|
149
|
+
|
|
150
|
+
const r = await detectViolations(
|
|
151
|
+
repo,
|
|
152
|
+
manifest([
|
|
153
|
+
{ pattern: "ai-docs/sessions/", action: "ignore", source: "builtin" },
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
expect(r.length).toBeGreaterThan(0);
|
|
157
|
+
expect(r[0].kind).toBe("tracked-but-should-ignore");
|
|
158
|
+
expect(r[0].path).toBe("ai-docs/sessions/leak.md");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns no violations when manifest is satisfied", async () => {
|
|
162
|
+
await writeFile(join(repo, ".gitignore"), ".env\n");
|
|
163
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
164
|
+
git(repo, "add", ".gitignore", ".mcp.json");
|
|
165
|
+
git(repo, "commit", "-q", "-m", "init");
|
|
166
|
+
|
|
167
|
+
const r = await detectViolations(
|
|
168
|
+
repo,
|
|
169
|
+
manifest([
|
|
170
|
+
{ pattern: ".env", action: "ignore", source: "builtin" },
|
|
171
|
+
{ pattern: ".mcp.json", action: "track", source: "builtin" },
|
|
172
|
+
]),
|
|
173
|
+
);
|
|
174
|
+
expect(r).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
});
|