@skilly-hand/skilly-hand 0.6.1 → 0.8.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.
@@ -0,0 +1,159 @@
1
+ # Test-Driven Development Guide
2
+
3
+ ## When to Use
4
+
5
+ Use this skill when:
6
+
7
+ - Implementing a new feature, service, component, or function from scratch.
8
+ - Adding behavior to existing code where the expected outcome can be defined upfront.
9
+ - Debugging a regression by writing a failing test that reproduces the bug first.
10
+ - Reviewing or pair-programming on code where test-first discipline is required.
11
+
12
+ Do not use this skill for:
13
+
14
+ - Exploratory prototyping where requirements are entirely undefined.
15
+ - Snapshot or visual regression tests driven by existing UI.
16
+ - Infrastructure or environment setup with no testable behavior.
17
+
18
+ ---
19
+
20
+ ## Critical Patterns
21
+
22
+ ### Pattern 1: RED First, Always
23
+
24
+ Write a failing test before writing any implementation code. This proves:
25
+
26
+ - The test is meaningful (not passing by accident).
27
+ - The feature is actually needed.
28
+ - You understand the requirements before touching implementation.
29
+
30
+ Never write implementation code without a failing test that demands it.
31
+
32
+ ### Pattern 2: Minimum Code to GREEN
33
+
34
+ Write the **smallest possible code** to make the test pass:
35
+
36
+ - No extra features beyond what the test requires.
37
+ - No premature optimization or defensive handling.
38
+ - No "while I'm here, let me add…" additions.
39
+
40
+ The goal is to satisfy the test, nothing more.
41
+
42
+ ### Pattern 3: REFACTOR With Tests GREEN
43
+
44
+ Only improve code structure **after** all tests pass:
45
+
46
+ - Extract constants, improve naming, simplify logic.
47
+ - Tests must stay green throughout every refactoring step.
48
+ - If a refactor breaks a test, revert — the refactor was wrong.
49
+
50
+ ### Pattern 4: One Scenario Per Test
51
+
52
+ Each test must validate exactly one behavior:
53
+
54
+ - Use explicit GIVEN / WHEN / THEN structure in test bodies.
55
+ - A test name should complete the sentence: *"it should ___"*.
56
+ - If a test asserts two behaviors, split it into two tests.
57
+
58
+ ---
59
+
60
+ ## Decision Tree
61
+
62
+ ```text
63
+ Starting a new feature or function? -> Write failing test first (RED)
64
+ Test is failing as expected? -> Write minimum code to pass (GREEN)
65
+ Test is passing? -> Improve code structure without changing behavior (REFACTOR)
66
+ Refactor broke a test? -> Revert — refactor introduced a behavior change
67
+ Test is already passing before writing code? -> Test is not meaningful; redesign it
68
+ Fixing a bug? -> Write failing test that reproduces the bug first
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Code Examples
74
+
75
+ ### Example 1: GIVEN / WHEN / THEN Structure
76
+
77
+ ```typescript
78
+ it('should return the sum of two numbers', () => {
79
+ // GIVEN: Two positive integers
80
+ const a = 3;
81
+ const b = 4;
82
+
83
+ // WHEN: Sum is computed
84
+ const result = add(a, b);
85
+
86
+ // THEN: Result equals their sum
87
+ expect(result).toBe(7);
88
+ });
89
+ ```
90
+
91
+ ### Example 2: RED — Write Failing Test First
92
+
93
+ ```typescript
94
+ // calculator.test.ts
95
+ import { divide } from './calculator';
96
+
97
+ it('should throw when dividing by zero', () => {
98
+ // GIVEN / WHEN / THEN
99
+ expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
100
+ });
101
+ ```
102
+
103
+ Run the test — it **must** fail before writing any implementation.
104
+
105
+ ### Example 3: GREEN — Write Minimum Implementation
106
+
107
+ ```typescript
108
+ // calculator.ts
109
+ export function divide(a: number, b: number): number {
110
+ if (b === 0) throw new Error('Cannot divide by zero');
111
+ return a / b;
112
+ }
113
+ ```
114
+
115
+ Run the test — it should now pass. No additional logic yet.
116
+
117
+ ### Example 4: REFACTOR — Improve Without Changing Behavior
118
+
119
+ ```typescript
120
+ // calculator.ts (refactored)
121
+ const DIVIDE_BY_ZERO_MESSAGE = 'Cannot divide by zero';
122
+
123
+ export function divide(numerator: number, denominator: number): number {
124
+ if (denominator === 0) throw new Error(DIVIDE_BY_ZERO_MESSAGE);
125
+ return numerator / denominator;
126
+ }
127
+ ```
128
+
129
+ Run the test — it must still pass after renaming and extracting the constant.
130
+
131
+ ---
132
+
133
+ ## Commands
134
+
135
+ ```bash
136
+ # Run a single test file
137
+ npm test -- {test-file}
138
+
139
+ # Run all tests
140
+ npm test
141
+
142
+ # Run tests in watch mode
143
+ npm test -- --watch
144
+
145
+ # Type check without emitting
146
+ npx tsc --noEmit
147
+
148
+ # Lint check
149
+ npm run lint
150
+
151
+ # Full verify (tests + lint + type check + build)
152
+ npm test && npm run lint && npx tsc --noEmit && npm run build
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Resources
158
+
159
+ - Full cycle examples with Angular: [assets/tdd-cycle.md](assets/tdd-cycle.md)
@@ -0,0 +1,487 @@
1
+ # TDD Templates: RED → GREEN → REFACTOR Cycles
2
+
3
+ Real examples of the RED → GREEN → REFACTOR TDD cycle for implementing components, services, and pipes.
4
+
5
+ **Pattern**: Write failing test → Write minimum code to pass → Refactor while tests stay green
6
+
7
+ ---
8
+
9
+ ## Example 1: Simple Pipe Component
10
+
11
+ ### Scenario: AsyncCachePipe
12
+ Cache hot observables to prevent resubscription overhead.
13
+
14
+ #### Step 1: RED — Write Failing Test
15
+
16
+ ```typescript
17
+ // async-cache.pipe.test.ts
18
+ import { TestBed } from '@angular/core/testing';
19
+ import { Subject, take } from 'rxjs';
20
+ import { AsyncCachePipe } from './async-cache.pipe';
21
+
22
+ describe('AsyncCachePipe', () => {
23
+ let pipe: AsyncCachePipe;
24
+
25
+ beforeEach(() => {
26
+ TestBed.configureTestingModule({});
27
+ pipe = TestBed.runInInjectionContext(() => new AsyncCachePipe());
28
+ });
29
+
30
+ it('should cache async values and replay on late subscriptions', (done) => {
31
+ // GIVEN: An observable that emits after 100ms
32
+ let emitCount = 0;
33
+ const source$ = new Subject<number>();
34
+
35
+ // WHEN: First subscription
36
+ const result$ = pipe.transform(source$);
37
+ const values: number[] = [];
38
+
39
+ const sub1 = result$.subscribe((val) => {
40
+ emitCount++;
41
+ values.push(val);
42
+ });
43
+
44
+ setTimeout(() => {
45
+ source$.next(42);
46
+ }, 100);
47
+
48
+ // WHEN: Second subscription arrives after value is cached
49
+ setTimeout(() => {
50
+ const sub2 = result$.pipe(take(1)).subscribe(() => {
51
+ // THEN: Second subscriber gets cached value immediately
52
+ expect(emitCount).toBe(1);
53
+ expect(values[0]).toBe(42);
54
+
55
+ sub1.unsubscribe();
56
+ sub2.unsubscribe();
57
+ source$.complete();
58
+ done();
59
+ });
60
+ }, 200);
61
+ });
62
+
63
+ // ❌ TEST FAILS HERE - transform() not implemented yet
64
+ });
65
+ ```
66
+
67
+ **Run**: `npm test -- async-cache.pipe.test.ts`
68
+ ```
69
+ FAIL async-cache.pipe.test.ts
70
+ should cache async values and replay on late subscriptions
71
+ TypeError: pipe.transform is not a function
72
+ ```
73
+
74
+ ✅ Test fails as expected (proves test is meaningful)
75
+
76
+ ---
77
+
78
+ #### Step 2: GREEN — Write Minimum Code
79
+
80
+ ```typescript
81
+ // async-cache.pipe.ts
82
+ import { Pipe, PipeTransform } from '@angular/core';
83
+ import { Observable, ReplaySubject } from 'rxjs';
84
+ import { tap } from 'rxjs/operators';
85
+
86
+ @Pipe({
87
+ name: 'asyncCache',
88
+ standalone: true,
89
+ })
90
+ export class AsyncCachePipe implements PipeTransform {
91
+ // ✅ MINIMUM: Just make the test pass
92
+ transform<T>(source: Observable<T>): Observable<T> {
93
+ const cached$ = new ReplaySubject<T>(1);
94
+ source.pipe(tap((val) => cached$.next(val))).subscribe();
95
+ return cached$.asObservable();
96
+ }
97
+ }
98
+ ```
99
+
100
+ **Run**: `npm test -- async-cache.pipe.test.ts`
101
+ ```
102
+ PASS async-cache.pipe.test.ts
103
+ should cache async values and replay on late subscriptions ✓
104
+
105
+ Tests: 1 passed, 1 total
106
+ ```
107
+
108
+ ✅ Test passes (behavior is correct, but code needs cleanup)
109
+
110
+ ---
111
+
112
+ #### Step 3: REFACTOR — Improve Without Changing Behavior
113
+
114
+ ```typescript
115
+ // async-cache.pipe.ts (refactored)
116
+ import { Pipe, PipeTransform } from '@angular/core';
117
+ import { Observable, ReplaySubject, Subject, takeUntil } from 'rxjs';
118
+ import { tap } from 'rxjs/operators';
119
+
120
+ @Pipe({
121
+ name: 'asyncCache',
122
+ standalone: true,
123
+ })
124
+ export class AsyncCachePipe implements PipeTransform {
125
+ private destroy$ = new Subject<void>();
126
+
127
+ // ✅ REFACTORED: Cleaner, adds cleanup
128
+ transform<T>(source: Observable<T>): Observable<T> {
129
+ const cacheSize = 1;
130
+ const cached$ = new ReplaySubject<T>(cacheSize);
131
+
132
+ source
133
+ .pipe(
134
+ tap((val) => cached$.next(val)),
135
+ takeUntil(this.destroy$)
136
+ )
137
+ .subscribe();
138
+
139
+ return cached$.asObservable();
140
+ }
141
+
142
+ ngOnDestroy(): void {
143
+ this.destroy$.next();
144
+ this.destroy$.complete();
145
+ }
146
+ }
147
+ ```
148
+
149
+ **Run**: `npm test -- async-cache.pipe.test.ts`
150
+ ```
151
+ PASS async-cache.pipe.test.ts
152
+ should cache async values and replay on late subscriptions ✓
153
+ ```
154
+
155
+ ✅ Test still passes
156
+
157
+ **What improved**:
158
+ - Constants extracted (`cacheSize`)
159
+ - Cleanup logic added (`takeUntil`, `ngOnDestroy`)
160
+
161
+ ---
162
+
163
+ ## Example 2: Component with Input/Output
164
+
165
+ ### Scenario: AlertComponent
166
+ Display a dismissible alert with a message.
167
+
168
+ #### Step 1: RED — Write Failing Test
169
+
170
+ ```typescript
171
+ // alert.component.test.ts
172
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
173
+ import { AlertComponent } from './alert.component';
174
+
175
+ describe('AlertComponent', () => {
176
+ let component: AlertComponent;
177
+ let fixture: ComponentFixture<AlertComponent>;
178
+
179
+ beforeEach(async () => {
180
+ await TestBed.configureTestingModule({
181
+ imports: [AlertComponent],
182
+ }).compileComponents();
183
+
184
+ fixture = TestBed.createComponent(AlertComponent);
185
+ component = fixture.componentInstance;
186
+ });
187
+
188
+ it('should display the alert message', () => {
189
+ // GIVEN: Component with message input
190
+ component.message = 'Error: Invalid input';
191
+ fixture.detectChanges();
192
+
193
+ // WHEN: Component renders
194
+ const messageEl = fixture.debugElement.nativeElement.querySelector('[data-testid="alert-message"]');
195
+
196
+ // THEN: Message is visible
197
+ expect(messageEl?.textContent).toBe('Error: Invalid input');
198
+ });
199
+
200
+ // ❌ FAILS - Component doesn't exist yet
201
+ });
202
+ ```
203
+
204
+ **Run**: `npm test -- alert.component.test.ts`
205
+ ```
206
+ FAIL alert.component.test.ts
207
+ Cannot find component 'AlertComponent'
208
+ ```
209
+
210
+ ---
211
+
212
+ #### Step 2: GREEN — Write Minimum Implementation
213
+
214
+ ```typescript
215
+ // alert.component.ts
216
+ import { Component, input } from '@angular/core';
217
+
218
+ @Component({
219
+ selector: 'app-alert',
220
+ standalone: true,
221
+ template: `<div data-testid="alert-message">{{ message() }}</div>`,
222
+ })
223
+ export class AlertComponent {
224
+ // ✅ MINIMUM: Just the message input
225
+ message = input<string>('');
226
+ }
227
+ ```
228
+
229
+ **Run**: `npm test -- alert.component.test.ts`
230
+ ```
231
+ PASS alert.component.test.ts
232
+ should display the alert message ✓
233
+ ```
234
+
235
+ ---
236
+
237
+ #### Step 3: REFACTOR — Add Accessibility and Dismiss
238
+
239
+ ```typescript
240
+ // alert.component.ts (refactored)
241
+ import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
242
+
243
+ @Component({
244
+ selector: 'app-alert',
245
+ standalone: true,
246
+ template: `
247
+ <div
248
+ class="alert"
249
+ role="alert"
250
+ aria-live="polite"
251
+ [attr.data-testid]="'alert-message'"
252
+ [class]="'alert--' + severity()">
253
+ <p class="alert__message">{{ message() }}</p>
254
+ <button class="alert__close" aria-label="Dismiss alert" (click)="onDismiss()">✕</button>
255
+ </div>
256
+ `,
257
+ changeDetection: ChangeDetectionStrategy.OnPush,
258
+ })
259
+ export class AlertComponent {
260
+ message = input<string>('');
261
+ severity = input<'success' | 'error' | 'warning' | 'info'>('info');
262
+ dismissed = output<void>();
263
+
264
+ onDismiss(): void {
265
+ this.dismissed.emit();
266
+ }
267
+ }
268
+ ```
269
+
270
+ **Run**: `npm test -- alert.component.test.ts`
271
+ ```
272
+ PASS alert.component.test.ts
273
+ should display the alert message ✓
274
+ ```
275
+
276
+ ✅ Test still passes
277
+
278
+ **What improved**:
279
+ - Accessibility attributes (`role="alert"`, `aria-live`, `aria-label`)
280
+ - Dismiss output and handler
281
+ - Severity levels
282
+ - `ChangeDetectionStrategy.OnPush`
283
+
284
+ ---
285
+
286
+ ## Example 3: Service with State Management
287
+
288
+ ### Scenario: TodoService
289
+ Manage a todo list with add and complete operations.
290
+
291
+ #### Step 1: RED — Multiple Scenarios
292
+
293
+ ```typescript
294
+ // todo.service.test.ts
295
+ import { TestBed } from '@angular/core/testing';
296
+ import { TodoService } from './todo.service';
297
+
298
+ describe('TodoService', () => {
299
+ let service: TodoService;
300
+
301
+ beforeEach(() => {
302
+ TestBed.configureTestingModule({ providers: [TodoService] });
303
+ service = TestBed.inject(TodoService);
304
+ });
305
+
306
+ it('should add a todo and update the list', () => {
307
+ // GIVEN: Empty list
308
+ // WHEN: Add todo
309
+ service.addTodo('Buy milk');
310
+
311
+ // THEN: Todo appears
312
+ expect(service.todos()).toContainEqual(
313
+ jasmine.objectContaining({ text: 'Buy milk', done: false })
314
+ );
315
+ });
316
+
317
+ it('should mark a todo as complete', () => {
318
+ // GIVEN: A todo in the list
319
+ service.addTodo('Buy milk');
320
+ const firstTodo = service.todos()[0];
321
+
322
+ // WHEN: Mark as complete
323
+ service.completeTodo(firstTodo.id);
324
+
325
+ // THEN: Todo is done
326
+ expect(service.todos()[0].done).toBe(true);
327
+ });
328
+
329
+ // ❌ FAILS - Service methods don't exist
330
+ });
331
+ ```
332
+
333
+ **Run**: `npm test -- todo.service.test.ts`
334
+ ```
335
+ FAIL todo.service.test.ts
336
+ service.addTodo is not a function
337
+ ```
338
+
339
+ ---
340
+
341
+ #### Step 2: GREEN — Write Minimum Implementation
342
+
343
+ ```typescript
344
+ // todo.service.ts
345
+ import { Injectable, signal } from '@angular/core';
346
+
347
+ interface Todo { id: number; text: string; done: boolean; }
348
+
349
+ @Injectable({ providedIn: 'root' })
350
+ export class TodoService {
351
+ todos = signal<Todo[]>([]);
352
+ private nextId = 1;
353
+
354
+ addTodo(text: string): void {
355
+ this.todos.update((current) => [...current, { id: this.nextId++, text, done: false }]);
356
+ }
357
+
358
+ completeTodo(id: number): void {
359
+ this.todos.update((current) =>
360
+ current.map((todo) => todo.id === id ? { ...todo, done: true } : todo)
361
+ );
362
+ }
363
+ }
364
+ ```
365
+
366
+ **Run**: `npm test -- todo.service.test.ts`
367
+ ```
368
+ PASS todo.service.test.ts
369
+ should add a todo and update the list ✓
370
+ should mark a todo as complete ✓
371
+ ```
372
+
373
+ ---
374
+
375
+ #### Step 3: REFACTOR — Add Validation and Persistence
376
+
377
+ ```typescript
378
+ // todo.service.ts (refactored)
379
+ import { Injectable, signal } from '@angular/core';
380
+
381
+ interface Todo {
382
+ id: number;
383
+ text: string;
384
+ done: boolean;
385
+ createdAt: Date;
386
+ completedAt?: Date;
387
+ }
388
+
389
+ @Injectable({ providedIn: 'root' })
390
+ export class TodoService {
391
+ todos = signal<Todo[]>([]);
392
+ private nextId = 1;
393
+ private readonly MAX_TODOS = 100;
394
+
395
+ constructor() {
396
+ this.loadFromStorage();
397
+ }
398
+
399
+ addTodo(text: string): void {
400
+ if (!text?.trim()) throw new Error('Todo text cannot be empty');
401
+ if (this.todos().length >= this.MAX_TODOS) throw new Error(`Cannot exceed ${this.MAX_TODOS} todos`);
402
+
403
+ this.todos.update((current) => [
404
+ ...current,
405
+ { id: this.nextId++, text: text.trim(), done: false, createdAt: new Date() },
406
+ ]);
407
+ this.saveToStorage();
408
+ }
409
+
410
+ completeTodo(id: number): void {
411
+ if (!this.todos().find((t) => t.id === id)) throw new Error(`Todo with id ${id} not found`);
412
+
413
+ this.todos.update((current) =>
414
+ current.map((t) => t.id === id ? { ...t, done: true, completedAt: new Date() } : t)
415
+ );
416
+ this.saveToStorage();
417
+ }
418
+
419
+ deleteTodo(id: number): void {
420
+ this.todos.update((current) => current.filter((t) => t.id !== id));
421
+ this.saveToStorage();
422
+ }
423
+
424
+ private loadFromStorage(): void {
425
+ try {
426
+ const stored = localStorage.getItem('todos');
427
+ if (stored) this.todos.set(JSON.parse(stored));
428
+ } catch (e) {
429
+ console.error('Failed to load todos from storage', e);
430
+ }
431
+ }
432
+
433
+ private saveToStorage(): void {
434
+ try {
435
+ localStorage.setItem('todos', JSON.stringify(this.todos()));
436
+ } catch (e) {
437
+ console.error('Failed to save todos to storage', e);
438
+ }
439
+ }
440
+ }
441
+ ```
442
+
443
+ **Run**: `npm test -- todo.service.test.ts`
444
+ ```
445
+ PASS todo.service.test.ts
446
+ should add a todo and update the list ✓
447
+ should mark a todo as complete ✓
448
+ ```
449
+
450
+ ✅ Tests still pass
451
+
452
+ **What improved**:
453
+ - Input validation (empty check, max limit)
454
+ - Timestamps (`createdAt`, `completedAt`)
455
+ - Persistence via `localStorage`
456
+ - `deleteTodo` added for completeness
457
+
458
+ ---
459
+
460
+ ## Quick Checklist: RED → GREEN → REFACTOR
461
+
462
+ ```markdown
463
+ ### For Each Task Using TDD
464
+
465
+ #### RED Phase
466
+ - [ ] Write test that describes the feature
467
+ - [ ] Test has explicit GIVEN / WHEN / THEN
468
+ - [ ] Run tests — FAILS as expected
469
+ - [ ] Failure proves test is meaningful
470
+
471
+ #### GREEN Phase
472
+ - [ ] Write minimum code to pass
473
+ - [ ] No extra features beyond test requirement
474
+ - [ ] Run tests — PASSES
475
+
476
+ #### REFACTOR Phase
477
+ - [ ] Improve code structure / naming
478
+ - [ ] Extract constants, simplify logic
479
+ - [ ] Run tests — STILL PASSES
480
+ - [ ] No behavior changes, only improvements
481
+
482
+ #### Verify
483
+ - [ ] All tests pass: `npm test`
484
+ - [ ] Lint passes: `npm run lint`
485
+ - [ ] Type check: `npx tsc --noEmit`
486
+ - [ ] Build succeeds: `npm run build`
487
+ ```
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "test-driven-development",
3
+ "title": "Test-Driven Development",
4
+ "description": "Guide implementation using the RED → GREEN → REFACTOR TDD cycle: write a failing test first, write the minimum code to pass, then refactor while tests stay green.",
5
+ "portable": true,
6
+ "tags": ["testing", "workflow", "quality", "core"],
7
+ "detectors": ["always"],
8
+ "detectionTriggers": ["manual"],
9
+ "installsFor": ["all"],
10
+ "agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
11
+ "skillMetadata": {
12
+ "author": "skilly-hand",
13
+ "last-edit": "2026-04-04",
14
+ "license": "Apache-2.0",
15
+ "version": "1.0.0",
16
+ "changelog": "Initial TDD skill ported from legacy scannlab-sdd tdd-templates; enables RED→GREEN→REFACTOR workflow across any stack; affects catalog skill coverage for test-first development",
17
+ "auto-invoke": "Implementing features, services, or components using test-driven development (TDD) or RED→GREEN→REFACTOR cycles",
18
+ "allowed-tools": ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
19
+ },
20
+ "files": [
21
+ { "path": "SKILL.md", "kind": "instruction" },
22
+ { "path": "assets/tdd-cycle.md", "kind": "asset" }
23
+ ],
24
+ "dependencies": []
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/skilly-hand",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "publishConfig": {