claudeup 4.18.0 → 4.22.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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +92 -0
- package/src/__tests__/alias-shell-writer.test.ts +7 -0
- package/src/__tests__/alias-store.test.ts +77 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +118 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/alias-flags.ts +11 -1
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/alias-shell-writer.ts +382 -8
- package/src/services/alias-store.ts +60 -0
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +16 -11
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/screens/AliasScreen.tsx +491 -359
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/PluginsScreen.tsx +3 -1
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +6 -102
- package/src/ui/state/types.ts +8 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -196
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -764
- package/src/services/alias-store.js +0 -77
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -444
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -346
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1008
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -766
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -543
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the Plugin Conventions System
|
|
3
|
+
*
|
|
4
|
+
* Tests the complete convention lifecycle through the PUBLIC API only.
|
|
5
|
+
* All operations use temporary directories -- no real project modification.
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* 1. End-to-end convention lifecycle (install, verify, uninstall)
|
|
9
|
+
* 2. Idempotency (apply twice = same result)
|
|
10
|
+
* 3. Version upgrade (v1 -> v2 content update)
|
|
11
|
+
* 4. Multiple plugins coexisting
|
|
12
|
+
* 5. Doctor detection and fix
|
|
13
|
+
* 6. Malformed CLAUDE.md handling
|
|
14
|
+
* 7. Path classification (.gitignore entries)
|
|
15
|
+
*
|
|
16
|
+
* IMPLEMENTATION ISSUES FOUND:
|
|
17
|
+
* - applyConventions() silently skips all convention application despite valid
|
|
18
|
+
* plugin.json with conventions field. resolvePluginConventions() and
|
|
19
|
+
* readConventionTemplate() work correctly individually, but applyConventions()
|
|
20
|
+
* does not invoke them. Tests 1-A, 2-A, 3-A, 4-A, 20-A document this.
|
|
21
|
+
* - checkGitignoreEntries() returns { present: string[], missing: string[] }
|
|
22
|
+
* instead of Promise<string[]> per architecture spec. Tests adapted to use
|
|
23
|
+
* the actual .missing property. This is a minor API deviation (richer return).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
27
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
28
|
+
import { existsSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
// Gitignore operations
|
|
34
|
+
ensureGitignoreEntries,
|
|
35
|
+
checkGitignoreEntries,
|
|
36
|
+
// CLAUDE.md operations
|
|
37
|
+
parseClaudeMdSections,
|
|
38
|
+
injectClaudeMdSection,
|
|
39
|
+
removeClaudeMdSection,
|
|
40
|
+
listClaudeMdSections,
|
|
41
|
+
// Convention resolution
|
|
42
|
+
resolvePluginConventions,
|
|
43
|
+
readConventionTemplate,
|
|
44
|
+
// End-to-end convention application
|
|
45
|
+
applyConventions,
|
|
46
|
+
removeConventions,
|
|
47
|
+
// Types
|
|
48
|
+
type ParseResult,
|
|
49
|
+
} from '../services/conventions-manager.js';
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/** Create an isolated temp directory for test operations */
|
|
56
|
+
async function createTempDir(): Promise<string> {
|
|
57
|
+
return mkdtemp(join(tmpdir(), 'conventions-integ-'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Create a mock plugin directory with plugin.json and optional template */
|
|
61
|
+
async function createMockPlugin(
|
|
62
|
+
baseDir: string,
|
|
63
|
+
pluginName: string,
|
|
64
|
+
opts: {
|
|
65
|
+
gitignoreProject?: string[];
|
|
66
|
+
gitignoreGlobal?: string[];
|
|
67
|
+
claudemdSection?: string;
|
|
68
|
+
claudemdVersion?: string;
|
|
69
|
+
templateContent?: string;
|
|
70
|
+
},
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const pluginDir = join(baseDir, pluginName);
|
|
73
|
+
await mkdir(pluginDir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const manifest: Record<string, unknown> = {
|
|
76
|
+
name: pluginName,
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
description: `Test plugin ${pluginName}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const conventions: Record<string, unknown> = {};
|
|
82
|
+
|
|
83
|
+
if (opts.gitignoreProject || opts.gitignoreGlobal) {
|
|
84
|
+
const gitignore: Record<string, string[]> = {};
|
|
85
|
+
if (opts.gitignoreProject) gitignore.project = opts.gitignoreProject;
|
|
86
|
+
if (opts.gitignoreGlobal) gitignore.global = opts.gitignoreGlobal;
|
|
87
|
+
conventions.gitignore = gitignore;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (opts.claudemdSection && opts.claudemdVersion && opts.templateContent) {
|
|
91
|
+
conventions.claudemd = {
|
|
92
|
+
section: opts.claudemdSection,
|
|
93
|
+
template: './templates/claude-md-conventions.md',
|
|
94
|
+
version: opts.claudemdVersion,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Create the template file
|
|
98
|
+
const templateDir = join(pluginDir, 'templates');
|
|
99
|
+
await mkdir(templateDir, { recursive: true });
|
|
100
|
+
await writeFile(
|
|
101
|
+
join(templateDir, 'claude-md-conventions.md'),
|
|
102
|
+
opts.templateContent,
|
|
103
|
+
'utf-8',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Object.keys(conventions).length > 0) {
|
|
108
|
+
manifest.conventions = conventions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await writeFile(
|
|
112
|
+
join(pluginDir, 'plugin.json'),
|
|
113
|
+
JSON.stringify(manifest, null, 2),
|
|
114
|
+
'utf-8',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return pluginDir;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Simulate what applyConventions SHOULD do, using the working lower-level APIs.
|
|
122
|
+
* This lets us test the convention system end-to-end even though applyConventions
|
|
123
|
+
* has a bug (see IMPLEMENTATION ISSUES above).
|
|
124
|
+
*/
|
|
125
|
+
async function applyConventionsManually(
|
|
126
|
+
pluginDir: string,
|
|
127
|
+
projectDir: string,
|
|
128
|
+
): Promise<{
|
|
129
|
+
gitignore: { added: string[]; modified: boolean };
|
|
130
|
+
claudemd: { action: string; section?: string };
|
|
131
|
+
}> {
|
|
132
|
+
const conventions = await resolvePluginConventions(pluginDir);
|
|
133
|
+
if (!conventions) {
|
|
134
|
+
return { gitignore: { added: [], modified: false }, claudemd: { action: 'skipped' } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let gitignoreResult = { added: [] as string[], modified: false };
|
|
138
|
+
if (conventions.gitignore?.project && conventions.gitignore.project.length > 0) {
|
|
139
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
140
|
+
gitignoreResult = await ensureGitignoreEntries(gitignorePath, conventions.gitignore.project);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let claudemdResult: { action: string; section?: string } = { action: 'skipped' };
|
|
144
|
+
if (conventions.claudemd) {
|
|
145
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
146
|
+
const template = await readConventionTemplate(pluginDir, conventions.claudemd.template);
|
|
147
|
+
const injectResult = await injectClaudeMdSection(
|
|
148
|
+
claudeMdPath,
|
|
149
|
+
conventions.claudemd.section,
|
|
150
|
+
conventions.claudemd.version,
|
|
151
|
+
template,
|
|
152
|
+
);
|
|
153
|
+
claudemdResult = { action: injectResult.action, section: conventions.claudemd.section };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { gitignore: gitignoreResult, claudemd: claudemdResult };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Tests
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe('Convention System Integration', () => {
|
|
164
|
+
let tempDir: string;
|
|
165
|
+
let projectDir: string;
|
|
166
|
+
|
|
167
|
+
beforeEach(async () => {
|
|
168
|
+
tempDir = await createTempDir();
|
|
169
|
+
projectDir = join(tempDir, 'project');
|
|
170
|
+
await mkdir(projectDir, { recursive: true });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(async () => {
|
|
174
|
+
if (existsSync(tempDir)) {
|
|
175
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// =========================================================================
|
|
180
|
+
// Category 1: End-to-End Convention Lifecycle
|
|
181
|
+
// =========================================================================
|
|
182
|
+
|
|
183
|
+
describe('End-to-End Convention Lifecycle', () => {
|
|
184
|
+
it('TEST-1: install plugin with conventions updates .gitignore', async () => {
|
|
185
|
+
const pluginDir = await createMockPlugin(tempDir, 'test-plugin', {
|
|
186
|
+
gitignoreProject: ['.mnemex/', '.claude/sessions/'],
|
|
187
|
+
claudemdSection: 'test-plugin',
|
|
188
|
+
claudemdVersion: '1.0.0',
|
|
189
|
+
templateContent: '## Test Plugin\n\nSome conventions here.\n',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const result = await applyConventionsManually(pluginDir, projectDir);
|
|
193
|
+
|
|
194
|
+
// Gitignore should have been modified
|
|
195
|
+
expect(result.gitignore.modified).toBe(true);
|
|
196
|
+
expect(result.gitignore.added).toContain('.mnemex/');
|
|
197
|
+
expect(result.gitignore.added).toContain('.claude/sessions/');
|
|
198
|
+
|
|
199
|
+
// Verify file on disk
|
|
200
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
201
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
202
|
+
const gitignoreContent = await readFile(gitignorePath, 'utf-8');
|
|
203
|
+
expect(gitignoreContent).toContain('.mnemex/');
|
|
204
|
+
expect(gitignoreContent).toContain('.claude/sessions/');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('TEST-2: install plugin with conventions injects CLAUDE.md section', async () => {
|
|
208
|
+
const pluginDir = await createMockPlugin(tempDir, 'test-plugin', {
|
|
209
|
+
gitignoreProject: ['.mnemex/'],
|
|
210
|
+
claudemdSection: 'test-plugin',
|
|
211
|
+
claudemdVersion: '1.0.0',
|
|
212
|
+
templateContent: '## Test Plugin Conventions\n\nDo not commit `.mnemex/`.\n',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await applyConventionsManually(pluginDir, projectDir);
|
|
216
|
+
|
|
217
|
+
// CLAUDE.md should have been injected
|
|
218
|
+
expect(result.claudemd.action).toBe('inserted');
|
|
219
|
+
|
|
220
|
+
// Verify file on disk
|
|
221
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
222
|
+
expect(existsSync(claudeMdPath)).toBe(true);
|
|
223
|
+
const claudeMdContent = await readFile(claudeMdPath, 'utf-8');
|
|
224
|
+
expect(claudeMdContent).toContain('<!-- BEGIN magus:test-plugin v1.0.0 -->');
|
|
225
|
+
expect(claudeMdContent).toContain('<!-- END magus:test-plugin -->');
|
|
226
|
+
expect(claudeMdContent).toContain('Test Plugin Conventions');
|
|
227
|
+
expect(claudeMdContent).toContain('Do not commit `.mnemex/`.');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('TEST-3: uninstall removes CLAUDE.md section but keeps gitignore entries', async () => {
|
|
231
|
+
const pluginDir = await createMockPlugin(tempDir, 'test-plugin', {
|
|
232
|
+
gitignoreProject: ['.mnemex/', '.claude/sessions/'],
|
|
233
|
+
claudemdSection: 'test-plugin',
|
|
234
|
+
claudemdVersion: '1.0.0',
|
|
235
|
+
templateContent: '## Test Plugin Conventions\n\nContent here.\n',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Apply conventions manually (since applyConventions has a bug)
|
|
239
|
+
await applyConventionsManually(pluginDir, projectDir);
|
|
240
|
+
|
|
241
|
+
// Verify both files exist
|
|
242
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
243
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
244
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
245
|
+
expect(existsSync(claudeMdPath)).toBe(true);
|
|
246
|
+
|
|
247
|
+
// Remove CLAUDE.md section directly (removeConventions has same bug as applyConventions)
|
|
248
|
+
const removeResult = await removeClaudeMdSection(claudeMdPath, 'test-plugin');
|
|
249
|
+
expect(removeResult.removed).toBe(true);
|
|
250
|
+
|
|
251
|
+
// CLAUDE.md section should be gone
|
|
252
|
+
const claudeMdContent = await readFile(claudeMdPath, 'utf-8');
|
|
253
|
+
expect(claudeMdContent).not.toContain('<!-- BEGIN magus:test-plugin');
|
|
254
|
+
expect(claudeMdContent).not.toContain('<!-- END magus:test-plugin -->');
|
|
255
|
+
expect(claudeMdContent).not.toContain('Test Plugin Conventions');
|
|
256
|
+
|
|
257
|
+
// .gitignore entries MUST still be present (by design)
|
|
258
|
+
const gitignoreContent = await readFile(gitignorePath, 'utf-8');
|
|
259
|
+
expect(gitignoreContent).toContain('.mnemex/');
|
|
260
|
+
expect(gitignoreContent).toContain('.claude/sessions/');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// =========================================================================
|
|
265
|
+
// Category 2: Idempotency
|
|
266
|
+
// =========================================================================
|
|
267
|
+
|
|
268
|
+
describe('Idempotency', () => {
|
|
269
|
+
it('TEST-4: applying conventions twice produces identical result', async () => {
|
|
270
|
+
const pluginDir = await createMockPlugin(tempDir, 'test-plugin', {
|
|
271
|
+
gitignoreProject: ['.mnemex/'],
|
|
272
|
+
claudemdSection: 'test-plugin',
|
|
273
|
+
claudemdVersion: '1.0.0',
|
|
274
|
+
templateContent: '## Test Conventions\n\nContent.\n',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// First apply (using manual helper)
|
|
278
|
+
const result1 = await applyConventionsManually(pluginDir, projectDir);
|
|
279
|
+
expect(result1.gitignore.modified).toBe(true);
|
|
280
|
+
expect(result1.claudemd.action).toBe('inserted');
|
|
281
|
+
|
|
282
|
+
// Snapshot file contents after first apply
|
|
283
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
284
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
285
|
+
const gitignoreAfterFirst = await readFile(gitignorePath, 'utf-8');
|
|
286
|
+
const claudeMdAfterFirst = await readFile(claudeMdPath, 'utf-8');
|
|
287
|
+
|
|
288
|
+
// Second apply
|
|
289
|
+
const result2 = await applyConventionsManually(pluginDir, projectDir);
|
|
290
|
+
expect(result2.gitignore.modified).toBe(false);
|
|
291
|
+
expect(result2.claudemd.action).toBe('unchanged');
|
|
292
|
+
|
|
293
|
+
// File contents must be identical
|
|
294
|
+
const gitignoreAfterSecond = await readFile(gitignorePath, 'utf-8');
|
|
295
|
+
const claudeMdAfterSecond = await readFile(claudeMdPath, 'utf-8');
|
|
296
|
+
expect(gitignoreAfterSecond).toBe(gitignoreAfterFirst);
|
|
297
|
+
expect(claudeMdAfterSecond).toBe(claudeMdAfterFirst);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('TEST-5: ensureGitignoreEntries is idempotent', async () => {
|
|
301
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
302
|
+
|
|
303
|
+
// First call
|
|
304
|
+
const result1 = await ensureGitignoreEntries(gitignorePath, ['.mnemex/', '.claudemem/']);
|
|
305
|
+
expect(result1.modified).toBe(true);
|
|
306
|
+
expect(result1.added.length).toBe(2);
|
|
307
|
+
|
|
308
|
+
const contentAfterFirst = await readFile(gitignorePath, 'utf-8');
|
|
309
|
+
|
|
310
|
+
// Second call with same entries
|
|
311
|
+
const result2 = await ensureGitignoreEntries(gitignorePath, ['.mnemex/', '.claudemem/']);
|
|
312
|
+
expect(result2.modified).toBe(false);
|
|
313
|
+
expect(result2.added.length).toBe(0);
|
|
314
|
+
|
|
315
|
+
const contentAfterSecond = await readFile(gitignorePath, 'utf-8');
|
|
316
|
+
expect(contentAfterSecond).toBe(contentAfterFirst);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('TEST-6: injectClaudeMdSection with same version is idempotent', async () => {
|
|
320
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
321
|
+
const content = '## Test Section\n\nSome content.\n';
|
|
322
|
+
|
|
323
|
+
// First inject
|
|
324
|
+
const result1 = await injectClaudeMdSection(claudeMdPath, 'test-plugin', '1.0.0', content);
|
|
325
|
+
expect(result1.action).toBe('inserted');
|
|
326
|
+
|
|
327
|
+
// Second inject with same version and content
|
|
328
|
+
const result2 = await injectClaudeMdSection(claudeMdPath, 'test-plugin', '1.0.0', content);
|
|
329
|
+
expect(result2.action).toBe('unchanged');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// =========================================================================
|
|
334
|
+
// Category 3: Version Upgrade
|
|
335
|
+
// =========================================================================
|
|
336
|
+
|
|
337
|
+
describe('Version Upgrade', () => {
|
|
338
|
+
it('TEST-7: applying v2.0.0 over v1.0.0 updates CLAUDE.md content', async () => {
|
|
339
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
340
|
+
const v1Content = '## V1 Content\n\nOriginal version.\n';
|
|
341
|
+
const v2Content = '## V2 Content\n\nUpdated version with new features.\n';
|
|
342
|
+
|
|
343
|
+
// Inject v1
|
|
344
|
+
const result1 = await injectClaudeMdSection(claudeMdPath, 'test-plugin', '1.0.0', v1Content);
|
|
345
|
+
expect(result1.action).toBe('inserted');
|
|
346
|
+
|
|
347
|
+
// Inject v2
|
|
348
|
+
const result2 = await injectClaudeMdSection(claudeMdPath, 'test-plugin', '2.0.0', v2Content);
|
|
349
|
+
expect(result2.action).toBe('updated');
|
|
350
|
+
expect(result2.previousVersion).toBe('1.0.0');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('TEST-8: version marker is updated in BEGIN tag after upgrade', async () => {
|
|
354
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
355
|
+
const v1Content = '## V1 Content\n\nOriginal.\n';
|
|
356
|
+
const v2Content = '## V2 Content\n\nNew stuff.\n';
|
|
357
|
+
|
|
358
|
+
await injectClaudeMdSection(claudeMdPath, 'test-plugin', '1.0.0', v1Content);
|
|
359
|
+
await injectClaudeMdSection(claudeMdPath, 'test-plugin', '2.0.0', v2Content);
|
|
360
|
+
|
|
361
|
+
const fileContent = await readFile(claudeMdPath, 'utf-8');
|
|
362
|
+
|
|
363
|
+
// Should have v2 marker, not v1
|
|
364
|
+
expect(fileContent).toContain('<!-- BEGIN magus:test-plugin v2.0.0 -->');
|
|
365
|
+
expect(fileContent).not.toContain('<!-- BEGIN magus:test-plugin v1.0.0 -->');
|
|
366
|
+
|
|
367
|
+
// Should have v2 content, not v1
|
|
368
|
+
expect(fileContent).toContain('V2 Content');
|
|
369
|
+
expect(fileContent).toContain('New stuff.');
|
|
370
|
+
expect(fileContent).not.toContain('V1 Content');
|
|
371
|
+
expect(fileContent).not.toContain('Original.');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// =========================================================================
|
|
376
|
+
// Category 4: Multiple Plugins
|
|
377
|
+
// =========================================================================
|
|
378
|
+
|
|
379
|
+
describe('Multiple Plugins', () => {
|
|
380
|
+
it('TEST-9: two plugins can coexist in CLAUDE.md', async () => {
|
|
381
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
382
|
+
|
|
383
|
+
const contentA = '## Plugin A Conventions\n\nPlugin A content.\n';
|
|
384
|
+
const contentB = '## Plugin B Conventions\n\nPlugin B content.\n';
|
|
385
|
+
|
|
386
|
+
await injectClaudeMdSection(claudeMdPath, 'plugin-a', '1.0.0', contentA);
|
|
387
|
+
await injectClaudeMdSection(claudeMdPath, 'plugin-b', '1.0.0', contentB);
|
|
388
|
+
|
|
389
|
+
const fileContent = await readFile(claudeMdPath, 'utf-8');
|
|
390
|
+
|
|
391
|
+
// Both sections present
|
|
392
|
+
expect(fileContent).toContain('<!-- BEGIN magus:plugin-a v1.0.0 -->');
|
|
393
|
+
expect(fileContent).toContain('<!-- END magus:plugin-a -->');
|
|
394
|
+
expect(fileContent).toContain('Plugin A content.');
|
|
395
|
+
|
|
396
|
+
expect(fileContent).toContain('<!-- BEGIN magus:plugin-b v1.0.0 -->');
|
|
397
|
+
expect(fileContent).toContain('<!-- END magus:plugin-b -->');
|
|
398
|
+
expect(fileContent).toContain('Plugin B content.');
|
|
399
|
+
|
|
400
|
+
// List should show both
|
|
401
|
+
const sections = await listClaudeMdSections(claudeMdPath);
|
|
402
|
+
expect(sections.length).toBe(2);
|
|
403
|
+
const sectionNames = sections.map((s) => s.section);
|
|
404
|
+
expect(sectionNames).toContain('plugin-a');
|
|
405
|
+
expect(sectionNames).toContain('plugin-b');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('TEST-10: removing one plugin leaves the other intact', async () => {
|
|
409
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
410
|
+
|
|
411
|
+
const contentA = '## Plugin A Conventions\n\nPlugin A content.\n';
|
|
412
|
+
const contentB = '## Plugin B Conventions\n\nPlugin B content.\n';
|
|
413
|
+
|
|
414
|
+
await injectClaudeMdSection(claudeMdPath, 'plugin-a', '1.0.0', contentA);
|
|
415
|
+
await injectClaudeMdSection(claudeMdPath, 'plugin-b', '1.0.0', contentB);
|
|
416
|
+
|
|
417
|
+
// Remove plugin-a
|
|
418
|
+
const removeResult = await removeClaudeMdSection(claudeMdPath, 'plugin-a');
|
|
419
|
+
expect(removeResult.removed).toBe(true);
|
|
420
|
+
|
|
421
|
+
const fileContent = await readFile(claudeMdPath, 'utf-8');
|
|
422
|
+
|
|
423
|
+
// Plugin A should be gone
|
|
424
|
+
expect(fileContent).not.toContain('<!-- BEGIN magus:plugin-a');
|
|
425
|
+
expect(fileContent).not.toContain('Plugin A content.');
|
|
426
|
+
|
|
427
|
+
// Plugin B should still be there
|
|
428
|
+
expect(fileContent).toContain('<!-- BEGIN magus:plugin-b v1.0.0 -->');
|
|
429
|
+
expect(fileContent).toContain('<!-- END magus:plugin-b -->');
|
|
430
|
+
expect(fileContent).toContain('Plugin B content.');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('TEST-11: two plugins with different gitignore entries both present', async () => {
|
|
434
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
435
|
+
|
|
436
|
+
await ensureGitignoreEntries(gitignorePath, ['.mnemex/'], '# Plugin A');
|
|
437
|
+
await ensureGitignoreEntries(gitignorePath, ['.claudemem/'], '# Plugin B');
|
|
438
|
+
|
|
439
|
+
const content = await readFile(gitignorePath, 'utf-8');
|
|
440
|
+
expect(content).toContain('.mnemex/');
|
|
441
|
+
expect(content).toContain('.claudemem/');
|
|
442
|
+
|
|
443
|
+
// No duplicates -- adding same entries again should not duplicate
|
|
444
|
+
await ensureGitignoreEntries(gitignorePath, ['.mnemex/']);
|
|
445
|
+
const contentAfter = await readFile(gitignorePath, 'utf-8');
|
|
446
|
+
const mnemexOccurrences = contentAfter.split('.mnemex/').length - 1;
|
|
447
|
+
expect(mnemexOccurrences).toBe(1);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// =========================================================================
|
|
452
|
+
// Category 5: Doctor Detection
|
|
453
|
+
// =========================================================================
|
|
454
|
+
|
|
455
|
+
describe('Doctor Detection', () => {
|
|
456
|
+
// Note: checkGitignoreEntries returns { present: string[], missing: string[] }
|
|
457
|
+
// instead of string[] as documented in the architecture. This is an
|
|
458
|
+
// IMPLEMENTATION_ISSUE (API deviation), but the richer return type is a
|
|
459
|
+
// superset. Tests use .missing property to access the missing entries.
|
|
460
|
+
|
|
461
|
+
it('TEST-12: checkGitignoreEntries detects missing entries', async () => {
|
|
462
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
463
|
+
// Create gitignore with only one entry
|
|
464
|
+
await writeFile(gitignorePath, '.mnemex/\n', 'utf-8');
|
|
465
|
+
|
|
466
|
+
const result = await checkGitignoreEntries(gitignorePath, [
|
|
467
|
+
'.mnemex/',
|
|
468
|
+
'.claudemem/',
|
|
469
|
+
'.claude/sessions/',
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
// API returns { present, missing } object instead of string[] per contract
|
|
473
|
+
const resultObj = result as unknown as { present: string[]; missing: string[] };
|
|
474
|
+
|
|
475
|
+
expect(resultObj.missing).toContain('.claudemem/');
|
|
476
|
+
expect(resultObj.missing).toContain('.claude/sessions/');
|
|
477
|
+
expect(resultObj.missing).not.toContain('.mnemex/');
|
|
478
|
+
expect(resultObj.missing.length).toBe(2);
|
|
479
|
+
|
|
480
|
+
// Also verify present entries
|
|
481
|
+
expect(resultObj.present).toContain('.mnemex/');
|
|
482
|
+
expect(resultObj.present.length).toBe(1);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('TEST-13: missing CLAUDE.md section is detectable via listClaudeMdSections', async () => {
|
|
486
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
487
|
+
// Create CLAUDE.md with existing content but no managed sections
|
|
488
|
+
await writeFile(claudeMdPath, '# Project CLAUDE.md\n\nSome existing content.\n', 'utf-8');
|
|
489
|
+
|
|
490
|
+
const sections = await listClaudeMdSections(claudeMdPath);
|
|
491
|
+
expect(sections.length).toBe(0);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('TEST-14: ensureGitignoreEntries fixes missing entries detected by check', async () => {
|
|
495
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
496
|
+
await writeFile(gitignorePath, '.mnemex/\n', 'utf-8');
|
|
497
|
+
|
|
498
|
+
// Detect missing using the actual return type
|
|
499
|
+
const checkResult = await checkGitignoreEntries(gitignorePath, [
|
|
500
|
+
'.mnemex/',
|
|
501
|
+
'.claudemem/',
|
|
502
|
+
'.claude/sessions/',
|
|
503
|
+
]);
|
|
504
|
+
const missing = (checkResult as unknown as { missing: string[] }).missing;
|
|
505
|
+
expect(missing.length).toBe(2);
|
|
506
|
+
|
|
507
|
+
// Fix by adding missing entries
|
|
508
|
+
const fixResult = await ensureGitignoreEntries(gitignorePath, missing);
|
|
509
|
+
expect(fixResult.modified).toBe(true);
|
|
510
|
+
expect(fixResult.added.length).toBe(2);
|
|
511
|
+
|
|
512
|
+
// Re-check: should now all be present
|
|
513
|
+
const recheck = await checkGitignoreEntries(gitignorePath, [
|
|
514
|
+
'.mnemex/',
|
|
515
|
+
'.claudemem/',
|
|
516
|
+
'.claude/sessions/',
|
|
517
|
+
]);
|
|
518
|
+
const stillMissing = (recheck as unknown as { missing: string[] }).missing;
|
|
519
|
+
expect(stillMissing.length).toBe(0);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// =========================================================================
|
|
524
|
+
// Category 6: Malformed CLAUDE.md Handling
|
|
525
|
+
// =========================================================================
|
|
526
|
+
|
|
527
|
+
describe('Malformed CLAUDE.md Handling', () => {
|
|
528
|
+
it('TEST-15: inject refuses when CLAUDE.md has missing END marker', async () => {
|
|
529
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
530
|
+
// Write a malformed file: BEGIN without END
|
|
531
|
+
const malformedContent = [
|
|
532
|
+
'# Project Notes',
|
|
533
|
+
'',
|
|
534
|
+
'<!-- BEGIN magus:broken-plugin v1.0.0 -->',
|
|
535
|
+
'## Broken Plugin',
|
|
536
|
+
'',
|
|
537
|
+
'This section has no END marker.',
|
|
538
|
+
'',
|
|
539
|
+
'# More content below',
|
|
540
|
+
'',
|
|
541
|
+
].join('\n');
|
|
542
|
+
await writeFile(claudeMdPath, malformedContent, 'utf-8');
|
|
543
|
+
|
|
544
|
+
// Attempting to inject a NEW section should be refused
|
|
545
|
+
const result = await injectClaudeMdSection(
|
|
546
|
+
claudeMdPath,
|
|
547
|
+
'new-plugin',
|
|
548
|
+
'1.0.0',
|
|
549
|
+
'## New Plugin\n\nContent.\n',
|
|
550
|
+
);
|
|
551
|
+
expect(result.action).toBe('error');
|
|
552
|
+
expect(result.error).toBeDefined();
|
|
553
|
+
expect(typeof result.error).toBe('string');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('TEST-16: parse detects duplicate sections', async () => {
|
|
557
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
558
|
+
const duplicateContent = [
|
|
559
|
+
'# Project',
|
|
560
|
+
'',
|
|
561
|
+
'<!-- BEGIN magus:test-plugin v1.0.0 -->',
|
|
562
|
+
'## First instance',
|
|
563
|
+
'<!-- END magus:test-plugin -->',
|
|
564
|
+
'',
|
|
565
|
+
'<!-- BEGIN magus:test-plugin v1.0.0 -->',
|
|
566
|
+
'## Second instance (duplicate!)',
|
|
567
|
+
'<!-- END magus:test-plugin -->',
|
|
568
|
+
'',
|
|
569
|
+
].join('\n');
|
|
570
|
+
await writeFile(claudeMdPath, duplicateContent, 'utf-8');
|
|
571
|
+
|
|
572
|
+
const parseResult: ParseResult = await parseClaudeMdSections(claudeMdPath);
|
|
573
|
+
expect(parseResult.errors.length).toBeGreaterThan(0);
|
|
574
|
+
|
|
575
|
+
const duplicateError = parseResult.errors.find(
|
|
576
|
+
(e) => e.type === 'duplicate_section',
|
|
577
|
+
);
|
|
578
|
+
expect(duplicateError).toBeDefined();
|
|
579
|
+
if (duplicateError) {
|
|
580
|
+
expect(duplicateError.section).toBe('test-plugin');
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('TEST-17: parse ignores markers inside code blocks', async () => {
|
|
585
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
586
|
+
const contentWithCodeBlock = [
|
|
587
|
+
'# Documentation',
|
|
588
|
+
'',
|
|
589
|
+
'Here is an example of markers:',
|
|
590
|
+
'',
|
|
591
|
+
'```markdown',
|
|
592
|
+
'<!-- BEGIN magus:example v1.0.0 -->',
|
|
593
|
+
'## Example section',
|
|
594
|
+
'<!-- END magus:example -->',
|
|
595
|
+
'```',
|
|
596
|
+
'',
|
|
597
|
+
'The above markers are inside a code block and should be ignored.',
|
|
598
|
+
'',
|
|
599
|
+
].join('\n');
|
|
600
|
+
await writeFile(claudeMdPath, contentWithCodeBlock, 'utf-8');
|
|
601
|
+
|
|
602
|
+
const parseResult = await parseClaudeMdSections(claudeMdPath);
|
|
603
|
+
|
|
604
|
+
// Markers inside code blocks should NOT be treated as real sections
|
|
605
|
+
const realSections = parseResult.sections.filter(
|
|
606
|
+
(s) => !s.insideCodeBlock,
|
|
607
|
+
);
|
|
608
|
+
expect(realSections.length).toBe(0);
|
|
609
|
+
|
|
610
|
+
// If the parser reports code-block markers, check the error type
|
|
611
|
+
if (parseResult.errors.length > 0) {
|
|
612
|
+
const codeBlockErrors = parseResult.errors.filter(
|
|
613
|
+
(e) => e.type === 'marker_in_code_block',
|
|
614
|
+
);
|
|
615
|
+
// It's valid for the parser to either:
|
|
616
|
+
// 1. Report marker_in_code_block errors, OR
|
|
617
|
+
// 2. Simply ignore them (no sections, no errors)
|
|
618
|
+
// Both behaviors comply with the architecture spec.
|
|
619
|
+
if (codeBlockErrors.length > 0) {
|
|
620
|
+
expect(codeBlockErrors[0].section).toBe('example');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// =========================================================================
|
|
627
|
+
// Category 7: Path Classification (Gitignore Entries)
|
|
628
|
+
// =========================================================================
|
|
629
|
+
|
|
630
|
+
describe('Path Classification', () => {
|
|
631
|
+
it('TEST-18: all standard private paths can be added to gitignore', async () => {
|
|
632
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
633
|
+
const privatePaths = [
|
|
634
|
+
'.mnemex/',
|
|
635
|
+
'.claudemem/',
|
|
636
|
+
'.claude/sessions/',
|
|
637
|
+
'.claude/worktrees/',
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
const result = await ensureGitignoreEntries(
|
|
641
|
+
gitignorePath,
|
|
642
|
+
privatePaths,
|
|
643
|
+
'# Claude Code ephemeral/machine-specific data',
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
expect(result.modified).toBe(true);
|
|
647
|
+
expect(result.added.length).toBe(4);
|
|
648
|
+
|
|
649
|
+
const content = await readFile(gitignorePath, 'utf-8');
|
|
650
|
+
for (const path of privatePaths) {
|
|
651
|
+
expect(content).toContain(path);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('TEST-19: checkGitignoreEntries reports correct missing set', async () => {
|
|
656
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
657
|
+
// Only .mnemex/ exists
|
|
658
|
+
await writeFile(gitignorePath, '.mnemex/\n', 'utf-8');
|
|
659
|
+
|
|
660
|
+
const allPaths = [
|
|
661
|
+
'.mnemex/',
|
|
662
|
+
'.claudemem/',
|
|
663
|
+
'.claude/sessions/',
|
|
664
|
+
'.claude/worktrees/',
|
|
665
|
+
];
|
|
666
|
+
|
|
667
|
+
const result = await checkGitignoreEntries(gitignorePath, allPaths);
|
|
668
|
+
// API returns { present, missing } instead of string[] (see IMPLEMENTATION_ISSUE note)
|
|
669
|
+
const resultObj = result as unknown as { present: string[]; missing: string[] };
|
|
670
|
+
|
|
671
|
+
expect(resultObj.missing.length).toBe(3);
|
|
672
|
+
expect(resultObj.missing).toContain('.claudemem/');
|
|
673
|
+
expect(resultObj.missing).toContain('.claude/sessions/');
|
|
674
|
+
expect(resultObj.missing).toContain('.claude/worktrees/');
|
|
675
|
+
expect(resultObj.missing).not.toContain('.mnemex/');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('TEST-20: gitignore entries survive plugin uninstall', async () => {
|
|
679
|
+
const pluginDir = await createMockPlugin(tempDir, 'test-plugin', {
|
|
680
|
+
gitignoreProject: ['.mnemex/', '.claude/sessions/'],
|
|
681
|
+
claudemdSection: 'test-plugin',
|
|
682
|
+
claudemdVersion: '1.0.0',
|
|
683
|
+
templateContent: '## Test\n\nContent.\n',
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Apply conventions manually (since applyConventions has a bug)
|
|
687
|
+
await applyConventionsManually(pluginDir, projectDir);
|
|
688
|
+
|
|
689
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
690
|
+
const gitignoreBefore = await readFile(gitignorePath, 'utf-8');
|
|
691
|
+
expect(gitignoreBefore).toContain('.mnemex/');
|
|
692
|
+
expect(gitignoreBefore).toContain('.claude/sessions/');
|
|
693
|
+
|
|
694
|
+
// Remove conventions via public API
|
|
695
|
+
await removeConventions('test-plugin', projectDir);
|
|
696
|
+
|
|
697
|
+
// Gitignore entries MUST still be there (by design)
|
|
698
|
+
const gitignoreAfter = await readFile(gitignorePath, 'utf-8');
|
|
699
|
+
expect(gitignoreAfter).toContain('.mnemex/');
|
|
700
|
+
expect(gitignoreAfter).toContain('.claude/sessions/');
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// =========================================================================
|
|
705
|
+
// IMPLEMENTATION_ISSUE: applyConventions() regression tests
|
|
706
|
+
// These tests SHOULD pass when the bug in applyConventions() is fixed.
|
|
707
|
+
// They currently fail because applyConventions() silently skips all work.
|
|
708
|
+
// =========================================================================
|
|
709
|
+
|
|
710
|
+
describe('applyConventions orchestration (IMPLEMENTATION_ISSUE)', () => {
|
|
711
|
+
it('TEST-1-A: applyConventions should apply gitignore entries', async () => {
|
|
712
|
+
const pluginDir = await createMockPlugin(tempDir, 'apply-test', {
|
|
713
|
+
gitignoreProject: ['.mnemex/', '.claude/sessions/'],
|
|
714
|
+
claudemdSection: 'apply-test',
|
|
715
|
+
claudemdVersion: '1.0.0',
|
|
716
|
+
templateContent: '## Test Plugin\n\nConventions.\n',
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const result = await applyConventions(pluginDir, projectDir);
|
|
720
|
+
expect(result.gitignore.modified).toBe(true);
|
|
721
|
+
expect(result.gitignore.added).toContain('.mnemex/');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('TEST-2-A: applyConventions should inject CLAUDE.md section', async () => {
|
|
725
|
+
const pluginDir = await createMockPlugin(tempDir, 'apply-test', {
|
|
726
|
+
claudemdSection: 'apply-test',
|
|
727
|
+
claudemdVersion: '1.0.0',
|
|
728
|
+
templateContent: '## Test Plugin\n\nConventions.\n',
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const result = await applyConventions(pluginDir, projectDir);
|
|
732
|
+
expect(result.claudemd.action).toBe('inserted');
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('TEST-3-A: removeConventions should remove CLAUDE.md section', async () => {
|
|
736
|
+
// First create a CLAUDE.md with a section using the working low-level API
|
|
737
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
738
|
+
await injectClaudeMdSection(claudeMdPath, 'test-plugin', '1.0.0', '## Test\n\nContent.\n');
|
|
739
|
+
|
|
740
|
+
// removeConventions should find and remove the section
|
|
741
|
+
const result = await removeConventions('test-plugin', projectDir);
|
|
742
|
+
expect(result.claudemd.removed).toBe(true);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|