mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +08:00
docs: salvage F# agent and language guidance
This commit is contained in:
parent
a8836d7bbd
commit
fd9453f6ee
@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"description": "The most comprehensive Claude Code plugin — 55 agents, 208 skills, 72 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
"description": "The most comprehensive Claude Code plugin — 56 agents, 209 skills, 72 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
||||||
"version": "2.0.0-rc.1",
|
"version": "2.0.0-rc.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"version": "2.0.0-rc.1",
|
"version": "2.0.0-rc.1",
|
||||||
"description": "Battle-tested Claude Code plugin for engineering teams — 55 agents, 208 skills, 72 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
"description": "Battle-tested Claude Code plugin for engineering teams — 56 agents, 209 skills, 72 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
"url": "https://x.com/affaanmustafa"
|
"url": "https://x.com/affaanmustafa"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — Agent Instructions
|
# Everything Claude Code (ECC) — Agent Instructions
|
||||||
|
|
||||||
This is a **production-ready AI coding plugin** providing 55 specialized agents, 208 skills, 72 commands, and automated hook workflows for software development.
|
This is a **production-ready AI coding plugin** providing 56 specialized agents, 209 skills, 72 commands, and automated hook workflows for software development.
|
||||||
|
|
||||||
**Version:** 2.0.0-rc.1
|
**Version:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ This is a **production-ready AI coding plugin** providing 55 specialized agents,
|
|||||||
| doc-updater | Documentation and codemaps | Updating docs |
|
| doc-updater | Documentation and codemaps | Updating docs |
|
||||||
| cpp-reviewer | C/C++ code review | C and C++ projects |
|
| cpp-reviewer | C/C++ code review | C and C++ projects |
|
||||||
| cpp-build-resolver | C/C++ build errors | C and C++ build failures |
|
| cpp-build-resolver | C/C++ build errors | C and C++ build failures |
|
||||||
|
| fsharp-reviewer | F# functional code review | F# projects |
|
||||||
| docs-lookup | Documentation lookup via Context7 | API/docs questions |
|
| docs-lookup | Documentation lookup via Context7 | API/docs questions |
|
||||||
| go-reviewer | Go code review | Go projects |
|
| go-reviewer | Go code review | Go projects |
|
||||||
| go-build-resolver | Go build errors | Go build failures |
|
| go-build-resolver | Go build errors | Go build failures |
|
||||||
@ -146,8 +147,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 55 specialized subagents
|
agents/ — 56 specialized subagents
|
||||||
skills/ — 208 workflow skills and domain knowledge
|
skills/ — 209 workflow skills and domain knowledge
|
||||||
commands/ — 72 slash commands
|
commands/ — 72 slash commands
|
||||||
hooks/ — Trigger-based automations
|
hooks/ — Trigger-based automations
|
||||||
rules/ — Always-follow guidelines (common + per-language)
|
rules/ — Always-follow guidelines (common + per-language)
|
||||||
|
|||||||
14
README.md
14
README.md
@ -358,7 +358,7 @@ If you stacked methods, clean up in this order:
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**That's it!** You now have access to 55 agents, 208 skills, and 72 legacy command shims.
|
**That's it!** You now have access to 56 agents, 209 skills, and 72 legacy command shims.
|
||||||
|
|
||||||
### Dashboard GUI
|
### Dashboard GUI
|
||||||
|
|
||||||
@ -456,7 +456,7 @@ everything-claude-code/
|
|||||||
| |-- plugin.json # Plugin metadata and component paths
|
| |-- plugin.json # Plugin metadata and component paths
|
||||||
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
||||||
|
|
|
|
||||||
|-- agents/ # 55 specialized subagents for delegation
|
|-- agents/ # 56 specialized subagents for delegation
|
||||||
| |-- planner.md # Feature implementation planning
|
| |-- planner.md # Feature implementation planning
|
||||||
| |-- architect.md # System design decisions
|
| |-- architect.md # System design decisions
|
||||||
| |-- tdd-guide.md # Test-driven development
|
| |-- tdd-guide.md # Test-driven development
|
||||||
@ -472,6 +472,7 @@ everything-claude-code/
|
|||||||
| |-- harness-optimizer.md # Harness config tuning
|
| |-- harness-optimizer.md # Harness config tuning
|
||||||
| |-- cpp-reviewer.md # C++ code review
|
| |-- cpp-reviewer.md # C++ code review
|
||||||
| |-- cpp-build-resolver.md # C++ build error resolution
|
| |-- cpp-build-resolver.md # C++ build error resolution
|
||||||
|
| |-- fsharp-reviewer.md # F# functional code review
|
||||||
| |-- go-reviewer.md # Go code review
|
| |-- go-reviewer.md # Go code review
|
||||||
| |-- go-build-resolver.md # Go build error resolution
|
| |-- go-build-resolver.md # Go build error resolution
|
||||||
| |-- python-reviewer.md # Python code review
|
| |-- python-reviewer.md # Python code review
|
||||||
@ -986,6 +987,7 @@ Not sure where to start? Use this quick reference. Skills are the canonical work
|
|||||||
| Update documentation | `/update-docs` | doc-updater |
|
| Update documentation | `/update-docs` | doc-updater |
|
||||||
| Review Go code | `/go-review` | go-reviewer |
|
| Review Go code | `/go-review` | go-reviewer |
|
||||||
| Review Python code | `/python-review` | python-reviewer |
|
| Review Python code | `/python-review` | python-reviewer |
|
||||||
|
| Review F# code | *(invoke `fsharp-reviewer` directly)* | fsharp-reviewer |
|
||||||
| Review TypeScript/JavaScript code | *(invoke `typescript-reviewer` directly)* | typescript-reviewer |
|
| Review TypeScript/JavaScript code | *(invoke `typescript-reviewer` directly)* | typescript-reviewer |
|
||||||
| Develop HarmonyOS apps | *(invoke `harmonyos-app-resolver` directly)* | harmonyos-app-resolver |
|
| Develop HarmonyOS apps | *(invoke `harmonyos-app-resolver` directly)* | harmonyos-app-resolver |
|
||||||
| Audit database queries | *(auto-delegated)* | database-reviewer |
|
| Audit database queries | *(auto-delegated)* | database-reviewer |
|
||||||
@ -1354,9 +1356,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
|||||||
|
|
||||||
| Feature | Claude Code | OpenCode | Status |
|
| Feature | Claude Code | OpenCode | Status |
|
||||||
|---------|-------------|----------|--------|
|
|---------|-------------|----------|--------|
|
||||||
| Agents | PASS: 55 agents | PASS: 12 agents | **Claude Code leads** |
|
| Agents | PASS: 56 agents | PASS: 12 agents | **Claude Code leads** |
|
||||||
| Commands | PASS: 72 commands | PASS: 35 commands | **Claude Code leads** |
|
| Commands | PASS: 72 commands | PASS: 35 commands | **Claude Code leads** |
|
||||||
| Skills | PASS: 208 skills | PASS: 37 skills | **Claude Code leads** |
|
| Skills | PASS: 209 skills | PASS: 37 skills | **Claude Code leads** |
|
||||||
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
||||||
@ -1459,9 +1461,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
|||||||
|
|
||||||
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||||
|---------|------------|------------|-----------|----------|
|
|---------|------------|------------|-----------|----------|
|
||||||
| **Agents** | 55 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
| **Agents** | 56 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||||
| **Commands** | 72 | Shared | Instruction-based | 35 |
|
| **Commands** | 72 | Shared | Instruction-based | 35 |
|
||||||
| **Skills** | 208 | Shared | 10 (native format) | 37 |
|
| **Skills** | 209 | Shared | 10 (native format) | 37 |
|
||||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
||||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||||
|
|||||||
@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 你现在可以使用 55 个代理、208 个技能和 72 个命令。
|
**完成!** 你现在可以使用 56 个代理、209 个技能和 72 个命令。
|
||||||
|
|
||||||
### multi-* 命令需要额外配置
|
### multi-* 命令需要额外配置
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,7 @@ skills:
|
|||||||
- foundation-models-on-device
|
- foundation-models-on-device
|
||||||
- frontend-patterns
|
- frontend-patterns
|
||||||
- frontend-slides
|
- frontend-slides
|
||||||
|
- fsharp-testing
|
||||||
- git-workflow
|
- git-workflow
|
||||||
- golang-patterns
|
- golang-patterns
|
||||||
- golang-testing
|
- golang-testing
|
||||||
|
|||||||
100
agents/fsharp-reviewer.md
Normal file
100
agents/fsharp-reviewer.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
name: fsharp-reviewer
|
||||||
|
description: Expert F# code reviewer specializing in functional idioms, type safety, pattern matching, computation expressions, and performance. Use for all F# code changes. MUST BE USED for F# projects.
|
||||||
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior F# code reviewer ensuring high standards of idiomatic functional F# code and best practices.
|
||||||
|
|
||||||
|
When invoked:
|
||||||
|
1. Run `git diff -- '*.fs' '*.fsx'` to see recent F# file changes
|
||||||
|
2. Run `dotnet build` and `fantomas --check .` if available
|
||||||
|
3. Focus on modified `.fs` and `.fsx` files
|
||||||
|
4. Begin review immediately
|
||||||
|
|
||||||
|
## Review Priorities
|
||||||
|
|
||||||
|
### CRITICAL - Security
|
||||||
|
- **SQL Injection**: String concatenation/interpolation in queries - use parameterized queries
|
||||||
|
- **Command Injection**: Unvalidated input in `Process.Start` - validate and sanitize
|
||||||
|
- **Path Traversal**: User-controlled file paths - use `Path.GetFullPath` + prefix check
|
||||||
|
- **Insecure Deserialization**: `BinaryFormatter`, unsafe JSON settings
|
||||||
|
- **Hardcoded secrets**: API keys, connection strings in source - use configuration/secret manager
|
||||||
|
- **CSRF/XSS**: Missing anti-forgery tokens, unencoded output in views
|
||||||
|
|
||||||
|
### CRITICAL - Error Handling
|
||||||
|
- **Swallowed exceptions**: `with _ -> ()` or `with _ -> None` - handle or reraise
|
||||||
|
- **Missing disposal**: Manual disposal of `IDisposable` - use `use` or `use!` bindings
|
||||||
|
- **Blocking async**: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` - use `let!` or `do!`
|
||||||
|
- **Bare `failwith` in library code**: Prefer `Result` or `Option` for expected failures
|
||||||
|
|
||||||
|
### HIGH - Functional Idioms
|
||||||
|
- **Mutable state in domain logic**: `mutable`, `ref` cells where immutable alternatives exist
|
||||||
|
- **Incomplete pattern matches**: Missing cases or catch-all `_` that hides new union cases
|
||||||
|
- **Imperative loops**: `for`/`while` where `List.map`, `Seq.filter`, `Array.fold` are clearer
|
||||||
|
- **Null usage**: Using `null` instead of `Option<'T>` for missing values
|
||||||
|
- **Class-heavy design**: OOP-style classes where modules + functions + records suffice
|
||||||
|
|
||||||
|
### HIGH - Type Safety
|
||||||
|
- **Primitive obsession**: Raw strings/ints for domain concepts - use single-case DUs
|
||||||
|
- **Unvalidated input**: Missing validation at system boundaries - use smart constructors
|
||||||
|
- **Downcasting**: `:?>` without type test - use pattern matching with `:? T as t`
|
||||||
|
- **`obj` usage**: Avoid `obj` boxing; prefer generics or explicit union types
|
||||||
|
|
||||||
|
### HIGH - Code Quality
|
||||||
|
- **Large functions**: Over 40 lines - extract helper functions
|
||||||
|
- **Deep nesting**: More than 3 levels - use early returns, `Result.bind`, or computation expressions
|
||||||
|
- **Missing `[<RequireQualifiedAccess>]`**: On modules/unions that could cause name collisions
|
||||||
|
- **Unused `open` declarations**: Remove unused module imports
|
||||||
|
|
||||||
|
### MEDIUM - Performance
|
||||||
|
- **Seq in hot paths**: Lazy sequences recomputed repeatedly - materialize with `Seq.toList` or `Seq.toArray`
|
||||||
|
- **String concatenation in loops**: Use `StringBuilder` or `String.concat`
|
||||||
|
- **Excessive boxing**: Value types passed through `obj` - use generic functions
|
||||||
|
- **N+1 queries**: Lazy loading in loops when using EF Core - use eager loading
|
||||||
|
|
||||||
|
### MEDIUM - Best Practices
|
||||||
|
- **Naming conventions**: camelCase for functions/values, PascalCase for types/modules/DU cases
|
||||||
|
- **Pipe operator readability**: Overly long chains - break into named intermediate bindings
|
||||||
|
- **Computation expression misuse**: Nested `task { task { } }` - flatten with `let!`
|
||||||
|
- **Module organization**: Related functions scattered across files - group cohesively
|
||||||
|
|
||||||
|
## Diagnostic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build # Compilation check
|
||||||
|
fantomas --check . # Format check
|
||||||
|
dotnet test --no-build # Run tests
|
||||||
|
dotnet test --collect:"XPlat Code Coverage" # Coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Output Format
|
||||||
|
|
||||||
|
```text
|
||||||
|
[SEVERITY] Issue title
|
||||||
|
File: path/to/File.fs:42
|
||||||
|
Issue: Description
|
||||||
|
Fix: What to change
|
||||||
|
```
|
||||||
|
|
||||||
|
## Approval Criteria
|
||||||
|
|
||||||
|
- **Approve**: No CRITICAL or HIGH issues
|
||||||
|
- **Warning**: MEDIUM issues only (can merge with caution)
|
||||||
|
- **Block**: CRITICAL or HIGH issues found
|
||||||
|
|
||||||
|
## Framework Checks
|
||||||
|
|
||||||
|
- **ASP.NET Core**: Giraffe or Saturn handlers, model validation, auth policies, middleware order
|
||||||
|
- **EF Core**: Migration safety, eager loading, `AsNoTracking` for reads
|
||||||
|
- **Fable**: Elmish architecture, message handling completeness, view function purity
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
For detailed .NET patterns, see skill: `dotnet-patterns`.
|
||||||
|
For testing guidelines, see skill: `fsharp-testing`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Review with the mindset: "Is this idiomatic F# that leverages the type system and functional patterns effectively?"
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — 智能体指令
|
# Everything Claude Code (ECC) — 智能体指令
|
||||||
|
|
||||||
这是一个**生产就绪的 AI 编码插件**,提供 55 个专业代理、208 项技能、72 条命令以及自动化钩子工作流,用于软件开发。
|
这是一个**生产就绪的 AI 编码插件**,提供 56 个专业代理、209 项技能、72 条命令以及自动化钩子工作流,用于软件开发。
|
||||||
|
|
||||||
**版本:** 2.0.0-rc.1
|
**版本:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -146,8 +146,8 @@
|
|||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 55 个专业子代理
|
agents/ — 56 个专业子代理
|
||||||
skills/ — 208 个工作流技能和领域知识
|
skills/ — 209 个工作流技能和领域知识
|
||||||
commands/ — 72 个斜杠命令
|
commands/ — 72 个斜杠命令
|
||||||
hooks/ — 基于触发的自动化
|
hooks/ — 基于触发的自动化
|
||||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||||
|
|||||||
@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**搞定!** 你现在可以使用 55 个智能体、208 项技能和 72 个命令了。
|
**搞定!** 你现在可以使用 56 个智能体、209 项技能和 72 个命令了。
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@ -1132,9 +1132,9 @@ opencode
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | OpenCode | 状态 |
|
| 功能特性 | Claude Code | OpenCode | 状态 |
|
||||||
|---------|-------------|----------|--------|
|
|---------|-------------|----------|--------|
|
||||||
| 智能体 | PASS: 55 个 | PASS: 12 个 | **Claude Code 领先** |
|
| 智能体 | PASS: 56 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||||
| 命令 | PASS: 72 个 | PASS: 35 个 | **Claude Code 领先** |
|
| 命令 | PASS: 72 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||||
| 技能 | PASS: 208 项 | PASS: 37 项 | **Claude Code 领先** |
|
| 技能 | PASS: 209 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||||
@ -1240,9 +1240,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||||
|---------|------------|------------|-----------|----------|
|
|---------|------------|------------|-----------|----------|
|
||||||
| **智能体** | 55 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
| **智能体** | 56 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||||
| **命令** | 72 | 共享 | 基于指令 | 35 |
|
| **命令** | 72 | 共享 | 基于指令 | 35 |
|
||||||
| **技能** | 208 | 共享 | 10 (原生格式) | 37 |
|
| **技能** | 209 | 共享 | 10 (原生格式) | 37 |
|
||||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||||
|
|||||||
@ -250,6 +250,14 @@
|
|||||||
"framework-language"
|
"framework-language"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "lang:fsharp",
|
||||||
|
"family": "language",
|
||||||
|
"description": "F# functional patterns and testing guidance. Currently resolves through the shared framework-language module.",
|
||||||
|
"modules": [
|
||||||
|
"framework-language"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "framework:laravel",
|
"id": "framework:laravel",
|
||||||
"family": "framework",
|
"family": "framework",
|
||||||
@ -363,6 +371,14 @@
|
|||||||
"agents-core"
|
"agents-core"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "agent:fsharp-reviewer",
|
||||||
|
"family": "agent",
|
||||||
|
"description": "F# code review agent for functional idioms, type safety, and .NET testing.",
|
||||||
|
"modules": [
|
||||||
|
"agents-core"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "agent:refactor-cleaner",
|
"id": "agent:refactor-cleaner",
|
||||||
"family": "agent",
|
"family": "agent",
|
||||||
|
|||||||
@ -128,6 +128,7 @@
|
|||||||
"skills/coding-standards",
|
"skills/coding-standards",
|
||||||
"skills/compose-multiplatform-patterns",
|
"skills/compose-multiplatform-patterns",
|
||||||
"skills/csharp-testing",
|
"skills/csharp-testing",
|
||||||
|
"skills/fsharp-testing",
|
||||||
"skills/cpp-coding-standards",
|
"skills/cpp-coding-standards",
|
||||||
"skills/cpp-testing",
|
"skills/cpp-testing",
|
||||||
"skills/dart-flutter-patterns",
|
"skills/dart-flutter-patterns",
|
||||||
|
|||||||
@ -156,6 +156,7 @@
|
|||||||
"skills/foundation-models-on-device/",
|
"skills/foundation-models-on-device/",
|
||||||
"skills/frontend-patterns/",
|
"skills/frontend-patterns/",
|
||||||
"skills/frontend-slides/",
|
"skills/frontend-slides/",
|
||||||
|
"skills/fsharp-testing/",
|
||||||
"skills/github-ops/",
|
"skills/github-ops/",
|
||||||
"skills/golang-patterns/",
|
"skills/golang-patterns/",
|
||||||
"skills/golang-testing/",
|
"skills/golang-testing/",
|
||||||
|
|||||||
112
rules/fsharp/coding-style.md
Normal file
112
rules/fsharp/coding-style.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.fs"
|
||||||
|
- "**/*.fsx"
|
||||||
|
---
|
||||||
|
# F# Coding Style
|
||||||
|
|
||||||
|
> This file extends [common/coding-style.md](../common/coding-style.md) with F#-specific content.
|
||||||
|
|
||||||
|
## Standards
|
||||||
|
|
||||||
|
- Follow standard F# conventions and leverage the type system for correctness
|
||||||
|
- Prefer immutability by default; use `mutable` only when justified by performance
|
||||||
|
- Keep modules focused and cohesive
|
||||||
|
|
||||||
|
## Types and Models
|
||||||
|
|
||||||
|
- Prefer discriminated unions for domain modeling over class hierarchies
|
||||||
|
- Use records for data with named fields
|
||||||
|
- Use single-case unions for type-safe wrappers around primitives
|
||||||
|
- Avoid classes unless interop or mutable state requires them
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type EmailAddress = EmailAddress of string
|
||||||
|
|
||||||
|
type OrderStatus =
|
||||||
|
| Pending
|
||||||
|
| Confirmed of confirmedAt: DateTimeOffset
|
||||||
|
| Shipped of trackingNumber: string
|
||||||
|
| Cancelled of reason: string
|
||||||
|
|
||||||
|
type Order =
|
||||||
|
{ Id: Guid
|
||||||
|
CustomerId: string
|
||||||
|
Status: OrderStatus
|
||||||
|
Items: OrderItem list }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Immutability
|
||||||
|
|
||||||
|
- Records are immutable by default; use `with` expressions for updates
|
||||||
|
- Prefer `list`, `map`, `set` over mutable collections
|
||||||
|
- Avoid `ref` cells and mutable fields in domain logic
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let rename (profile: UserProfile) newName =
|
||||||
|
{ profile with Name = newName }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function Style
|
||||||
|
|
||||||
|
- Prefer small, composable functions over large methods
|
||||||
|
- Use the pipe operator `|>` to build readable data pipelines
|
||||||
|
- Prefer pattern matching over if/else chains
|
||||||
|
- Use `Option` instead of null; use `Result` for operations that can fail
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let processOrder order =
|
||||||
|
order
|
||||||
|
|> validateItems
|
||||||
|
|> Result.bind calculateTotal
|
||||||
|
|> Result.map applyDiscount
|
||||||
|
|> Result.mapError OrderError
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async and Error Handling
|
||||||
|
|
||||||
|
- Use `task { }` for interop with .NET async APIs
|
||||||
|
- Use `async { }` for F#-native async workflows
|
||||||
|
- Propagate `CancellationToken` through public async APIs
|
||||||
|
- Prefer `Result` and railway-oriented programming over exceptions for expected failures
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let loadOrderAsync (orderId: Guid) (ct: CancellationToken) =
|
||||||
|
task {
|
||||||
|
let! order = repository.FindAsync(orderId, ct)
|
||||||
|
return
|
||||||
|
order
|
||||||
|
|> Option.defaultWith (fun () ->
|
||||||
|
failwith $"Order {orderId} was not found.")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
- Use `fantomas` for automatic formatting
|
||||||
|
- Prefer significant whitespace; avoid unnecessary parentheses
|
||||||
|
- Remove unused `open` declarations
|
||||||
|
|
||||||
|
### Open Declaration Order
|
||||||
|
|
||||||
|
Group `open` statements into four sections separated by a blank line, each section sorted lexically within itself:
|
||||||
|
|
||||||
|
1. `System.*`
|
||||||
|
2. `Microsoft.*`
|
||||||
|
3. Third-party namespaces
|
||||||
|
4. First-party / project namespaces
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
open System
|
||||||
|
open System.Collections.Generic
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
open FsCheck.Xunit
|
||||||
|
open Swensen.Unquote
|
||||||
|
|
||||||
|
open MyApp.Domain
|
||||||
|
open MyApp.Infrastructure
|
||||||
|
```
|
||||||
26
rules/fsharp/hooks.md
Normal file
26
rules/fsharp/hooks.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.fs"
|
||||||
|
- "**/*.fsx"
|
||||||
|
- "**/*.fsproj"
|
||||||
|
- "**/*.sln"
|
||||||
|
- "**/*.slnx"
|
||||||
|
- "**/Directory.Build.props"
|
||||||
|
- "**/Directory.Build.targets"
|
||||||
|
---
|
||||||
|
# F# Hooks
|
||||||
|
|
||||||
|
> This file extends [common/hooks.md](../common/hooks.md) with F#-specific content.
|
||||||
|
|
||||||
|
## PostToolUse Hooks
|
||||||
|
|
||||||
|
Configure in `~/.claude/settings.json`:
|
||||||
|
|
||||||
|
- **fantomas**: Auto-format edited F# files
|
||||||
|
- **dotnet build**: Verify the solution or project still compiles after edits
|
||||||
|
- **dotnet test --no-build**: Re-run the nearest relevant test project after behavior changes
|
||||||
|
|
||||||
|
## Stop Hooks
|
||||||
|
|
||||||
|
- Run a final `dotnet build` before ending a session with broad F# changes
|
||||||
|
- Warn on modified `appsettings*.json` files so secrets do not get committed
|
||||||
111
rules/fsharp/patterns.md
Normal file
111
rules/fsharp/patterns.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.fs"
|
||||||
|
- "**/*.fsx"
|
||||||
|
---
|
||||||
|
# F# Patterns
|
||||||
|
|
||||||
|
> This file extends [common/patterns.md](../common/patterns.md) with F#-specific content.
|
||||||
|
|
||||||
|
## Result Type for Error Handling
|
||||||
|
|
||||||
|
Use `Result<'T, 'TError>` with railway-oriented programming instead of exceptions for expected failures.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type OrderError =
|
||||||
|
| InvalidCustomer of string
|
||||||
|
| EmptyItems
|
||||||
|
| ItemOutOfStock of sku: string
|
||||||
|
|
||||||
|
let validateOrder (request: CreateOrderRequest) : Result<ValidatedOrder, OrderError> =
|
||||||
|
if String.IsNullOrWhiteSpace request.CustomerId then
|
||||||
|
Error(InvalidCustomer "CustomerId is required")
|
||||||
|
elif request.Items |> List.isEmpty then
|
||||||
|
Error EmptyItems
|
||||||
|
else
|
||||||
|
Ok { CustomerId = request.CustomerId; Items = request.Items }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option for Missing Values
|
||||||
|
|
||||||
|
Prefer `Option<'T>` over null. Use `Option.map`, `Option.bind`, and `Option.defaultValue` to transform.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let findUser (id: Guid) : User option =
|
||||||
|
users |> Map.tryFind id
|
||||||
|
|
||||||
|
let getUserEmail userId =
|
||||||
|
findUser userId
|
||||||
|
|> Option.map (fun u -> u.Email)
|
||||||
|
|> Option.defaultValue "unknown@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discriminated Unions for Domain Modeling
|
||||||
|
|
||||||
|
Model business states explicitly. The compiler enforces exhaustive handling.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type PaymentState =
|
||||||
|
| AwaitingPayment of amount: decimal
|
||||||
|
| Paid of paidAt: DateTimeOffset * transactionId: string
|
||||||
|
| Refunded of refundedAt: DateTimeOffset * reason: string
|
||||||
|
| Failed of error: string
|
||||||
|
|
||||||
|
let describePayment = function
|
||||||
|
| AwaitingPayment amount -> $"Awaiting payment of {amount:C}"
|
||||||
|
| Paid (at, txn) -> $"Paid at {at} (txn: {txn})"
|
||||||
|
| Refunded (at, reason) -> $"Refunded at {at}: {reason}"
|
||||||
|
| Failed error -> $"Payment failed: {error}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Computation Expressions
|
||||||
|
|
||||||
|
Use computation expressions to simplify sequential operations that may fail.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let placeOrder request =
|
||||||
|
result {
|
||||||
|
let! validated = validateOrder request
|
||||||
|
let! inventory = checkInventory validated.Items
|
||||||
|
let! order = createOrder validated inventory
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Organization
|
||||||
|
|
||||||
|
- Group related functions in modules rather than classes
|
||||||
|
- Use `[<RequireQualifiedAccess>]` to prevent name collisions
|
||||||
|
- Keep modules small and focused on a single responsibility
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Order =
|
||||||
|
let create customerId items = { Id = Guid.NewGuid(); CustomerId = customerId; Items = items; Status = Pending }
|
||||||
|
let confirm order = { order with Status = Confirmed(DateTimeOffset.UtcNow) }
|
||||||
|
let cancel reason order = { order with Status = Cancelled reason }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
- Define dependencies as function parameters or record-of-functions
|
||||||
|
- Use interfaces sparingly, primarily at the boundary with .NET libraries
|
||||||
|
- Prefer partial application for injecting dependencies into pipelines
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type OrderDeps =
|
||||||
|
{ FindOrder: Guid -> Task<Order option>
|
||||||
|
SaveOrder: Order -> Task<unit>
|
||||||
|
SendNotification: Order -> Task<unit> }
|
||||||
|
|
||||||
|
let processOrder (deps: OrderDeps) orderId =
|
||||||
|
task {
|
||||||
|
match! deps.FindOrder orderId with
|
||||||
|
| None -> return Error "Order not found"
|
||||||
|
| Some order ->
|
||||||
|
let confirmed = Order.confirm order
|
||||||
|
do! deps.SaveOrder confirmed
|
||||||
|
do! deps.SendNotification confirmed
|
||||||
|
return Ok confirmed
|
||||||
|
}
|
||||||
|
```
|
||||||
76
rules/fsharp/security.md
Normal file
76
rules/fsharp/security.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.fs"
|
||||||
|
- "**/*.fsx"
|
||||||
|
- "**/*.fsproj"
|
||||||
|
- "**/appsettings*.json"
|
||||||
|
---
|
||||||
|
# F# Security
|
||||||
|
|
||||||
|
> This file extends [common/security.md](../common/security.md) with F#-specific content.
|
||||||
|
|
||||||
|
## Secret Management
|
||||||
|
|
||||||
|
- Never hardcode API keys, tokens, or connection strings in source code
|
||||||
|
- Use environment variables, user secrets for local development, and a secret manager in production
|
||||||
|
- Keep `appsettings.*.json` free of real credentials
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
// BAD
|
||||||
|
let apiKey = "sk-live-123"
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
let apiKey =
|
||||||
|
configuration["OpenAI:ApiKey"]
|
||||||
|
|> Option.ofObj
|
||||||
|
|> Option.defaultWith (fun () -> failwith "OpenAI:ApiKey is not configured.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL Injection Prevention
|
||||||
|
|
||||||
|
- Always use parameterized queries with ADO.NET, Dapper, or EF Core
|
||||||
|
- Never concatenate user input into SQL strings
|
||||||
|
- Validate sort fields and filter operators before using dynamic query composition
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let findByCustomer (connection: IDbConnection) customerId =
|
||||||
|
task {
|
||||||
|
let sql = "SELECT * FROM Orders WHERE CustomerId = @customerId"
|
||||||
|
return! connection.QueryAsync<Order>(sql, {| customerId = customerId |})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
- Validate inputs at the application boundary using types
|
||||||
|
- Use single-case discriminated unions for validated values
|
||||||
|
- Reject invalid input before it enters domain logic
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type ValidatedEmail = private ValidatedEmail of string
|
||||||
|
|
||||||
|
module ValidatedEmail =
|
||||||
|
let create (input: string) =
|
||||||
|
if System.Text.RegularExpressions.Regex.IsMatch(input, @"^[^@]+@[^@]+\.[^@]+$") then
|
||||||
|
Ok(ValidatedEmail input)
|
||||||
|
else
|
||||||
|
Error "Invalid email address"
|
||||||
|
|
||||||
|
let value (ValidatedEmail v) = v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication and Authorization
|
||||||
|
|
||||||
|
- Prefer framework auth handlers instead of custom token parsing
|
||||||
|
- Enforce authorization policies at endpoint or handler boundaries
|
||||||
|
- Never log raw tokens, passwords, or PII
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Return safe client-facing messages
|
||||||
|
- Log detailed exceptions with structured context server-side
|
||||||
|
- Do not expose stack traces, SQL text, or filesystem paths in API responses
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
See skill: `security-review` for broader application security review checklists.
|
||||||
62
rules/fsharp/testing.md
Normal file
62
rules/fsharp/testing.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.fs"
|
||||||
|
- "**/*.fsx"
|
||||||
|
- "**/*.fsproj"
|
||||||
|
---
|
||||||
|
# F# Testing
|
||||||
|
|
||||||
|
> This file extends [common/testing.md](../common/testing.md) with F#-specific content.
|
||||||
|
|
||||||
|
## Test Framework
|
||||||
|
|
||||||
|
- Prefer **xUnit** with **FsUnit.xUnit** for F#-friendly assertions
|
||||||
|
- Use **Unquote** for quotation-based assertions with clear failure messages
|
||||||
|
- Use **FsCheck.xUnit** for property-based testing
|
||||||
|
- Use **NSubstitute** or function stubs for mocking dependencies
|
||||||
|
- Use **Testcontainers** when integration tests need real infrastructure
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
- Mirror `src/` structure under `tests/`
|
||||||
|
- Separate unit, integration, and end-to-end coverage clearly
|
||||||
|
- Name tests by behavior, not implementation details
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
open Xunit
|
||||||
|
open Swensen.Unquote
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder returns success when request is valid`` () =
|
||||||
|
let request = { CustomerId = "cust-123"; Items = [ validItem ] }
|
||||||
|
let result = OrderService.placeOrder request
|
||||||
|
test <@ Result.isOk result @>
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder returns error when items are empty`` () =
|
||||||
|
let request = { CustomerId = "cust-123"; Items = [] }
|
||||||
|
let result = OrderService.placeOrder request
|
||||||
|
test <@ Result.isError result @>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Property-Based Testing with FsCheck
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
open FsCheck.Xunit
|
||||||
|
|
||||||
|
[<Property>]
|
||||||
|
let ``order total is never negative`` (items: OrderItem list) =
|
||||||
|
let total = Order.calculateTotal items
|
||||||
|
total >= 0m
|
||||||
|
```
|
||||||
|
|
||||||
|
## ASP.NET Core Integration Tests
|
||||||
|
|
||||||
|
- Use `WebApplicationFactory<TEntryPoint>` for API integration coverage
|
||||||
|
- Test auth, validation, and serialization through HTTP, not by bypassing middleware
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
- Target 80%+ line coverage
|
||||||
|
- Focus coverage on domain logic, validation, auth, and failure paths
|
||||||
|
- Run `dotnet test` in CI with coverage collection enabled where available
|
||||||
@ -40,6 +40,7 @@ const LEGACY_LANGUAGE_ALIAS_TO_CANONICAL = Object.freeze({
|
|||||||
c: 'c',
|
c: 'c',
|
||||||
cpp: 'cpp',
|
cpp: 'cpp',
|
||||||
csharp: 'csharp',
|
csharp: 'csharp',
|
||||||
|
fsharp: 'fsharp',
|
||||||
go: 'go',
|
go: 'go',
|
||||||
golang: 'go',
|
golang: 'go',
|
||||||
arkts: 'arkts',
|
arkts: 'arkts',
|
||||||
@ -58,6 +59,7 @@ const LEGACY_LANGUAGE_EXTRA_MODULE_IDS = Object.freeze({
|
|||||||
c: ['framework-language'],
|
c: ['framework-language'],
|
||||||
cpp: ['framework-language'],
|
cpp: ['framework-language'],
|
||||||
csharp: ['framework-language'],
|
csharp: ['framework-language'],
|
||||||
|
fsharp: ['framework-language'],
|
||||||
go: ['framework-language'],
|
go: ['framework-language'],
|
||||||
arkts: ['framework-language'],
|
arkts: ['framework-language'],
|
||||||
java: ['framework-language'],
|
java: ['framework-language'],
|
||||||
|
|||||||
@ -60,6 +60,11 @@ const LANGUAGE_RULES = [
|
|||||||
markers: [],
|
markers: [],
|
||||||
extensions: ['.cs', '.csproj', '.sln']
|
extensions: ['.cs', '.csproj', '.sln']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'fsharp',
|
||||||
|
markers: [],
|
||||||
|
extensions: ['.fs', '.fsx', '.fsproj']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'swift',
|
type: 'swift',
|
||||||
markers: ['Package.swift'],
|
markers: ['Package.swift'],
|
||||||
|
|||||||
280
skills/fsharp-testing/SKILL.md
Normal file
280
skills/fsharp-testing/SKILL.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
---
|
||||||
|
name: fsharp-testing
|
||||||
|
description: F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# F# Testing Patterns
|
||||||
|
|
||||||
|
Comprehensive testing patterns for F# applications using xUnit, FsUnit, Unquote, FsCheck, and modern .NET testing practices.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Writing new tests for F# code
|
||||||
|
- Reviewing test quality and coverage
|
||||||
|
- Setting up test infrastructure for F# projects
|
||||||
|
- Debugging flaky or slow tests
|
||||||
|
|
||||||
|
## Test Framework Stack
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| **xUnit** | Test framework (standard .NET ecosystem choice) |
|
||||||
|
| **FsUnit.xUnit** | F#-friendly assertion syntax for xUnit |
|
||||||
|
| **Unquote** | Assertion library using F# quotations for clear failure messages |
|
||||||
|
| **FsCheck.xUnit** | Property-based testing integrated with xUnit |
|
||||||
|
| **NSubstitute** | Mocking .NET dependencies |
|
||||||
|
| **Testcontainers** | Real infrastructure in integration tests |
|
||||||
|
| **WebApplicationFactory** | ASP.NET Core integration tests |
|
||||||
|
|
||||||
|
## Unit Tests with xUnit + FsUnit
|
||||||
|
|
||||||
|
### Basic Test Structure
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
module OrderServiceTests
|
||||||
|
|
||||||
|
open Xunit
|
||||||
|
open FsUnit.Xunit
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``create sets status to Pending`` () =
|
||||||
|
let order = Order.create "cust-1" [ validItem ]
|
||||||
|
order.Status |> should equal Pending
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``confirm changes status to Confirmed`` () =
|
||||||
|
let order = Order.create "cust-1" [ validItem ]
|
||||||
|
let confirmed = Order.confirm order
|
||||||
|
confirmed.Status |> should be (ofCase <@ Confirmed @>)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions with Unquote
|
||||||
|
|
||||||
|
Unquote uses F# quotations so failure messages show the full expression that failed, not just "expected X got Y".
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
module OrderValidationTests
|
||||||
|
|
||||||
|
open Xunit
|
||||||
|
open Swensen.Unquote
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder returns success when request is valid`` () =
|
||||||
|
let request = { CustomerId = "cust-123"; Items = [ validItem ] }
|
||||||
|
let result = OrderService.placeOrder request
|
||||||
|
test <@ Result.isOk result @>
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``order total sums item prices`` () =
|
||||||
|
let items = [ { Sku = "A"; Quantity = 2; Price = 10m }
|
||||||
|
{ Sku = "B"; Quantity = 1; Price = 5m } ]
|
||||||
|
let total = Order.calculateTotal items
|
||||||
|
test <@ total = 25m @>
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``validated email rejects empty input`` () =
|
||||||
|
let result = ValidatedEmail.create ""
|
||||||
|
test <@ Result.isError result @>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Tests
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder returns success when request is valid`` () = task {
|
||||||
|
let deps = createTestDeps ()
|
||||||
|
let request = { CustomerId = "cust-123"; Items = [ validItem ] }
|
||||||
|
|
||||||
|
let! result = OrderService.placeOrder deps request
|
||||||
|
|
||||||
|
test <@ Result.isOk result @>
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder returns error when items are empty`` () = task {
|
||||||
|
let deps = createTestDeps ()
|
||||||
|
let request = { CustomerId = "cust-123"; Items = [] }
|
||||||
|
|
||||||
|
let! result = OrderService.placeOrder deps request
|
||||||
|
|
||||||
|
test <@ Result.isError result @>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Tests with Theory
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
[<Theory>]
|
||||||
|
[<InlineData("")>]
|
||||||
|
[<InlineData(" ")>]
|
||||||
|
let ``PlaceOrder rejects empty customer ID`` (customerId: string) =
|
||||||
|
let request = { CustomerId = customerId; Items = [ validItem ] }
|
||||||
|
let result = OrderService.placeOrder request
|
||||||
|
result |> should be (ofCase <@ Error @>)
|
||||||
|
|
||||||
|
[<Theory>]
|
||||||
|
[<InlineData("", false)>]
|
||||||
|
[<InlineData("a", false)>]
|
||||||
|
[<InlineData("user@example.com", true)>]
|
||||||
|
[<InlineData("user+tag@example.co.uk", true)>]
|
||||||
|
let ``IsValidEmail returns expected result`` (email: string, expected: bool) =
|
||||||
|
test <@ EmailValidator.isValid email = expected @>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Property-Based Testing with FsCheck
|
||||||
|
|
||||||
|
### Using FsCheck.xUnit
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
open FsCheck
|
||||||
|
open FsCheck.Xunit
|
||||||
|
|
||||||
|
[<Property>]
|
||||||
|
let ``order total is always non-negative`` (items: NonEmptyList<PositiveInt * decimal>) =
|
||||||
|
let orderItems =
|
||||||
|
items.Get
|
||||||
|
|> List.map (fun (qty, price) ->
|
||||||
|
{ Sku = "SKU"; Quantity = qty.Get; Price = abs price })
|
||||||
|
let total = Order.calculateTotal orderItems
|
||||||
|
total >= 0m
|
||||||
|
|
||||||
|
[<Property>]
|
||||||
|
let ``serialization roundtrips`` (order: Order) =
|
||||||
|
let json = JsonSerializer.Serialize order
|
||||||
|
let deserialized = JsonSerializer.Deserialize<Order> json
|
||||||
|
deserialized = order
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Generators
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type OrderGenerators =
|
||||||
|
static member ValidEmail () =
|
||||||
|
gen {
|
||||||
|
let! user = Gen.elements [ "alice"; "bob"; "carol" ]
|
||||||
|
let! domain = Gen.elements [ "example.com"; "test.org" ]
|
||||||
|
return $"{user}@{domain}"
|
||||||
|
}
|
||||||
|
|> Arb.fromGen
|
||||||
|
|
||||||
|
[<Property(Arbitrary = [| typeof<OrderGenerators> |])>]
|
||||||
|
let ``valid emails pass validation`` (email: string) =
|
||||||
|
EmailValidator.isValid email
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking Dependencies
|
||||||
|
|
||||||
|
### Function Stubs (Preferred)
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let createTestDeps () =
|
||||||
|
let mutable savedOrders = []
|
||||||
|
{ FindOrder = fun id -> task { return Map.tryFind id testData }
|
||||||
|
SaveOrder = fun order -> task { savedOrders <- order :: savedOrders }
|
||||||
|
SendNotification = fun _ -> Task.CompletedTask }
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``PlaceOrder saves the confirmed order`` () = task {
|
||||||
|
let mutable saved = []
|
||||||
|
let deps =
|
||||||
|
{ createTestDeps () with
|
||||||
|
SaveOrder = fun order -> task { saved <- order :: saved } }
|
||||||
|
|
||||||
|
let! _ = OrderService.placeOrder deps validRequest
|
||||||
|
|
||||||
|
test <@ saved.Length = 1 @>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NSubstitute for .NET Interfaces
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
open NSubstitute
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``calls repository with correct ID`` () = task {
|
||||||
|
let repo = Substitute.For<IOrderRepository>()
|
||||||
|
repo.FindByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult(Some testOrder))
|
||||||
|
|
||||||
|
let service = OrderService(repo)
|
||||||
|
let! _ = service.GetOrder(testOrder.Id, CancellationToken.None)
|
||||||
|
|
||||||
|
do! repo.Received(1).FindByIdAsync(testOrder.Id, Arg.Any<CancellationToken>())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ASP.NET Core Integration Tests
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type OrderApiTests (factory: WebApplicationFactory<Program>) =
|
||||||
|
interface IClassFixture<WebApplicationFactory<Program>>
|
||||||
|
|
||||||
|
let client =
|
||||||
|
factory.WithWebHostBuilder(fun builder ->
|
||||||
|
builder.ConfigureServices(fun services ->
|
||||||
|
services.RemoveAll<DbContextOptions<AppDbContext>>() |> ignore
|
||||||
|
services.AddDbContext<AppDbContext>(fun options ->
|
||||||
|
options.UseInMemoryDatabase("TestDb") |> ignore) |> ignore))
|
||||||
|
.CreateClient()
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
member _.``GET order returns 404 when not found`` () = task {
|
||||||
|
let! response = client.GetAsync($"/api/orders/{Guid.NewGuid()}")
|
||||||
|
test <@ response.StatusCode = HttpStatusCode.NotFound @>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
MyApp.Tests/
|
||||||
|
Unit/
|
||||||
|
OrderServiceTests.fs
|
||||||
|
PaymentServiceTests.fs
|
||||||
|
Integration/
|
||||||
|
OrderApiTests.fs
|
||||||
|
OrderRepositoryTests.fs
|
||||||
|
Properties/
|
||||||
|
OrderPropertyTests.fs
|
||||||
|
Helpers/
|
||||||
|
TestData.fs
|
||||||
|
TestDeps.fs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-Pattern | Fix |
|
||||||
|
|---|---|
|
||||||
|
| Testing implementation details | Test behavior and outcomes |
|
||||||
|
| Mutable shared test state | Fresh state per test |
|
||||||
|
| `Thread.Sleep` in async tests | Use `Task.Delay` with timeout, or polling helpers |
|
||||||
|
| Asserting on `sprintf` output | Assert on typed values and pattern matches |
|
||||||
|
| Ignoring `CancellationToken` | Always pass and verify cancellation |
|
||||||
|
| Skipping property-based tests | Use FsCheck for any function with clear invariants |
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- `dotnet-patterns` - Idiomatic .NET patterns, dependency injection, and architecture
|
||||||
|
- `csharp-testing` - C# testing patterns (shared infrastructure like WebApplicationFactory and Testcontainers applies to F# too)
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
dotnet test --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
# Run specific project
|
||||||
|
dotnet test tests/MyApp.Tests/
|
||||||
|
|
||||||
|
# Filter by test name
|
||||||
|
dotnet test --filter "FullyQualifiedName~OrderService"
|
||||||
|
|
||||||
|
# Watch mode during development
|
||||||
|
dotnet watch test --project tests/MyApp.Tests/
|
||||||
|
```
|
||||||
@ -179,6 +179,7 @@ function runTests() {
|
|||||||
assert.ok(languages.includes('cpp'));
|
assert.ok(languages.includes('cpp'));
|
||||||
assert.ok(languages.includes('c'));
|
assert.ok(languages.includes('c'));
|
||||||
assert.ok(languages.includes('csharp'));
|
assert.ok(languages.includes('csharp'));
|
||||||
|
assert.ok(languages.includes('fsharp'));
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('resolves a real project profile with target-specific skips', () => {
|
if (test('resolves a real project profile with target-specific skips', () => {
|
||||||
@ -420,6 +421,17 @@ function runTests() {
|
|||||||
'csharp should resolve to framework-language module');
|
'csharp should resolve to framework-language module');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('resolves fsharp legacy compatibility into framework-language module', () => {
|
||||||
|
const selection = resolveLegacyCompatibilitySelection({
|
||||||
|
target: 'cursor',
|
||||||
|
legacyLanguages: ['fsharp'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(selection.moduleIds.includes('rules-core'));
|
||||||
|
assert.ok(selection.moduleIds.includes('framework-language'),
|
||||||
|
'fsharp should resolve to framework-language module');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('keeps antigravity legacy compatibility selections target-safe', () => {
|
if (test('keeps antigravity legacy compatibility selections target-safe', () => {
|
||||||
const selection = resolveLegacyCompatibilitySelection({
|
const selection = resolveLegacyCompatibilitySelection({
|
||||||
target: 'antigravity',
|
target: 'antigravity',
|
||||||
|
|||||||
@ -234,6 +234,21 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log('\nF# Detection:');
|
||||||
|
|
||||||
|
if (test('detects fsharp from project and source files', () => {
|
||||||
|
const dir = createTempDir();
|
||||||
|
try {
|
||||||
|
writeTestFile(dir, 'App.fsproj', '<Project Sdk="Microsoft.NET.Sdk"></Project>');
|
||||||
|
writeTestFile(dir, 'Program.fs', 'printfn "hello"\n');
|
||||||
|
const result = detectProjectType(dir);
|
||||||
|
assert.ok(result.languages.includes('fsharp'));
|
||||||
|
assert.strictEqual(result.primary, 'fsharp');
|
||||||
|
} finally {
|
||||||
|
cleanupDir(dir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Go detection
|
// Go detection
|
||||||
console.log('\nGo Detection:');
|
console.log('\nGo Detection:');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user