

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
123456# Testing Guide for Script Kit App78## Avoid Running the App910Never, ever run the app. Ask me to start it, then you can check the logs.1112## Test Configuration1314### Performance Optimizations15- Use `bail: 1` for fast failure (stop on first test failure)16- Enable parallel execution with `singleThread: false` and `isolate: true` for speed17- Set reasonable thread limits: `maxThreads: 4`, `minThreads: 2`18- Use `vitest run` instead of `vitest` for single test runs1920### Environment Setup21- **Main process tests**: Use `node` environment22- **Renderer tests**: Use `jsdom` environment23- Configure separate vitest workspace configs for different environments2425### Test Script Configuration26```json27{28 "scripts": {29 "test": "vitest run" // Single run, not watch mode30 }31}32```3334## Test Execution Strategy3536### Concurrent vs Sequential Testing37Choose the right execution strategy based on test complexity:3839#### ✅ Parallel Tests (Use `describe.concurrent()`)40- Simple file operations (create, read, delete)41- Basic validation tests42- Mock-heavy tests with no external dependencies43- Tests that don't require complex timing4445#### ✅ Sequential Tests (Use regular `describe()`)46- File system operations that require exclusive access47- Directory renames and complex file operations48- Tests with intricate timing dependencies49- Resource-intensive operations5051### Load-Resilient Test Design52Tests must work under both isolated and concurrent execution:5354```typescript55// ❌ Fragile - only works in isolation56const events = await collectEvents(500, async () => {57 await writeFile(path, content);58});5960// ✅ Robust - works under system load61const events = await collectEventsIsolated(1500, async (events, dirs) => {62 await writeFile(path, content);63 // Wait for file system under load64 await new Promise(resolve => setTimeout(resolve, 300));65});66```6768### Test Environment Differences69Be aware that `pnpm test` creates different conditions than individual file execution:7071- **Individual files**: Lower resource contention, faster execution72- **Full test suite**: Higher system load, requires longer timeouts73- **Solution**: Design tests to be resilient to both conditions7475## Mocking Strategies7677### ✅ What Works7879#### Inline Mocks in Test Files80Prefer inline mocks over shared mock utilities to avoid circular dependencies:8182```typescript83vi.mock('electron', () => ({84 app: {85 getPath: vi.fn((name: string) => {86 switch (name) {87 case 'userData': return '/Users/test/Library/Application Support/ScriptKit';88 default: return '/Users/test';89 }90 }),91 // ... other app methods92 },93 powerMonitor: {94 on: vi.fn(),95 addListener: vi.fn(), // Important: Include both on AND addListener96 listeners: vi.fn(() => []),97 }98}));99```100101#### Essential Electron APIs to Mock102- `app.getPath()` - Critical for path resolution103- `powerMonitor.addListener()` - Not just `on()`, include both104- `nativeTheme` - Often required by components105- `BrowserWindow` - With full webContents mock106- `crashReporter.start()` - Often called during initialization107108#### Electron-log Mock Structure109```typescript110vi.mock('electron-log', () => ({111 default: {112 transports: {113 file: { level: 'info' },114 console: { level: false },115 ipc: { level: false }, // Essential - prevents "Cannot set properties of undefined"116 },117 info: vi.fn(),118 error: vi.fn(),119 // ... other log methods120 }121}));122```123124#### Node.js Module Mocking125For `node:os` and other Node modules, provide complete API surface:126```typescript127vi.mock('node:os', () => ({128 default: {129 homedir: vi.fn(() => '/Users/test'),130 platform: vi.fn(() => 'darwin'),131 // ... all other os methods132 constants: {133 signals: { /* full signal definitions */ }134 }135 }136}));137```138139### ❌ What Doesn't Work140141#### Shared Mock Utilities142Avoid creating shared mock files that are imported across tests:143```typescript144// ❌ Don't do this - causes circular dependencies145import { setupCommonMocks } from './src/test-utils/mocks';146```147148#### Incomplete Mock Objects149Missing required properties cause runtime errors:150```typescript151// ❌ Incomplete - missing required properties152const testScript = {153 filePath: '/test/path/script.ts',154 system: 'resume'155 // Missing: command, id, name156};157```158159#### Complex Debounce/Timing Tests160Tests that rely on complex timing with fake timers and lodash debounce are fragile:161```typescript162// ❌ Fragile - timing-dependent163vi.advanceTimersByTime(250);164expect(debouncedFunction).toHaveBeenCalledTimes(2);165```166167## Test Object Requirements168169### Script Objects170When creating test script objects, include all required properties:171```typescript172const testScript = {173 filePath: '/test/path/script.ts',174 kenv: '',175 system: 'resume' as const,176 type: ProcessType.System,177 command: 'node', // Required178 id: 'test-script', // Required179 name: 'test-script' // Required for Choice interface180};181```182183### System Event Strings184Use proper system event syntax:185```typescript186system: 'suspend lock-screen' as const // Multiple events187system: 'resume' as const // Single event188```189190## Mock State Management191192### Between Tests193```typescript194beforeEach(() => {195 vi.useFakeTimers();196 // Don't clear mocks if they're used by debounced functions197});198199afterEach(() => {200 vi.useRealTimers();201 vi.clearAllMocks(); // Clear after tests complete202});203```204205### PowerMonitor Event Handling206For tests that need to simulate events:207```typescript208const mockElectronBase = vi.hoisted(() => {209 const handlers = new Map();210 return {211 powerMonitor: {212 addListener: vi.fn((event: string, handler: Function) => {213 if (!handlers.has(event)) handlers.set(event, []);214 handlers.get(event).push(handler);215 }),216 listeners: vi.fn((event: string) => handlers.get(event) || [])217 }218 };219});220```221222## Integration vs Unit Tests223224### Skip Integration Tests225Integration tests that require external dependencies should be skipped in the main test suite:226```typescript227it.skip('should run external command', async () => {228 // Test requires pnpm, specific file paths, etc.229});230```231232### Focus on Unit Logic233Test the core business logic rather than external integrations:234- File watching logic (mocked file operations)235- Event registration/deregistration236- State management237- Component rendering238239## Performance Testing240241### File System Tests242Use temporary directories and proper cleanup:243```typescript244const testDir = vi.hoisted(() =>245 import('tmp-promise').then(({ dir }) => dir({ unsafeCleanup: true }))246);247```248249### Isolated Directory Pattern250For file system tests that need complete isolation from each other:251252```typescript253/**254 * Create isolated test directories for parallel-safe testing255 */256async function createIsolatedTestDirs(testName: string) {257 const { dir } = await import('tmp-promise');258 const tmpDir = await dir({259 unsafeCleanup: true,260 prefix: `test-${testName}-`,261 });262263 const isolatedDirs = {264 root: tmpDir.path,265 kit: path.join(tmpDir.path, '.kit'),266 kenv: path.join(tmpDir.path, '.kenv'),267 scripts: path.join(tmpDir.path, '.kenv', 'scripts'),268 // ... other directories269 cleanup: tmpDir.cleanup,270 };271272 // Create directory structure273 await Promise.all([274 ensureDir(isolatedDirs.kit),275 ensureDir(isolatedDirs.kenv),276 ensureDir(isolatedDirs.scripts),277 ]);278279 return isolatedDirs;280}281282/**283 * Isolated test execution with environment variable override284 */285async function collectEventsIsolated(286 duration: number,287 action: (events: TestEvent[], dirs: any) => Promise<void>,288 testName: string,289): Promise<TestEvent[]> {290 const isolatedDirs = await createIsolatedTestDirs(testName);291292 // Override environment variables for this test293 const originalKIT = process.env.KIT;294 const originalKENV = process.env.KENV;295 process.env.KIT = isolatedDirs.kit;296 process.env.KENV = isolatedDirs.kenv;297298 try {299 // Execute test logic...300 return events;301 } finally {302 // Restore environment variables303 process.env.KIT = originalKIT;304 process.env.KENV = originalKENV;305 await isolatedDirs.cleanup();306 }307}308```309310### Test Isolation Strategy311Choose isolation level based on test requirements:312313- **Shared test directory**: Fast, but potential cross-test interference314- **Isolated directories**: Slower setup, but complete isolation315- **Environment variable override**: Essential for file system tests316317### Timing Guidelines318Adjust timeouts based on execution context:319320```typescript321// ❌ Fixed timing - breaks under load322it('should detect file changes', async () => {323 // Always fails when system is busy324 const events = await collectEvents(500, ...);325}, 3000);326327// ✅ Load-aware timing328it('should detect file changes', async () => {329 const events = await collectEventsIsolated(330 1500, // Longer collection time for concurrent environment331 async (events, dirs) => {332 await writeFile(filePath, content);333 // Extra wait for file system under load334 await new Promise(resolve => setTimeout(resolve, 300));335 },336 'test-name'337 );338}, 8000); // Longer overall timeout339```340341## Common Pitfalls3423431. **Missing `addListener` in powerMonitor** - Electron uses both `on` and `addListener`3442. **Incomplete electron-log transports** - Must include `ipc` transport3453. **Type mismatches in test objects** - Include all required Script properties3464. **Mock state bleeding between tests** - Timing of `vi.clearAllMocks()`3475. **Integration test failures** - Skip tests requiring external setup3486. **Resource contention in parallel tests** - Move complex operations to sequential execution3497. **Fixed timing assumptions** - Tests fail under system load, use load-aware timeouts3508. **Ignoring test environment differences** - `pnpm test` vs individual files require different strategies3519. **Over-engineering solutions** - Sometimes moving tests is better than complex timing fixes35210. **Insufficient isolation** - File system tests interfere without proper directory isolation353354## Problem-Solving Strategy355356When tests fail intermittently:3573581. **First, try the obvious solution** - Move timing-sensitive tests to sequential execution3592. **Increase timeouts progressively** - 500ms → 1500ms → 2000ms until stable3603. **Add load-aware waits** - Extra delays between file operations under concurrent load3614. **Use isolated directories** - Prevent cross-test contamination3625. **Check execution context** - Individual vs full test suite may need different approaches363364## Performance Optimization Results365366### Actual Optimization Case Study (Chokidar Tests)367- **Original**: 30.9 seconds, multiple failures368- **After timing optimization**: 17.0 seconds, 100% passing369- **After strategic test placement**: 18.1 seconds, 100% reliable370- **Total improvement**: 42% faster + 100% reliability371372### Key Strategies That Worked3731. **Strategic test categorization** - Parallel vs sequential based on complexity3742. **Load-resilient timing** - Timeouts that work under system load3753. **Isolated directory pattern** - Complete test isolation3764. **Pragmatic problem solving** - Move problematic tests rather than fix timing377378## Success Metrics379380A well-optimized test suite should achieve:381- ✅ 75+ tests passing (expanded coverage)382- ✅ ~21 second execution time (full suite)383- ✅ 100% reliability under both individual and concurrent execution384- ✅ Clean exit code (0) in all environments385- ✅ Strategic test placement (parallel vs sequential)386- ✅ Load-resilient timing patterns387- ✅ Comprehensive file system coverage with proper isolation388389Focus on **reliability over speed** - better to have slightly slower tests that always pass than fast tests that fail intermittently.390391## Benchmark Testing and Naming Conventions392393### File Naming Standards394395#### ✅ `.bench.ts` - Pure Performance Benchmarks396- Industry standard for dedicated benchmark files397- Uses `bench()` functions for performance measurement398- Separate from regular test suite399- Run with: `pnpm vitest bench file.bench.ts --run`400- Requires vitest config to include `bench` in file patterns401402#### ✅ `.test.ts` - Performance Tests with Assertions403- Performance validation using `it()` + `expect()`404- Part of regular test suite with performance assertions405- Includes detailed console output + validation406- Run with: `pnpm vitest run file.test.ts`407408### Vitest Configuration for Benchmarks409Update [vitest.config.ts](mdc:vitest.config.ts) to include benchmark files:410```typescript411test: {412 include: ['**/*.{test,spec,bench}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],413 benchmark: {414 // Add tinybench options here if needed415 }416}417```418419### Benchmark Execution Commands420```bash421# Pure benchmarks (single-run, not watch mode)422pnpm vitest bench src/main/file.bench.ts --run423424# Performance tests with assertions425pnpm vitest run src/main/file.test.ts426427# Both together via script428node scripts/bench-search.js429```430431## Search Performance Testing432433### Large Dataset Testing (10,000+ Choices)434For testing search performance with realistic datasets:435436```typescript437// Generate realistic mock choices438function generateMockChoices(count: number): Choice[] {439 const categories = ['File Operations', 'Git Tools', 'Development'];440 const prefixes = ['Quick', 'Advanced', 'Simple', 'Super'];441 const actions = ['Manager', 'Tool', 'Helper', 'Runner'];442443 return Array.from({ length: count }, (_, i) => ({444 id: `choice-${i}`,445 name: `${prefixes[i % prefixes.length]} ${actions[i % actions.length]} ${i}`,446 keyword: `keyword${i}`,447 group: categories[i % categories.length],448 // Add variety: 10% shortcodes, 1% info choices, 2% hidden449 info: i % 100 === 0,450 hideWithoutInput: i % 50 === 0,451 }));452}453454// Performance measurement utility455class PerformanceTracker {456 private measurements: Map<string, number[]> = new Map();457458 startTimer(operation: string): () => number {459 const start = performance.now();460 return () => {461 const duration = performance.now() - start;462 if (!this.measurements.has(operation)) {463 this.measurements.set(operation, []);464 }465 this.measurements.get(operation)!.push(duration);466 return duration;467 };468 }469470 getStats(operation: string) {471 const times = this.measurements.get(operation) || [];472 if (times.length === 0) return null;473474 const sorted = times.slice().sort((a, b) => a - b);475 return {476 avg: times.reduce((a, b) => a + b, 0) / times.length,477 min: sorted[0],478 max: sorted[sorted.length - 1],479 p95: sorted[Math.floor(sorted.length * 0.95)],480 };481 }482}483```484485### Search Performance Benchmarks486Essential benchmarks to include:487488```typescript489// Progressive typing simulation490bench('Progressive typing simulation', () => {491 const progressiveTerms = ['f', 'fi', 'fil', 'file', 'file m'];492 progressiveTerms.forEach(term => measureSearch(term));493});494495// Real-world usage patterns496bench('Real-world usage pattern', () => {497 const searchTerms = ['quick', 'file', 'git', '', ' ', 'nonexistent'];498 searchTerms.forEach(term => measureSearch(term));499});500501// Scaling tests502it('should benchmark different choice set sizes', () => {503 const sizes = [1000, 2500, 5000, 7500, 10000];504 sizes.forEach(size => {505 const subset = choices.slice(0, size);506 setChoices(mockPrompt, subset, { preload: false });507 const result = measureSearch('file manager');508 expect(result.duration).toBeLessThan(size * 0.02); // Max 0.02ms per choice509 });510});511```512513### Performance Assertions and Targets514Set realistic performance targets:515516```typescript517// Individual search performance518expect(result.duration).toBeLessThan(100); // Should be under 100ms519520// Average performance across many searches521expect(avgDuration).toBeLessThan(50); // Average under 50ms522523// Memory usage validation524expect(memoryDiff.heapUsed).toBeLessThan(200 * 1024 * 1024); // Under 200MB growth525526// Scaling requirements527expect(slowSearches.length).toBeLessThan(results.length * 0.1); // <10% slow searches528```529530## Integration Testing Patterns531532### UI Input → IPC → Search Flow Testing533Test the complete user interaction flow:534535```typescript536describe('UI Input Integration', () => {537 // Test Channel.INPUT message handling538 it('should handle user typing via Channel.INPUT', () => {539 const inputMessage: InputMessage = {540 input: 'file manager',541 from: 'user-typing'542 };543544 // Simulate IPC message545 handleInputMessage(mockPrompt, inputMessage);546547 // Verify search was invoked548 expect(mockInvokeSearch).toHaveBeenCalledWith(549 mockPrompt,550 'file manager',551 'user-typing'552 );553554 // Verify results sent back555 expect(mockSendToPrompt).toHaveBeenCalledWith(556 Channel.SET_SCORED_CHOICES,557 expect.any(Array)558 );559 });560561 // Test search state management562 it('should update search state correctly', () => {563 const choices = generateMockChoices(1000);564 setChoices(mockPrompt, choices, { preload: false });565566 // Test hasGroup setting for info choices567 mockPrompt.kitSearch.hasGroup = true;568569 invokeSearch(mockPrompt, 'test', 'integration');570571 expect(mockPrompt.kitSearch.input).toBe('test');572 expect(mockPrompt.kitSearch.choices).toHaveLength(1000);573 });574});575```576577### End-to-End Search Integration578Simulate complete user workflows:579580```typescript581// Helper to simulate complete UI → IPC → Search flow582const simulateUserTyping = (583 input: string,584 choices: Choice[],585 options: { mode?: Mode; ui?: UI; expectSearch?: boolean } = {}586) => {587 // Setup choices588 mockPrompt.kitSearch.choices = choices;589 mockPrompt.kitSearch.hasGroup = choices.some(c => !!c.group);590591 // Mock QuickScore for realistic search592 const mockQs = {593 search: vi.fn((searchInput: string) => {594 return choices595 .filter(choice => {596 if (choice.hideWithoutInput && (!searchInput || searchInput.trim() === '')) {597 return false;598 }599 return choice.name?.toLowerCase().includes(searchInput.toLowerCase()) ||600 choice.keyword?.toLowerCase().includes(searchInput.toLowerCase()) ||601 choice.info === true;602 })603 .map(choice => ({604 item: choice,605 score: 0.8,606 matches: { name: [[0, searchInput.length]] },607 _: ''608 }));609 })610 };611 mockPrompt.kitSearch.qs = mockQs as any;612613 // Execute search614 invokeSearch(mockPrompt, input, 'test');615616 // Return results for analysis617 const scoredChoicesMessage = sentMessages.find(m => m.channel === Channel.SET_SCORED_CHOICES);618 return (scoredChoicesMessage?.data as ScoredChoice[]) || [];619};620```621622### Search-Specific Mock Requirements623Essential mocks for search testing:624625```typescript626// Required search mocks627vi.mock('./search', () => ({628 invokeSearch: vi.fn(),629 setChoices: vi.fn(),630 setShortcodes: vi.fn(),631}));632633vi.mock('./messages', () => ({634 cacheChoices: vi.fn()635}));636637vi.mock('./state', () => ({638 kitCache: {639 choices: [],640 scripts: [],641 triggers: new Map(),642 keywords: new Map(),643 shortcodes: new Map(),644 },645 kitState: {646 kenvEnv: {647 KIT_SEARCH_MAX_ITERATIONS: '3',648 KIT_SEARCH_MIN_SCORE: '0.6',649 },650 },651}));652653// Fix lodash debounce mock for search654vi.mock('lodash-es', () => ({655 debounce: vi.fn((fn) => {656 const mockDebounced = vi.fn(fn) as any;657 mockDebounced.cancel = vi.fn();658 return mockDebounced;659 }),660}));661```662663### Search Test Object Requirements664Ensure complete test objects for search functionality:665666```typescript667// Complete KitPrompt mock for search testing668const mockPrompt: KitPrompt = {669 ui: UI.arg,670 pid: 12345,671 scriptPath: '/test/script.ts',672 sendToPrompt: mockSendToPrompt,673 kitSearch: {674 input: '',675 inputRegex: undefined,676 keyword: '',677 keywordCleared: false,678 generated: false,679 flaggedValue: '',680 choices: [],681 scripts: [],682 qs: null,683 hasGroup: false, // Critical for info choice handling684 keys: ['name', 'keyword', 'tag'],685 keywords: new Map(),686 triggers: new Map(),687 postfixes: new Map(),688 shortcodes: new Map(),689 },690 flagSearch: {691 input: '',692 choices: [],693 hasGroup: false,694 qs: null,695 },696 updateShortcodes: vi.fn(),697} as unknown as KitPrompt;698```699700## Performance Testing Success Metrics701702### Search Performance Targets703- ✅ Average search time < 50ms (10,000 choices)704- ✅ Individual searches < 100ms705- ✅ Progressive typing responsive (< 75ms average)706- ✅ Memory growth < 200MB during intensive testing707- ✅ <10% of searches should be considered "slow" (>100ms)708- ✅ Scaling: Max 0.02ms per choice709710### Integration Testing Coverage711- ✅ UI input → IPC → Search → Results flow712- ✅ Search state management (hasGroup, choices, etc.)713- ✅ Info choice prioritization714- ✅ Hide-without-input behavior715- ✅ Empty/whitespace input handling716- ✅ Progressive typing simulation717- ✅ Memory leak prevention718719### Example Performance Test Files720- [search-performance.bench.ts](mdc:src/main/search-performance.bench.ts) - Pure benchmarks721- [search-performance.test.ts](mdc:src/main/search-performance.test.ts) - Performance tests with assertions722- [search-integration.test.ts](mdc:src/main/search-integration.test.ts) - End-to-end integration723- [ipc-input.test.ts](mdc:src/main/ipc-input.test.ts) - IPC message handling724
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| script-kit/app.cursor/rules/debugging-logs.mdc · 97 | Cursor rules | archagent-behaviour | 54/100 | today | |
| script-kit/app.cursor/rules/debugging-workflow.mdc · 97 | Cursor rules | lint-formatarchagent-behaviour | 58/100 | today | |
| script-kit/app.cursor/rules/development-workflow.mdc · 97 | Cursor rules | lint-formatstyletypesagent-behaviour | 61/100 | today | |
| script-kit/app.cursor/rules/search.mdc · 97 | Cursor rules | teststyletesting-strategyperformance | 62/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 14 days ago | |
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 46 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 14 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 14 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 14 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 46 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/script-kit-app-cursor-rules-testing)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.