Skip to content

Commit 647a22f

Browse files
ithiria894claude
andcommitted
refactor: move destination tests from E2E to unit tests (v0.14.1)
- Remove 6 E2E destination tests (replaced by unit tests, 10s → 54ms) - Add 2 new unit tests: batch verify all non-movable and movable categories - Total: 37 unit tests (<100ms), ~135 E2E tests Unit tests now cover: - All category move destinations (12 tests) - Locked item behavior (3 tests) - All effective rules + shadowing + conflicts + ancestors (22 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2910699 commit 647a22f

8 files changed

Lines changed: 341 additions & 68 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpware/claude-code-organizer",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"description": "Organize all your Claude Code memories, skills, MCP servers, commands, agents, rules, and hooks — see what loads globally vs per-project, then move items between scopes",
55
"type": "module",
66
"files": [

tests/e2e/dashboard.spec.mjs

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3215,68 +3215,5 @@ test.describe('Show Effective — per-category rules', () => {
32153215
});
32163216
});
32173217

3218-
test.describe('Move restrictions — per-category destinations', () => {
3219-
let env;
3220-
test.beforeAll(async () => { env = await createTestEnv(); });
3221-
test.afterAll(async () => { await env.cleanup(); });
3222-
3223-
test('plan items have no valid move destinations', async () => {
3224-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3225-
const plan = items.find(i => i.category === 'plan');
3226-
if (!plan) return; // skip if no plans
3227-
3228-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(plan.path)}&category=plan&name=${encodeURIComponent(plan.name)}`)).json();
3229-
expect(res.ok).toBe(true);
3230-
expect(res.destinations).toHaveLength(0);
3231-
});
3232-
3233-
test('rule items have no valid move destinations', async () => {
3234-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3235-
const rule = items.find(i => i.category === 'rule');
3236-
if (!rule) return;
3237-
3238-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(rule.path)}&category=rule&name=${encodeURIComponent(rule.name)}`)).json();
3239-
expect(res.ok).toBe(true);
3240-
expect(res.destinations).toHaveLength(0);
3241-
});
3242-
3243-
test('skill items have valid move destinations', async () => {
3244-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3245-
const skill = items.find(i => i.category === 'skill');
3246-
expect(skill).toBeTruthy();
3247-
3248-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(skill.path)}&category=skill&name=${encodeURIComponent(skill.name)}`)).json();
3249-
expect(res.ok).toBe(true);
3250-
expect(res.destinations.length).toBeGreaterThan(0);
3251-
});
3252-
3253-
test('mcp items have valid move destinations', async () => {
3254-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3255-
const mcp = items.find(i => i.category === 'mcp');
3256-
expect(mcp).toBeTruthy();
3257-
3258-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(mcp.path)}&category=mcp&name=${encodeURIComponent(mcp.name)}`)).json();
3259-
expect(res.ok).toBe(true);
3260-
expect(res.destinations.length).toBeGreaterThan(0);
3261-
});
3262-
3263-
test('command items have valid move destinations', async () => {
3264-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3265-
const cmd = items.find(i => i.category === 'command');
3266-
expect(cmd).toBeTruthy();
3267-
3268-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(cmd.path)}&category=command&name=${encodeURIComponent(cmd.name)}`)).json();
3269-
expect(res.ok).toBe(true);
3270-
expect(res.destinations.length).toBeGreaterThan(0);
3271-
});
3272-
3273-
test('agent items have valid move destinations', async () => {
3274-
const { items } = await (await fetch(`${env.baseURL}/api/scan`)).json();
3275-
const agent = items.find(i => i.category === 'agent');
3276-
expect(agent).toBeTruthy();
3277-
3278-
const res = await (await fetch(`${env.baseURL}/api/destinations?path=${encodeURIComponent(agent.path)}&category=agent&name=${encodeURIComponent(agent.name)}`)).json();
3279-
expect(res.ok).toBe(true);
3280-
expect(res.destinations.length).toBeGreaterThan(0);
3281-
});
3282-
});
3218+
// Move restrictions tests moved to unit tests (tests/unit/test-move-destinations.mjs)
3219+
// — tests the same getValidDestinations() function directly, <100ms vs ~10s E2E

tests/pw-fix-prompt.cjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const { chromium } = require('/home/nicole/.nvm/versions/node/v20.19.4/lib/node_modules/playwright');
2+
3+
(async () => {
4+
const browser = await chromium.launch({ headless: false });
5+
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
6+
// Grant clipboard permission
7+
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
8+
const page = await context.newPage();
9+
const errors = [];
10+
page.on('pageerror', e => errors.push(e.message));
11+
12+
try {
13+
await page.goto('http://localhost:3847');
14+
await page.waitForTimeout(2000);
15+
await page.click('#securityScanBtn');
16+
await page.waitForTimeout(1000);
17+
18+
// Select AgentSeal + Scan
19+
await page.locator('#securityEngineSelect').selectOption('agentseal');
20+
await page.locator('#securityStartBtn').click();
21+
console.log('Scanning...');
22+
await page.waitForTimeout(12000);
23+
24+
console.log('Findings:', await page.locator('.sec-finding-item').count());
25+
26+
// Scroll + expand first server
27+
await page.locator('#securityBody').evaluate(el => el.scrollTop = 200);
28+
await page.waitForTimeout(300);
29+
await page.locator('.sec-collapse-btn').first().click();
30+
await page.waitForTimeout(500);
31+
32+
// Find a "Fix with Claude →" button
33+
const fixBtn = page.locator('.sec-fix-clickable').first();
34+
const fixVisible = await fixBtn.isVisible();
35+
console.log('Fix button visible:', fixVisible);
36+
37+
if (fixVisible) {
38+
// Hover to see "Fix with Claude →"
39+
await fixBtn.hover();
40+
await page.waitForTimeout(500);
41+
await page.screenshot({ path: '/tmp/qa-fix-hover.png' });
42+
console.log('Hover screenshot saved');
43+
44+
// Click to copy prompt
45+
await fixBtn.click();
46+
await page.waitForTimeout(500);
47+
48+
// Read clipboard
49+
const clipboard = await page.evaluate(() => navigator.clipboard.readText());
50+
console.log('Clipboard content (first 200 chars):', clipboard.slice(0, 200));
51+
52+
// Verify prompt contains key info
53+
const hasServer = clipboard.includes('MCP server');
54+
const hasEngine = clipboard.includes('AgentSeal') || clipboard.includes('agentseal');
55+
const hasFix = clipboard.includes('Suggested fix');
56+
const hasEvaluate = clipboard.includes('evaluate');
57+
console.log('Has server name:', hasServer);
58+
console.log('Has engine name:', hasEngine);
59+
console.log('Has suggested fix:', hasFix);
60+
console.log('Has evaluate request:', hasEvaluate);
61+
62+
await page.screenshot({ path: '/tmp/qa-fix-clicked.png' });
63+
}
64+
65+
console.log('JS Errors:', errors.length ? errors : 'NONE');
66+
console.log('\n✅ ALL PASSED');
67+
} catch (e) {
68+
console.error('❌', e.message);
69+
await page.screenshot({ path: '/tmp/qa-fix-err.png' });
70+
} finally {
71+
await page.waitForTimeout(2000);
72+
await browser.close();
73+
}
74+
})();

tests/pw-published-smoke.cjs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Smoke test for published npm version — verifies core functionality works
3+
* Run: DISPLAY=:0 node tests/pw-published-smoke.cjs
4+
*/
5+
const { chromium } = require('/home/nicole/.nvm/versions/node/v20.19.4/lib/node_modules/playwright');
6+
7+
(async () => {
8+
const browser = await chromium.launch({ headless: false });
9+
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
10+
const errors = [];
11+
page.on('pageerror', e => errors.push(e.message));
12+
let passed = 0, failed = 0;
13+
14+
function ok(name) { passed++; console.log(` ✅ ${name}`); }
15+
function fail(name, reason) { failed++; console.log(` ❌ ${name}: ${reason}`); }
16+
17+
try {
18+
await page.goto('http://localhost:3847');
19+
await page.waitForTimeout(2000);
20+
21+
// ═══ 1: Dashboard loads ═══
22+
console.log('TEST 1: Dashboard loads');
23+
const title = await page.title();
24+
if (title.includes('Claude Code Organizer')) ok('title correct: ' + title);
25+
else fail('title', title);
26+
27+
// ═══ 2: Sidebar scope tree ═══
28+
console.log('\nTEST 2: Sidebar scope tree');
29+
const globalNode = page.locator('.s-nm:has-text("Global")').first();
30+
if (await globalNode.isVisible()) ok('Global scope visible');
31+
else fail('Global scope', 'not visible');
32+
33+
// ═══ 3: Items load ═══
34+
console.log('\nTEST 3: Items load');
35+
const scanData = await page.evaluate(() => fetch('/api/scan').then(r => r.json()));
36+
const itemCount = scanData.items?.length || 0;
37+
if (itemCount > 0) ok(`${itemCount} items discovered`);
38+
else fail('items', '0 items');
39+
40+
// ═══ 4: MCP servers discovered ═══
41+
console.log('\nTEST 4: MCP servers');
42+
const mcpItems = scanData.items?.filter(i => i.category === 'mcp') || [];
43+
if (mcpItems.length > 0) ok(`${mcpItems.length} MCP servers found`);
44+
else fail('MCP', '0 servers');
45+
46+
// ═══ 5: File content API (Issue #12 fix) ═══
47+
console.log('\nTEST 5: File content API (Issue #12)');
48+
const fileItem = scanData.items?.find(i => i.path && i.category === 'skill');
49+
if (fileItem) {
50+
const resp = await page.evaluate(async (p) => {
51+
return fetch(`/api/file-content?path=${encodeURIComponent(p)}`).then(r => r.json());
52+
}, fileItem.path);
53+
if (resp.ok || resp.content !== undefined) ok('file-content works for: ' + fileItem.name);
54+
else fail('file-content', resp.error || 'no content');
55+
} else {
56+
ok('no skill items to test (skipped)');
57+
}
58+
59+
// ═══ 6: Export API accepts absolute path (Issue #12 fix) ═══
60+
console.log('\nTEST 6: Export API (Issue #12)');
61+
const exportResp = await page.evaluate(async () => {
62+
return fetch('/api/export', {
63+
method: 'POST',
64+
headers: { 'Content-Type': 'application/json' },
65+
body: JSON.stringify({ exportDir: '/tmp/cco-publish-test' }),
66+
}).then(r => r.json());
67+
});
68+
if (exportResp.ok) ok('export works with absolute path');
69+
else fail('export', exportResp.error);
70+
71+
// ═══ 7: claudeJsonProjectKey present (Issue #11 fix) ═══
72+
console.log('\nTEST 7: claudeJsonProjectKey (Issue #11)');
73+
const claudeJsonItems = mcpItems.filter(i => i.fileName === '.claude.json');
74+
const withKey = claudeJsonItems.filter(i => i.claudeJsonProjectKey);
75+
ok(`${claudeJsonItems.length} .claude.json servers, ${withKey.length} with projectKey`);
76+
77+
// ═══ 8: Security scan works ═══
78+
console.log('\nTEST 8: Security scan API');
79+
const secResp = await page.evaluate(async () => {
80+
return fetch('/api/security-scan', { method: 'POST' }).then(r => r.json());
81+
});
82+
if (secResp.ok) ok(`scan returned ${secResp.findings?.length || 0} findings from ${secResp.serversConnected} servers`);
83+
else fail('security scan', secResp.error);
84+
85+
// ═══ 9: Context budget API ═══
86+
console.log('\nTEST 9: Context budget API');
87+
const ctxResp = await page.evaluate(async () => {
88+
return fetch('/api/context-budget?scope=global').then(r => r.json());
89+
});
90+
if (ctxResp.ok) ok(`context budget: ${ctxResp.percentUsed || 0}% used`);
91+
else fail('context budget', ctxResp.error);
92+
93+
// ═══ 10: Security panel opens ═══
94+
console.log('\nTEST 10: Security panel UI');
95+
await page.click('#securityScanBtn');
96+
await page.waitForTimeout(500);
97+
const secPanel = page.locator('#securityPanel');
98+
if (await secPanel.isVisible()) ok('security panel opens');
99+
else fail('security panel', 'not visible');
100+
101+
// ═══ 11: Context budget panel opens ═══
102+
console.log('\nTEST 11: Context budget UI');
103+
await page.locator('#securityClose').click();
104+
await page.waitForTimeout(300);
105+
const ctxBtn = page.locator('button:has-text("Context Budget")');
106+
if (await ctxBtn.isVisible()) {
107+
await ctxBtn.click();
108+
await page.waitForTimeout(500);
109+
const ctxPanel = page.locator('#ctxBudgetPanel');
110+
if (await ctxPanel.isVisible()) ok('context budget panel opens');
111+
else fail('context budget panel', 'not visible');
112+
} else {
113+
ok('context budget button not on this scope (skipped)');
114+
}
115+
116+
// ═══ 12: Click item shows detail ═══
117+
console.log('\nTEST 12: Item detail panel');
118+
const firstItem = page.locator('.item').first();
119+
if (await firstItem.isVisible()) {
120+
await firstItem.click();
121+
await page.waitForTimeout(500);
122+
const detail = page.locator('#detail');
123+
if (await detail.isVisible()) ok('detail panel opens on item click');
124+
else fail('detail panel', 'not visible after click');
125+
} else {
126+
ok('no items visible (skipped)');
127+
}
128+
129+
// ═══ 13: No JS errors ═══
130+
console.log('\nTEST 13: No JavaScript errors');
131+
if (errors.length === 0) ok('zero JS errors');
132+
else fail(`${errors.length} JS errors`, errors.join('; '));
133+
134+
// Screenshot
135+
await page.screenshot({ path: '/tmp/pw-published-smoke.png' });
136+
137+
console.log(`\n═══ RESULTS: ${passed} passed, ${failed} failed ═══`);
138+
if (failed > 0) process.exitCode = 1;
139+
} catch (e) {
140+
console.error('❌ FATAL:', e.message);
141+
await page.screenshot({ path: '/tmp/pw-published-err.png' });
142+
process.exitCode = 1;
143+
} finally {
144+
await page.waitForTimeout(2000);
145+
await browser.close();
146+
}
147+
})();

tests/pw-qa.cjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { chromium } = require('/home/nicole/.nvm/versions/node/v20.19.4/lib/node_modules/playwright');
2+
3+
(async () => {
4+
const browser = await chromium.launch({ headless: false });
5+
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
6+
const errors = [];
7+
page.on('pageerror', e => errors.push(e.message));
8+
9+
try {
10+
await page.goto('http://localhost:3847');
11+
await page.waitForTimeout(2000);
12+
13+
// Open security panel
14+
await page.click('#securityScanBtn');
15+
await page.waitForTimeout(1000);
16+
17+
// Select AgentSeal + Scan
18+
await page.locator('#securityEngineSelect').selectOption('agentseal');
19+
await page.locator('#securityStartBtn').click();
20+
console.log('Scanning with AgentSeal...');
21+
await page.waitForTimeout(12000);
22+
23+
const findings = await page.locator('.sec-finding-item').count();
24+
console.log('Findings:', findings);
25+
26+
// Scroll + expand first server
27+
await page.locator('#securityBody').evaluate(el => el.scrollTop = 200);
28+
await page.waitForTimeout(300);
29+
await page.locator('.sec-collapse-btn').first().click();
30+
await page.waitForTimeout(500);
31+
32+
// Screenshot findings detail
33+
await page.screenshot({ path: '/tmp/qa-findings.png' });
34+
console.log('Screenshot: /tmp/qa-findings.png');
35+
36+
// Click to navigate
37+
const first = page.locator('.sec-finding-item').first();
38+
console.log('Server:', await first.getAttribute('data-sec-server'));
39+
await first.click();
40+
await page.waitForTimeout(1000);
41+
await page.screenshot({ path: '/tmp/qa-navigate.png' });
42+
43+
console.log('JS Errors:', errors.length ? errors : 'NONE');
44+
console.log('\n✅ ALL PASSED');
45+
} catch (e) {
46+
console.error('❌', e.message);
47+
await page.screenshot({ path: '/tmp/qa-err.png' });
48+
} finally {
49+
await page.waitForTimeout(2000);
50+
await browser.close();
51+
}
52+
})();

0 commit comments

Comments
 (0)