ワンクリックで
planning
Provides expertise on how to plan for work in this repo
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Provides expertise on how to plan for work in this repo
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | planning |
| description | Provides expertise on how to plan for work in this repo |
This skill provides comprehensive guidance on how to plan a new feature in this repo.
Runtime tests verify the actual behavior of code during execution.
When to use:
Tools:
pnpm test - runs all runtime testspnpm test GLOB - runs tests matching the glob patternpnpm test:watch - runs tests in watch modeExample structure:
import { describe, it, expect } from "vitest";
import { prettyPath } from "~/utils";
describe("prettyPath()", () => {
it("should format a path with directory and filename", () => {
const result = prettyPath("/path/to/file.ts");
expect(result).toBe("/path/to/file.ts"); // Expected formatting
});
it("should handle edge case: empty string", () => {
const result = prettyPath("");
expect(result).toBe("");
});
it("should handle edge case: root path", () => {
const result = prettyPath("/");
expect(result).toBe("/");
});
});
Type tests verify the type correctness of TypeScript code at design time.
pnpm test:types - runs all type testspnpm test:types GLOB - runs type tests matching the glob patternThis section will show you, layer by layer, how to compose and build good type tests.
cases blockAll type tests in a given it test block (defined by Vitest) will have a type called cases defined as an array of type tests.
type cases = [
// ... type tests go here
]
Note: our linting rules allow for the name
casesto be defined without being used; this is intentional and a good thing.
Expect<...> wrapperEvery type test will be wrapped by an Expect type utility.
type cases = [
Expect<...>,
Expect<...>,
// ...
]
The inferred-types library provides a number of useful assertion utilities you can use to create your tests:
AssertTrue<T> ****
T is the type trueAssertFalse<T>
T is the type falseAssertEqual<T,E>
T equals the expected type EAssertExtends<T,E>
T extends the expected type EAssertSameValues<T,E>
T is an array type and every element of E and T are the same but the order in which they arrive does not matterAssertContains<T,E>
T is a string:
E is also a string and represents a sub-string of the sting literal TT is an array then:
In all cases you put the test assertion inside of the Expect utility:
type cases [
Expect<AssertTrue<T>>,
Expect<AssertExtends<T, string>>,
// ...
]
In our example we'll just test a built-in type utility of Typescript's named Capitalize<T>.
import type { Expect, Equal } from "inferred-types/types";
describe("Example 1", () => {
it("string literals", () => {
type Lowercase = Capitalize<"foo">;
type AlreadyCapitalized = Capitalize<"Foo">;
type cases = [
Expect<AssertEqual<Lowercase, "Foo">>,
Expect<AssertEqual<AlreadyCapitalized, "Foo">>,
]
});
it("wide string", () => {
type Wide = Capitalize<string>;
type cases = [
Expect<AssertEqual<Wide, string>>
]
})
it("only first letter capitalized", () => {
type SpaceThenLetter = Capitalize<" foo">;
type TabThenLetter = Capitalize<"\tfoo">;
type cases = [
Expect<AssertEqual<SpaceThenLetter, " foo">>,
Expect<AssertEqual<TabThenLetter, "\tfoo">>,
]
})
});
IMPORTANT: in the example above we were testing a type utility (where a type utility is any type which accepts generics and uses them to produce a type); and with type utilities you CAN'T do runtime testing because there is no runtime component to test. However, we do still use the Vitest primitives of describe and it to organize the test.
Let's imagine we create a simple function:
capitalize<T extends string>(text: T): Capitalize<T>.describe("example", () => {
it("leading alpha character", () => {
const lowercase = capitalize("foo");
const alreadyCapitalized = capitalize("Foo");
expect(lowercase).toEqual("Foo");
expect(alreadyCapitalized).toEqual("Foo");
type cases = [
Expect<AssertEqual<typeof lowercase, "Foo">>,
Expect<AssertEqual<typeof alreadyCapitalized, "Foo">>,
]
});
it("wide string", () => {
const wide = capitalize("foo" as string);
expect(wide).toBe("Foo");
type cases = [
Expect<AssertEqual<typeof wide, string>>
]
})
it("non-alpha leading character", () => {
const spaceThenLetter = capitalize(" foo");
const tabThenLetter = capitalize("\tfoo");
expect(spaceThenLetter).toBe(" foo");
expect(tabThenLetter).toBe("\tfoo");
type cases = [
Expect<AssertEqual<typeof spaceThenLetter, " foo">>,
Expect<AssertEqual<typeof tabThenLetter, "\tfoo">>,
]
})
})
IMPORTANT: in these sorts of tests the runtime and type tests naturally fit into the same describe/it blocks. You should almost NEVER have a set of runtime tests in one structure, and then a set of type tests in another. This almost always indicates someone who doesn't understand type testing well enough yet.
IMPORTANT: in both examples we've see a test structure where define intermediate variable/types which assume the value/type of the "test". Then we use the variable/type in our tests. We could possibly just inline the expression you're testing into the runtime and type tests but this can actually have undesirable side effects in some cases but having the intermediate variables/types defined first allows a human observer to hover over the variable to see what type resolution there was. This is highly valuable!
❌ WRONG - Separated structure:
describe("myFunction()", () => {
describe("Runtime tests", () => {
it("should work", () => {
expect(myFunction("test")).toBe("result");
});
});
describe("Type Tests", () => { // ❌ WRONG!
it("should have correct type", () => {
const result = myFunction("test");
const _check: typeof result extends string ? true : false = true;
expect(_check).toBe(true); // ❌ This is NOT a type test!
});
});
});
✅ CORRECT - Integrated structure:
describe("myFunction()", () => {
it("should work with string input", () => {
const result = myFunction("test");
// Runtime test
expect(result).toBe("result");
// Type test - in the SAME it() block
type cases = [
Expect<AssertEqual<typeof result, "result">>
];
});
});
❌ WRONG:
const result = myFunction("test");
const _isString: typeof result extends string ? true : false = true;
expect(_isString).toBe(true); // This is runtime testing, not type testing!
✅ CORRECT:
const result = myFunction("test");
type cases = [
Expect<AssertExtends<typeof result, string>>
];
cases Array❌ WRONG:
Expect<AssertEqual<typeof result, "expected">>; // Not in cases array!
✅ CORRECT:
type cases = [
Expect<AssertEqual<typeof result, "expected">>
];
Before submitting ANY work with type tests, verify:
type cases = [...]?Expect<Assert...>?inferred-types/types?pnpm test:types show "🎉 No errors!"?If any check fails, the type tests are incorrect and must be rewritten.
Use this flowchart to determine what tests you need:
Is the symbol exported from the module?
│
├─ NO → Consider if it needs tests at all
│ (internal helpers may not need dedicated tests)
│
└─ YES → What kind of symbol is it?
│
├─ Type Utility (e.g., a type which takes generics)
│ └─ Write TYPE TESTS always; no RUNTIME tests are even possible!
│
├─ Constant (literal value)
│ └─ Usually NO tests needed
│ (unless it's a complex computed value)
│
├─ Function / Arrow Function
│ └─ Does it return a literal type?
│ ├─ YES → Write BOTH runtime AND type tests
│ └─ NO → Write RUNTIME tests (minimum); possibly write type tests
│
├─ Class
│ └─ Does it use generics or have methods which return literal types?
│ ├─ YES → Write BOTH runtime AND type tests
│ └─ NO → Write RUNTIME tests primarily
│
└─ Interface / Type Definition (e.g., a type without a generic input)
└─ Usually NO test needed; if there is no generic then there is no variance to test
└─ Only exception might be when the type being defined uses a lot of type utilities in it's definition. In these cases, you _might_ test that the type is not an `any` or `never` type because the underlying utilities
Rule of thumb: When in doubt, write tests. It's better to have coverage than to skip it.
Tests are organized by feature/command area:
tests/
├── unit/
│ ├── test-command/ # Tests for the 'test' CLI command
│ ├── symbol-command/ # Tests for the 'symbols' CLI command
│ ├── source-command/ # Tests for the 'source' CLI command
│ ├── utils/ # Tests for utility functions
│ └── WIP/ # Temporary location for in-progress phase tests
├── integration/
│ ├── fast/ # Fast integration tests (<2s each)
│ └── *.test.ts # Full integration tests
└── fixtures/ # Test fixtures and sample projects
*.test.ts*.fast.test.tsit("should return true when...)it("should handle empty arrays")DO:
describe blocksDON'T:
When implementing a new feature, ALWAYS follow this comprehensive TDD workflow:
When a user provides you with a new feature or plan idea, the first step is always to take that as an input into making a more structured and formalized plan:
.ai/plans/${YYYY}-${MM}-${DD}-${NAME}.mdtxt if you are simply using the block as a text output.The remaining steps represent how each PHASE of the plan should be structured.
Capture the current state of all tests before making any changes.
Actions:
Run all runtime tests:
pnpm test
Run all type tests:
pnpm test:types
Create a simple XML representation of test results distinguishing between runtime and type test runs
Document any existing failures (these are your baseline - don't fix yet)
Purpose: Establish a clear baseline so you can detect regressions and measure progress.
Create a log file to track this phase of work.
Actions:
Create log file with naming convention:
mkdir -p .ai/logs
touch .ai/logs/YYYY-MM-planName-phaseN-log.md
Example: .ai/logs/2025-10-symbol-filtering-phase1-log.md
Add ## Starting Test Position section with XML code block containing test results from SNAPSHOT
Add ## Repo Starting Position section
Run the start-position script to capture git state:
bun run .claude/skills/scripts/start-position.ts planName phaseNumber
This returns markdown content showing:
Append the start-position output to the log file
Purpose: Create a detailed record of the starting point for debugging and tracking progress.
Write tests FIRST before any implementation. This is true Test-Driven Development.
Actions:
Understand existing test structure:
Create tests in WIP directory:
tests/unit/WIP/pnpm test WIPpnpm test --exclude WIPWrite comprehensive test coverage:
Verify tests FAIL initially:
pnpm test WIPExample WIP structure:
tests/unit/WIP/
├── phase1-cli-options.test.ts
├── phase1-filter-logic.test.ts
└── phase1-integration.test.ts
Purpose: Tests define the contract and expected behavior before any code is written.
Use the tests to guide your implementation.
Actions:
Implement minimal code to pass each test:
Iterate rapidly:
pnpm test WIPpnpm test:types WIPContinue until all phase tests pass:
tests/unit/WIP/ should be greenRefactor with confidence:
Purpose: Let tests drive the implementation, ensuring you build exactly what's needed.
Verify completeness, check for regressions, and finalize the phase.
🚨 CRITICAL WARNING: DO NOT MIGRATE TESTS AUTOMATICALLY 🚨
Tests MUST remain in tests/unit/WIP/ until the user explicitly reviews and approves them. Even if the user says "closeout this phase" or "finish up" - DO NOT migrate tests. Only migrate after user says "migrate the tests" or explicitly approves migration.
Actions:
Run full test suite:
pnpm test # All runtime tests
pnpm test:types # All type tests
Handle any regressions:
If existing tests now fail:
## Regressions FoundUpdate the log file:
Add a ## Phase Completion section with:
tests/unit/WIP/ (awaiting user review)Report completion to user:
Inform the user that the phase is complete with a summary of:
tests/unit/WIP/ awaiting reviewCRITICAL: Tests remain in WIP directory until user reviews:
tests/unit/WIP/ until the user has reviewed and approved themTest migration (only after user approval):
When the user approves the tests:
tests/unit/WIP/ to their permanent homestests/unit/WIP/ directoryPurpose: Ensure quality, prevent regressions, and properly integrate work into the codebase.
Prefer real implementations over mocks: Only mock external dependencies (APIs, file system, databases). Keep internal code integration real.
Use realistic test data: Mirror actual usage patterns. If your function processes user objects, use realistic user data in tests.
One behavior per test: Each it() block should test a single specific behavior. This makes failures easier to diagnose.
Tests should be deterministic: Same input = same output, every time. Avoid depending on current time, random values, or external state unless that's what you're testing.
Keep tests independent: Each test should be able to run in isolation. Use beforeEach() for setup, not shared variables.
Test the contract, not the implementation: If you change HOW something works but it still behaves the same, tests shouldn't break.
Prioritize fixing source code over changing tests: When tests fail, your first instinct should be to fix the implementation to meet the test's expectation, not to change the test to match the implementation.
Understand failures deeply: Don't just read the error message - understand WHY the test is failing. Use debugging, logging, or step through the code if needed.
Document complex test scenarios: If a test needs explanation, add a comment describing what scenario it's covering and why it matters.
Keep unit tests fast: Unit tests should run in milliseconds. If a test is slow, it's likely testing too much or hitting external resources.
Separate fast and slow tests: Integration tests can be slower. Keep them in separate files (e.g., *.fast.test.ts vs *.test.ts).
Use focused test runs during development: Don't run the entire suite on every change. Use glob patterns to run just what you're working on.
Always test the positive case: Verify that valid types are accepted and produce the expected result type.
Test the negative case when relevant: Use @ts-expect-error to verify that invalid types are properly rejected.
Test edge cases in type logic: Empty objects, never, unknown, union types, etc.
Keep type tests close to runtime tests: When testing a function with both runtime and type tests, keep them in the same file within the same describe block for cohesion.
it("should throw error for invalid input", () => {
expect(() => parseConfig("invalid")).toThrow("Invalid config format");
});
it("should return error result for invalid type", () => {
const result = safeParseConfig("invalid");
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain("Invalid config");
}
});
it("should resolve with data on success", async () => {
const result = await fetchUser(123);
expect(result.id).toBe(123);
expect(result.name).toBeDefined();
});
it("should reject with error on failure", async () => {
await expect(fetchUser(-1)).rejects.toThrow("User not found");
});
it("should narrow type based on discriminant", () => {
type Result = { success: true; data: string } | { success: false; error: string };
const handleResult = (result: Result) => {
if (result.success) {
type Test = Expect<Equal<typeof result, { success: true; data: string }>>;
return result.data;
} else {
type Test = Expect<Equal<typeof result, { success: false; error: string }>>;
return result.error;
}
};
});
# Runtime tests
pnpm test # Run all runtime tests
pnpm test path/to/test # Run specific test file
pnpm test WIP # Run only WIP tests
pnpm test --exclude WIP # Run all except WIP (regression check)
pnpm test:watch # Run in watch mode
pnpm test:ui # Run with UI
# Type tests
pnpm test:types # Run all type tests
pnpm test:types GLOB # Run type tests matching pattern
pnpm test:types WIP # Run only WIP type tests
# Common patterns during development
pnpm test utils # Test all utils
pnpm test:types utils # Type test all utils
Before considering tests complete, verify:
Before closing out a phase:
tests/unit/WIP/tests/unit/WIP/ (DO NOT migrate automatically)After user review and approval:
tests/unit/WIP/ directory removedEffective testing requires understanding what to test, how to test it, and when to use different testing approaches:
Follow TDD principles: write tests first, implement to pass them, then refactor with confidence. Keep tests fast, focused, and independent.
For phase-based development, use the five-step workflow: SNAPSHOT → CREATE LOG → WRITE TESTS → IMPLEMENTATION → CLOSE OUT. This ensures comprehensive test coverage, prevents regressions, and maintains clear documentation of your progress.
When tests fail, understand why before fixing. Prioritize fixing implementation over changing tests, unless the test itself was wrong.