Install any skill in seconds. Free to start, no credit card required.
Get Started Free →測試檔案最佳實踐規範。
SkillScore86/100
Test results 86
3/3 safety checks passed
32%86%of test cases passed +55 pts
Passed 19 of 22 test cases with the skill, and 7 of 22 without it.
12 fixed7 passed either way3 still failing0 broke
One test task請幫我用 Node.js 原生測試 (`node:test`) 撰寫 `parseHeader(raw: string)` 的測試檔。我的草稿開頭是 `import assert from 'node:assert/strict'; import { test } from 'node:test'; test('parses key-value', () => { assert.deepStrictEqual(parseHeader('a: 1'), { a: '1' }); });`。請將這段程式碼修正為符合團隊型別推導與斷言隔離規範的版本。
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add bluelovers-test-file-best-practices
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install bluelovers-test-file-best-practicesThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/bluelovers-test-file-best-practices/SKILL.md https://app.decimal.ai/s/bluelovers-test-file-best-practices/SKILL.md.claude/skills/bluelovers-test-file-best-practices/SKILL.mdThis gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add bluelovers-test-file-best-practicesimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add bluelovers-test-file-best-practicesfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add bluelovers-test-file-best-practicesimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add bluelovers-test-file-best-practicesimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add bluelovers-test-file-best-practicesfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add bluelovers-test-file-best-practicesfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 6 other files are not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add bluelovers-test-file-best-practicesimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add bluelovers-test-file-best-practicesfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 6 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add bluelovers-test-file-best-practicesfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 6 other files are not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add bluelovers-test-file-best-practicesimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 6 other files are not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add bluelovers-test-file-best-practicesimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 6 other files are not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add bluelovers-test-file-best-practicesfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("bluelovers-test-file-best-practices")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/bluelovers-test-file-best-practices/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/bluelovers-test-file-best-practices/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 6 other files are not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add bluelovers-test-file-best-practicesNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/bluelovers-test-file-best-practices/SKILL.md https://app.decimal.ai/s/bluelovers-test-file-best-practices/SKILL.md.agents/skills/bluelovers-test-file-best-practices/SKILL.mdThis gets SKILL.md only. The skill's 6 other files are not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 6 other files are not included.
Raw files: /s/bluelovers-test-file-best-practices/SKILL.md · /s/bluelovers-test-file-best-practices.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/bluelovers-test-file-best-practices/SKILL.mdcurl -fsSL https://app.decimal.ai/s/bluelovers-test-file-best-practices.jsonThis gets SKILL.md only. The skill's 6 other files are not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
本規則定義了撰寫或重構測試檔案時的最佳實踐,確保測試程式碼的可維護性和可讀性。
📌 版本前提與探索限制:執行相關任務時,除非已經得知版本資訊,否則一律假設處於最新版環境。 既有代碼就是使用指南:指定測試檔或區塊時,直接依既有 import、呼叫、Mock 與斷言進行修改;嚴禁預先探索無關檔案、目錄、依賴、實作細節、API 或快照,亦不主動進行環境偵測與版本查詢(如 Node.js 版本、tsx 路徑等)。 僅實際遇錯、明確調查需求或必要資訊不足時,才進行與問題直接相關的最小範圍查詢;如果不相容,會被動地從錯誤訊息中得知。
測試檔案的擺放位置應根據專案規模與測試特性選擇適當的模式。
| 模式 | 說明 | 適用場景 |
|---|---|---|
| Centralized / Decoupled(集中/分離) | 測試檔案統一放置於獨立的測試目錄(如 test/、__tests__/),與原始碼分離 | 大型專案、跨模組測試、多人協作 |
| Co-located(同目錄) | 測試檔案放置於原始碼相同目錄(如 src/module.spec.ts) | 小型專案、單一模組、簡易測試 |
優先採用 Centralized / Decoupled(集中/分離)
若原始測試已採用 Co-located(同目錄),則沿用該模式
複雜或橫跨多模組的測試,應採用 Centralized / Decoupled
# Centralized / Decoupled(集中/分離)- 優先採用
project/
├── src/
│ └── modules/
│ ├── user/
│ │ └── user.service.ts
│ └── order/
│ └── order.service.ts
└── test/ # 獨立測試目錄
├── fixtures/
├── helpers/
├── user/
│ └── user.service.spec.ts
└── order/
└── order.service.spec.ts
# Co-located(同目錄)- 僅當原始測試已採用時沿用
project/
└── src/
└── modules/
├── user/
│ ├── user.service.ts
│ └── user.service.spec.ts # 測試檔案與原始碼同目錄
└── order/
├── order.service.ts
└── order.service.spec.ts
無論選擇何種模式,皆須遵守以下章節的規則:
避免在單一測試檔案中放置過多測試案例。
describe 區塊時,考慮分割test/
├── module/
│ ├── feature-a.spec.ts # 功能 A 相關測試
│ ├── feature-b.spec.ts # 功能 B 相關測試
│ └── edge-cases.spec.ts # 邊界案例測試
// ❌ 單一檔案包含過多測試
describe('UserService', () => {
describe('create', () => { /* 50+ 測試案例 */ });
describe('update', () => { /* 50+ 測試案例 */ });
describe('delete', () => { /* 50+ 測試案例 */ });
describe('query', () => { /* 50+ 測試案例 */ });
});
// ✅ 分割後的測試檔案
// test/user-service/create.spec.ts
describe('UserService.create', () => { /* 相關測試 */ });
// test/user-service/update.spec.ts
describe('UserService.update', () => { /* 相關測試 */ });
根據測試框架使用不同的檔案副檔名,以便未來同時使用多個測試工具時能夠區分。
*.spec.ts*.test.ts採用不同命名慣例的優點:框架識別、並行使用、設定隔離(各框架於設定檔限定匹配模式)。
Reference: 測試框架相容性與 API 對應 - 五框架命名規則表、預設檔名匹配模式、設計理念與注意事項。
在結果可控的情況下,優先使用 snapshot 測試。
toMatchSnapshot() - 用於一般輸出比對toThrowErrorMatchingSnapshot() - 用於錯誤訊息比對// ✅ 使用 toMatchSnapshot 驗證複雜輸出
test('should parse configuration correctly', () => {
const result = parseConfig(rawConfig);
expect(result).toMatchSnapshot();
});
// ✅ 使用 toThrowErrorMatchingSnapshot 驗證錯誤
test('should throw error for invalid input', () => {
expect(() => validateInput(invalidInput)).toThrowErrorMatchingSnapshot();
});
如有驗證結果的必要,應適度加上指定值或類型。
使用 toMatchSnapshot() 的 property matchers 參數,可以同時驗證特定欄位的值,並將其餘欄位交給 snapshot 處理。
// ✅ 驗證特定欄位,其餘交給 snapshot
const result = npa('my-lodash@npm:[email protected]');
expect(result).toMatchSnapshot({
"name": "my-lodash",
"subSpec": {
"name": "lodash",
"rawSpec": "4.17.21",
}
});
這樣 snapshot 會比對完整物件結構,同時確保 name 和 subSpec.name、subSpec.rawSpec 的值正確。
對於動態資料(如時間戳、隨機 ID),可使用 Asymmetric matchers 例如:
expect.any(constructor)expect.arrayContaining(array)參閱:
Reference: 斷言語法優化 - API 可讀性原則、
toHaveProperty、toHaveLength與物件比對(toMatchObject/objectContaining)等與 Snapshot 無關的斷言寫法優化。
測試檔案開頭應加入 TypeScript 三斜線參考指令(並依框架匯入測試函數),以確保正確引入類型定義。
t.assert 而非 node:assert/strict(避免使用獨立全域斷言,以確保斷言計入上下文並精準隔離失敗)。t: test.TestContext,確保編輯器能完整推導型別。Reference: 測試框架相容性與 API 對應 - 通用/Jest/Vitest/Bun/Node.js 原生測試的 Header 範例、框架注意事項(如 Jest v30 移除
toThrowError)與 matcher API 對應表。
// 測試檔案頭部
import { functionToTest } from '../src/module';
// 常數與測試資料
const TEST_FIXTURES = {
validInput: { /* ... */ },
invalidInput: { /* ... */ },
};
// 測試套件
describe('functionToTest', () => {
describe('正常案例', () => {
test('should handle valid input', () => {
// ...
});
});
describe('邊界案例', () => {
test('should handle edge case', () => {
// ...
});
});
describe('錯誤處理', () => {
test('should throw error for invalid input', () => {
expect(() => functionToTest(invalidInput)).toThrowErrorMatchingSnapshot();
});
});
});
一個 test() 應只驗證一個目的。
當測試標題只描述行為 A、內容卻也斷言行為 B,或在測試內重新實作被測函式的控制流時,失敗訊息將無法指出真正出錯的環節。
常見徵兆:
if (!guard) return ...,複製被測函式的走訪流程if (!condition) return 讓守衛失敗時測試未執行任何斷言卻仍通過Array.isArray(x) 與 deepStrictEqual 重複、.length 與陣列比對重複原則:快照記錄「走訪結果與實例結構」,具體輸出交給明確斷言。 兩者各司其職,不要在同一筆測試中同時承擔。
Reference: 單一測試混雜多個目的 - 完整不良案例、問題解析、改善版本與不必要雜訊速查表。
測試檔案的註解或測試標題不應出現非必要的計數/序列。
測試隨時都有可能會增減或移動位置,這些計數會增加額外的非必要修改工作。
// ❌ 不良範例:包含計數的測試標題
/**
* 測試 12:deep 巢狀物件
* Test 12: Deep nested object
*/
test('should correctly validate deeply nested structures', () => {
// ...
});
// ✅ 良好範例:移除計數的測試標題
/**
* 測試:deep 巢狀物件
* Test: Deep nested object
*/
test('should correctly validate deeply nested structures', () => {
// ...
});
原則:
describe 區塊進行邏輯組織// ✅ 良好範例:使用描述性標題區分測試
describe('validation', () => {
test('should handle valid input', () => { /* ... */ });
test('should handle invalid input', () => { /* ... */ });
test('should handle empty input', () => { /* ... */ });
});
// ✅ 良好範例:使用 describe 區塊分組
describe('UserService', () => {
describe('create', () => {
test('should create user with valid data', () => { /* ... */ });
test('should reject duplicate email', () => { /* ... */ });
});
describe('update', () => {
test('should update user info', () => { /* ... */ });
test('should handle not found error', () => { /* ... */ });
});
});
當多個測試有共通邏輯時,應建立共用的工具函數,方便日後更新擴充時能夠輕鬆維護。
// ❌ 邏輯重複,維護困難
describe('UserService', () => {
test('should create user', () => {
const user = {
id: generateId(),
name: 'Test User',
email: '[email protected]',
createdAt: new Date(),
status: 'active'
};
// ...測試邏輯
});
test('should update user', () => {
const user = {
id: generateId(),
name: 'Test User',
email: '[email protected]',
createdAt: new Date(),
status: 'active'
};
// ...測試邏輯
});
});
// ✅ 提取共用邏輯至工具函數
// test/lib/helpers/user-factory.ts
export function _createTestUser(overrides?: Partial<IUser>) {
return {
id: generateId(),
name: 'Test User',
email: '[email protected]',
createdAt: new Date(),
status: 'active',
...overrides
};
}
// test/user-service.spec.ts
import { _createTestUser } from './lib/helpers/user-factory';
describe('UserService', () => {
test('should create user', () => {
const user = _createTestUser();
// ...測試邏輯
});
test('should update user', () => {
const user = _createTestUser({ name: 'Updated Name' });
// ...測試邏輯
});
});
test/lib/helpers/ 或 test/lib/utils/ 目錄下_createTestUser、_mockApiResponse)對於可能會後續擴充的參數,應使用物件參數(Object Parameters)而非多個獨立參數。
當函式接收多個參數時,若這些參數未來可能有增減的需求,使用物件參數可以:
?)// ❌ 使用多個獨立參數,未來擴充困難
function _createAndValidateSnapshot(
capturedData: any,
validData: any,
result: any
): void {
// ...實作
}
// 呼叫時語意不清,且未來新增參數需修改函式簽章
_createAndValidateSnapshot(data, expected, actual);
// ✅ 使用物件參數,弹性擴充
/**
* 建立並驗證 Snapshot
* Create and validate snapshot
*
* @param data - 測試資料物件 / Test data object
* @param data.capturedData - 擷取的資料 / Captured data
* @param data.validData - 驗證用的資料 / Data for validation
* @param data.result - 測試結果 / Test result
*/
export function _createAndValidateSnapshot(
data: {
capturedData: any;
validData: any;
result: any;
}
): void {
const { capturedData, validData, result } = data;
// ...實作
}
// 呼叫時語意清晰
_createAndValidateSnapshot({
capturedData: data,
validData: expected,
result: actual
});
// 未來需要新增參數時,只需擴充物件型別
interface ICreateAndValidateSnapshotData {
/** 擷取的資料 / Captured data */
capturedData: any;
/** 驗證用的資料 / Data for validation */
validData: any;
/** 測試結果 / Test result */
result: any;
/** 是否需要更新 snapshot / Whether to update snapshot */
updateSnapshot?: boolean;
/** 自定義驗證選項 / Custom validation options */
validationOptions?: {
strict?: boolean;
ignoreKeys?: string[];
};
}
export function _createAndValidateSnapshot(
data: ICreateAndValidateSnapshotData
): void {
// ...實作
}
函式參數數量是否 >= 3?
│
├─ 是 → 參數是否可能未來擴充?
│ │
│ ├─ 是 → 使用物件參數
│ │
│ └─ 否 → 可考慮使用物件參數或獨立參數
│
└─ 否 → 使用獨立參數
any(除非必要)測試資料相關的主題集中於此章節,包含 Fixtures 檔案管理、資料集結構規範,以及用於產生/整理測試資料的輔助腳本,避免測試資料邏輯散落各處。
test/
├── fixtures/ # 測試靜態資料集中管理
│ ├── users/ # 按功能模組分類
│ │ ├── valid-user.json # 有效的使用者資料
│ │ └── invalid-users.json # 多種無效使用者資料
│ ├── configs/ # 配置文件
│ │ └── app-config.json
│ └── api-responses/ # API 回應模擬
│ └── mock-api-response.json
├── module/
│ ├── feature-a.spec.ts
│ └── feature-b.spec.ts
└── scripts/ # 測試輔助腳本(可選)
// ✅ 從 fixtures 目錄載入測試資料
import validUser from '../fixtures/users/valid-user.json';
import mockApiResponse from '../fixtures/api-responses/mock-api-response.json';
describe('UserService', () => {
test('should create user with valid data', () => {
const result = createUser(validUser);
expect(result).toBeDefined();
});
test('should handle API response correctly', () => {
const result = processResponse(mockApiResponse);
expect(result).toMatchSnapshot();
});
});
.json、.csv 等標準格式測試資料應獨立於測試邏輯,使用專門的 fixture 文件定義,每個測試用例應包含完整的測試資料,包括輸入、預期輸出和描述。
規則: 測試資料應獨立於測試邏輯,使用專門的 fixture 文件定義。
test/
├── fixtures/ # 測試資料集中管理
│ └── <module>-test-cases.ts # 各模組測試資料集
└── <module>/
└── <feature>.test.ts # 測試邏輯(引用 fixture)
規則: 每個測試用例應包含完整的測試資料,包括輸入、預期輸出和描述。
/**
* 測試用例結構
* Test case structure
*/
export interface ITestCase
{
/** 測試用例名稱 / Test case name */
name: string;
/** 輸入資料(測試目標)/ Input data (test target) */
input: any;
/** 預期結果 / Expected result */
expected: any;
/** 備註(可選)/ Note (optional) */
note?: string;
}
/**
* 測試群組結構
* Test group structure
*/
export interface ITestGroup
{
/** 測試群組名稱 / Test group name */
name: string;
/** 測試用例陣列 / Test cases array */
testCases: ITestCase[];
}
規則: 使用單一陣列導出所有測試群組,便於自動產生測試。
/**
* 完整測試資料集
* Complete test dataset
*
* 所有測試群組的集合
*/
export const testGroups: ITestGroup[] = [
{
name: "基本類型",
testCases: [
{
name: "stringWithDefault",
input: z.string().default("hello"),
expected: "hello",
},
// ... 更多測試用例
],
},
// ... 更多測試群組
];
規則: 測試腳本應自動遍歷測試資料集,無需手動定義每個測試。
import { testGroups } from "../fixtures/zod-defaults-test-cases";
/**
* 自動產生所有測試群組
* Automatically generate all test groups
*/
for (const group of testGroups)
{
describe(group.name, () =>
{
for (const testCase of group.testCases)
{
test(testCase.name, () =>
{
// 執行測試邏輯
runTestCase(testCase);
});
}
});
}
規則: 測試資料集文件應與被測模組對應。
| 模組 | 測試資料集 | 測試文件 |
|---|---|---|
src/config/schema.ts | test/fixtures/config-test-cases.ts | test/issues/config-all.test.ts |
src/utils/helper.ts | test/fixtures/helper-test-cases.ts | test/issues/helper-all.test.ts |
export const testGroups: ITestGroup[] = [
{
name: "基本類型",
testCases: [
// 基本類型測試用例
],
},
{
name: "巢狀結構",
testCases: [
// 巢狀結構測試用例
],
},
{
name: "邊界情況",
testCases: [
// 邊界情況測試用例
],
},
];
export const testGroups: ITestGroup[] = [
{
name: "字串輸入",
testCases: [
// 字串輸入測試用例
],
},
{
name: "數字輸入",
testCases: [
// 數字輸入測試用例
],
},
{
name: "物件輸入",
testCases: [
// 物件輸入測試用例
],
},
];
ITestCase 和 ITestGroup 接口確保類型安全若有需要額外建立腳本用於抓取更新資料、整理歸納等,可放置於 test/scripts 資料夾。
test/
├── fixtures/ # 靜態測試資料
├── scripts/ # 測試輔助腳本
│ ├── fetch-test-data.ts # 從遠端抓取測試資料
│ ├── update-mock-data.ts # 更新模擬資料
│ └── generate-fixtures.ts # 生成測試 fixtures
└── module/
└── feature-a.spec.ts
# 直接使用 tsx 執行測試腳本
tsx test/scripts/fetch-test-data.ts
tsx test/scripts/update-mock-data.ts
# 或使用 ts-node(若 tsx 無法正常運作)
ts-node test/scripts/generate-fixtures.ts
.gitignore(如大型資料庫或臨時檔案)當測試需要創建臨時檔案或臨時目錄時,應在專案內建立專用的臨時目錄來操作,而非直接在根目錄或 src 目錄下創建。
當實作或測試需要驗證「檔案內容」時,除非有必要性的意圖與理由,否則應建立不需實際寫入檔案也能取得內容的 API 或 helper 邏輯,避免不必要的讀寫行為,甚至還得額外建立臨時檔案並且清除。
// ❌ 只為了取得內容,付出 寫入 → 讀回 → 清理 的代價
const optimized = new IgnoreFile(path.join(__TEMP_DIR, 'finalized.gitignore'), raw.join('\n'));
optimized.save();
t.assert.deepStrictEqual(readFileSync(optimized.path, 'utf8'), [
'# note',
'*.log',
'',
'',
'',
].join('\n'));
// ✅ 生產程式碼:stringify() 產生內容,save() 只負責持久化
stringify(opts?: IIgnoreFileOptions): string
{
// do something
return this.content;
}
save(opts?: IIgnoreFileOptions): this
{
outputFileSync(this.path, this.stringify(opts), {
encoding: ENCODING,
});
return this;
}
// ✅ 測試:path 僅為路徑宣告,不會實際建立檔案
const optimized = new IgnoreFile(path.join(__TEMP_DIR, 'finalized.gitignore'), raw.join('\n'));
t.assert.deepStrictEqual(optimized.stringify(), [
'# note',
'*.log',
'',
'',
'',
].join('\n'));
即使路徑僅供內容驗證、預期不會實際建檔,也必須限制在專案臨時目錄內,避免任何非預期的副作用(遺漏的 save()、第三方函式庫內部讀寫、誤呼叫 readFileSync 等)在專案根目錄或其他位置意外造成真實讀寫。
// ❌ 裸相對路徑:一旦意外被讀寫,會落在當前工作目錄(專案根目錄)
const __VIRTUAL_GITIGNORE = 'virtual/.gitignore';
// ✅ 以臨時目錄為根:即使意外讀寫也只會發生在 test/temp/ 範圍內
const __VIRTUAL_GITIGNORE = path.join(__TEST_TEMP, 'virtual/.gitignore');
__TEST_TEMP、__TEMP_DIR)為根,使用 path.join() 組出'virtual/.gitignore'、'./out.json')與專案臨時目錄外的絕對路徑__VIRTUAL_*),但其值必須落在臨時目錄範圍內stringify() / render() / serialize() / toJSON())與檔案持久化(save() / write())應拆為兩個方法,且 save() 內部應呼叫 stringify(),確保兩者行為一致path.join(__TEST_TEMP, ...) 組出,避免意外的真實讀寫落於臨時目錄之外_serialize() 等 helper,同樣以不落盤的方式取得內容\n / \r\n)、編碼、檔案權限禁止對臨時目錄以外的路徑進行讀取以外的行為(包含但不限於 寫入/更改/刪除/建立)。
如果需要測試,則必須建立安全的 mock/sandbox 環境以模擬的方式執行,確保測試隔離性,避免意外修改或刪除重要的專案檔案。
// ✅ 正確:在臨時目錄下進行操作
const tempDir = path.join(process.cwd(), 'test', 'temp', 'test-output');
const tempFile = path.join(tempDir, 'output.json');
fs.writeFileSync(tempFile, data); // ✅ 允許
fs.readFileSync(tempFile); // ✅ 允許
fs.unlinkSync(tempFile); // ✅ 允許(在臨時目錄內)
// ❌ 錯誤:對臨時目錄外的路徑進行寫入/刪除
const srcFile = path.join(process.cwd(), 'src', 'config.json');
fs.writeFileSync(srcFile, data); // ❌ 禁止:寫入
fs.unlinkSync(srcFile); // ❌ 禁止:刪除
fs.mkdirSync(path.join(process.cwd(), 'new-dir')); // ❌ 禁止:新增
// ✅ 正確:對臨時目錄外的路徑僅進行讀取
const configData = fs.readFileSync(
path.join(process.cwd(), 'src', 'config.json'),
'utf-8'
); // ✅ 允許:僅讀取
涉及檔案寫入/刪除的模組或測試,應使用 Mock 環境防止 fs 操作臨時目錄以外的路徑。詳細的 Mock 安全規則、路徑控管模組用法與共享路徑定義,請參閱 references/temp-file-management.md。
project/
├── src/ # 原始碼
├── test/ # 測試目錄
│ ├── fixtures/ # 靜態測試資料
│ └── temp/ # 臨時檔案專用目錄 ⭐
│ ├── test-output-1708152000000/ # 測試輸出檔案(具有唯一性 ID)
│ ├── mock-cache/ # 模擬快取資料(可覆寫)
│ └── uploads/ # 上傳測試檔案
└── temp/ # 專案層級臨時目錄 ⭐
├── test-output-1708152000000/ # 測試輸出檔案
├── build-cache/ # 建置快取(可覆寫)
└── uploads/ # 上傳測試檔案
原則一:禁止直接建立在臨時主目錄下,必須建立在子目錄中。
// ✅ 正確:建立於 temp 子目錄下
const tempDir = path.join(process.cwd(), 'temp', 'test-output');
// ❌ 錯誤:直接建立在 temp 主目錄
const tempDir = path.join(process.cwd(), 'temp');
原則二:臨時子目錄名稱應具有唯一性的 ID(例如 timestamp),除非是多個測試共用的臨時子目錄或者可以被多次覆寫的臨時子目錄。
// ✅ 正確:具有唯一性 ID 的臨時目錄
const timestamp = Date.now();
const tempDir = path.join(process.cwd(), 'temp', `test-output-${timestamp}`);
// ✅ 正確:可覆寫的共用臨時目錄(不需要唯一性 ID)
const tempDir = path.join(process.cwd(), 'temp', 'mock-cache');
// ❌ 錯誤:直接建立在 temp 主目錄
const tempDir = path.join(process.cwd(), 'temp');
// ✅ 正確:在專用臨時目錄下操作
import * as path from 'path';
import * as fs from 'fs';
// 取得臨時目錄路徑(具有唯一性 ID)
const getTempDir = (subDir: string) => {
const timestamp = Date.now();
const tempDir = path.join(process.cwd(), 'test', 'temp', `${subDir}-${timestamp}`);
// 確保目錄存在
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
return tempDir;
};
// 取得共用臨時目錄路徑(可覆寫,無需唯一性 ID)
const getSharedTempDir = (subDir: string) => {
const tempDir = path.join(process.cwd(), 'test', 'temp', subDir);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
return tempDir;
};
// 在具有唯一性 ID 的臨時目錄下創建檔案
test('should generate output file', async () => {
const outputDir = getTempDir('test-output');
const outputFile = path.join(outputDir, 'result.json');
const result = await processData(inputData);
fs.writeFileSync(outputFile, JSON.stringify(result));
expect(fs.existsSync(outputFile)).toBe(true);
});
使用 afterEach 僅清理本次測試創建的臨時子目錄,避免廣域性清除影響並行測試。完整的自動清理範例、需要審閱時的保留策略與輔助資訊類型,請參閱 references/temp-file-management.md。
// ❌ 錯誤:廣域性清除會影響其他正在運行的測試
afterEach(() => {
fs.rmSync(path.join(process.cwd(), 'test', 'temp'), { recursive: true });
});
確保臨時目錄不會被提交至版本控制系統:
# .gitignore
# 測試臨時檔案
test/temp/
# 專案層級臨時目錄
temp/
tmp/
# 特定臨時檔案
*.tmp
*.temp
temp/test-output/ 而非 temp/)stringify() 等回傳內容的 API 取得內容後斷言,避免非必要的讀寫與臨時檔案建立/清除path.join(__TEST_TEMP, ...) 組出,禁止裸相對路徑,避免意外造成真實讀寫temp/xxxx/ 底下的檔案或目錄,而非刪除 temp 目錄本身test/temp/、temp/),讓團隊成員一目了然test/fixtures/ 而非臨時目錄.gitignore 排除或使用虛擬檔案系統測試中涉及系統資源(檔案系統、日期時間、環境變數、網路請求)時,應遵循以下處理原則:
| 系統資源 | 處理方式 | 優先順序 |
|---|---|---|
| 檔案系統 (fs) | jest.mock('fs') 或臨時目錄 | Mock 優先 |
| 日期時間 (Date) | jest.useFakeTimers() / spyOn(Date, 'now') | Mock 優先 |
| 環境變數 | beforeEach 複製 process.env | 備份與恢復 |
| 網路請求 | fetchMock / nock / msw | 一律 Mock |
日期時間相關參考資源:
需要使用系統資源?
│
├─ 檔案系統 ───────────────────────────────────┐
│ 可以 Mock? │
│ ├─ 是 → 使用 jest.mock('fs') + memfs-extra │
│ │ 或 spyOn 模組方法 │
│ └─ 否 → 使用專案內臨時目錄 + try/finally 清理 │
│ │
├─ 日期時間 ───────────────────────────────────┤
│ 使用 jest.useFakeTimers() │
│ 或 spyOn(Date, 'now') │
│ │
├─ 環境變數 ───────────────────────────────────┤
│ beforeEach: 複製 process.env │
│ afterAll: 恢復原值 │
│ │
└─ 網路請求 ───────────────────────────────────┘
一律使用 fetchMock / nock / msw
/etc/、/usr/、C:\Windows\ 等系統目錄,或專案根目錄下的固定路徑tmp/ 或 .tmp/),禁止寫入系統級臨時目錄(如 /tmp、os.tmpdir())jest.mock()、spyOn()、useFakeTimers(), memfs-extra 能滿足需求時,不要自行實作複雜的 mock 機制完整的系統依賴處理範例(檔案系統 Mock、依賴模組內部 fs 的 Mock 策略、日期時間 fake timers、環境變數備份與恢復、網路請求 Mock)請參閱:
不應撰寫僅斷言常數字面值的測試(tautological tests,重言式/同義反覆)。 此類測試只是重複原始碼中的常數定義,無法捕捉行為錯誤,且每次調整常數都需同步修改,徒增維護成本。
expect(CONSTANT).toBe(<該常數的字面值>) → 多半為無意義測試// ❌ 無意義:重複常數定義,且預期值寫死字面值
t.assert.strictEqual(DEFAULT_WORKSPACE_PROTOCOL, 'workspace:');
t.assert.strictEqual(getWorkspaceProtocol(), 'workspace:');
// ✅ 良好:刪除常數定義斷言,預期值引用常數
t.assert.strictEqual(getWorkspaceProtocol(), DEFAULT_WORKSPACE_PROTOCOL);
Reference: 避免無意義的測試 - 無意義斷言類型速查、常數字面值與重複字面值的錯誤/正確案例,以及允許字面值斷言的例外情境。
開始撰寫測試
│
▼
測試案例是否超過 100 行?
│
├─ 是 → 考慮依功能分割成多個檔案
│
└─ 否 → 繼續在同一檔案
│
▼
輸出是否複雜且穩定?
│
├─ 是 → 使用 toMatchSnapshot()
│
└─ 否 → 使用傳統 matcher
│
▼
是否測試錯誤拋出?
│
├─ 是 → 使用 toThrow() / toThrowErrorMatchingSnapshot()
│
└─ 否 → 使用適當的斷言 matcher
│
▼
是否涉及系統資源(fs / Date / env / 網路)?
│
├─ 是 → 是否只需驗證檔案內容?
│ ├─ 是 → 以非 I/O API(stringify 等)取得內容後斷言
│ └─ 否 → 使用框架 Mock 或專案內臨時目錄 + 清理
│
└─ 否 → 完成測試
test-convert-to-node-test)Automatically, when a task matches. Use when users request (1) Testing best practices, (2) Test file organization, (3) "測試檔案規範", (4) "測試最佳實踐", (5) "測試檔案組織", (6) "測試資料管理", (7) "臨時檔案管理", (8) "重構測試", (9) "優化測試", (10) "整合測試". Defines guidelines for writing and organizing test files, including test location patterns, naming conventions, snapshot testing, fixtures management, and temporary file handling.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.