Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,15 @@ testsprite agent install cline # .clinerules/testsprite-verify.md
testsprite agent install windsurf # .windsurf/rules/testsprite-verify.md
testsprite agent install antigravity # .agents/skills/testsprite-verify/SKILL.md
testsprite agent install kiro # .kiro/skills/testsprite-verify/SKILL.md
testsprite agent list # list all 7 targets with status + mode + path
testsprite agent install copilot # .github/instructions/testsprite-verify.instructions.md
testsprite agent list # list all 8 targets with status + mode + path
```

Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental).
Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental), `copilot` (experimental).

The `codex` target uses **managed-section mode** — it writes only a sentinel-delimited section inside your existing `AGENTS.md`, so your project instructions are never clobbered. Re-running without `--force` replaces the section in-place; user content outside the sentinels is always preserved.

Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf) backs up the existing file to `<path>.bak` first.
Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf, copilot) backs up the existing file to `<path>.bak` first.

## Command reference

Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,28 @@ Prefer to configure each step by hand (or learn the surface offline with `--dry-

## Commands

| Group | Command | What it does |
| --------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill |
| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes |
| | `auth remove` | Remove the active profile from the credentials file |
| **Read** | `project list` / `project get` | List projects / fetch one by id |
| | `test list` / `test get` | List tests under a project / fetch one by id |
| | `test code get` | Print (or write) the generated test source |
| | `test steps` | List the latest run's steps with screenshot / DOM pointers |
| | `test result` | Latest result; `--history` lists a test's prior runs |
| | `test failure get` | The agent entry point: one self-contained latest-failure bundle |
| | `test failure summary` | One-screen triage card (no media download) |
| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata |
| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete |
| | `test code put` | Replace generated code (etag-guarded) |
| | `test plan put` | Replace a frontend test's plan-steps |
| | `project create` / `project update` | Manage projects |
| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project <id>` runs all tests in a project in wave order |
| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project <id>` reruns all tests |
| | `test wait` | Block on a `runId` until terminal |
| | `test artifact get` | Download the failure bundle for a specific `runId` |
| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf` |
| Group | Command | What it does |
| --------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill |
| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes |
| | `auth remove` | Remove the active profile from the credentials file |
| **Read** | `project list` / `project get` | List projects / fetch one by id |
| | `test list` / `test get` | List tests under a project / fetch one by id |
| | `test code get` | Print (or write) the generated test source |
| | `test steps` | List the latest run's steps with screenshot / DOM pointers |
| | `test result` | Latest result; `--history` lists a test's prior runs |
| | `test failure get` | The agent entry point: one self-contained latest-failure bundle |
| | `test failure summary` | One-screen triage card (no media download) |
| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata |
| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete |
| | `test code put` | Replace generated code (etag-guarded) |
| | `test plan put` | Replace a frontend test's plan-steps |
| | `project create` / `project update` | Manage projects |
| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project <id>` runs all tests in a project in wave order |
| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project <id>` reruns all tests |
| | `test wait` | Block on a `runId` until terminal |
| | `test artifact get` | Download the failure bundle for a specific `runId` |
| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf`, `copilot` |

> The earlier command names — `init`, `auth configure`, `auth whoami`, `auth logout` — still work as hidden, deprecated aliases (each prints a one-line notice pointing at the new name), so existing scripts keep running. `auth configure` now runs the full `setup` (it also installs the skill).

Expand Down
15 changes: 8 additions & 7 deletions src/commands/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,13 +769,14 @@ describe('runList', () => {

const json = JSON.parse(capture.stdout.join('\n')) as ListResult[];
expect(Array.isArray(json)).toBe(true);
// 7 targets × 2 default skills = 14 rows
expect(json).toHaveLength(14);
// 8 targets × 2 default skills = 16 rows
expect(json).toHaveLength(16);
const targets = json.map(r => r.target);
expect(targets).toContain('claude');
expect(targets).toContain('cursor');
expect(targets).toContain('cline');
expect(targets).toContain('windsurf');
expect(targets).toContain('copilot');
expect(targets).toContain('antigravity');
expect(targets).toContain('kiro');
expect(targets).toContain('codex');
Expand Down Expand Up @@ -949,11 +950,11 @@ describe('runInstall — all own-file targets', () => {
});

// ---------------------------------------------------------------------------
// Dry-run for all six own-file targets
// Dry-run for all seven own-file targets
// ---------------------------------------------------------------------------

describe('runInstall — dry-run all own-file targets', () => {
it('writes nothing for any of the six own-file targets (default 2 skills = 12 would-write lines)', async () => {
it('writes nothing for any of the seven own-file targets (default 2 skills = 14 would-write lines)', async () => {
const { store, fs: agentFs } = makeMemFs();
const { capture, deps } = makeCapture();

Expand All @@ -963,7 +964,7 @@ describe('runInstall — dry-run all own-file targets', () => {
output: 'text',
debug: false,
dryRun: true,
target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf'],
target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf', 'copilot'],
force: false,
},
{ cwd: CWD, fs: agentFs, ...deps },
Expand All @@ -973,9 +974,9 @@ describe('runInstall — dry-run all own-file targets', () => {
const stderrOut = capture.stderr.join('\n');
// Banner appears once
expect(stderrOut).toContain('[dry-run] no files written');
// 6 targets × 2 default skills = 12 would-write lines
// 7 targets × 2 default skills = 14 would-write lines
const wouldWriteLines = stderrOut.split('\n').filter(l => l.includes('would write'));
expect(wouldWriteLines.length).toBe(12);
expect(wouldWriteLines.length).toBe(14);
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,7 +1061,7 @@ function collect(v: string, prev: string[]): string[] {

export function createAgentCommand(deps: AgentDeps = {}): Command {
const agent = new Command('agent').description(
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Antigravity, Codex)',
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Antigravity, Kiro, Windsurf, Copilot, Codex)',
);

agent
Expand All @@ -1071,7 +1071,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command {
)
.option(
'--target <t>',
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, codex (comma-separated or repeated)',
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, copilot, codex (comma-separated or repeated)',
collect,
[],
)
Expand Down
51 changes: 48 additions & 3 deletions src/lib/agent-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,29 @@ testsprite test artifact get <run-id> --out ./out/
// ---------------------------------------------------------------------------

describe('TARGETS', () => {
it('has all seven required keys', () => {
it('has all eight required keys', () => {
const keys = Object.keys(TARGETS).sort();
expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kiro', 'windsurf']);
expect(keys).toEqual([
'antigravity',
'claude',
'cline',
'codex',
'copilot',
'cursor',
'kiro',
'windsurf',
]);
});

it('claude is GA', () => {
expect(TARGETS.claude.status).toBe('ga');
});

it('cursor, cline, windsurf, antigravity, kiro, and codex are experimental', () => {
it('cursor, cline, windsurf, copilot, antigravity, kiro, and codex are experimental', () => {
expect(TARGETS.cursor.status).toBe('experimental');
expect(TARGETS.cline.status).toBe('experimental');
expect(TARGETS.windsurf.status).toBe('experimental');
expect(TARGETS.copilot.status).toBe('experimental');
expect(TARGETS.antigravity.status).toBe('experimental');
expect(TARGETS.kiro.status).toBe('experimental');
expect(TARGETS.codex.status).toBe('experimental');
Expand All @@ -112,6 +122,7 @@ describe('TARGETS', () => {
expect(TARGETS.cline.mode).toBe('own-file');
expect(TARGETS.kiro.mode).toBe('own-file');
expect(TARGETS.windsurf.mode).toBe('own-file');
expect(TARGETS.copilot.mode).toBe('own-file');
});

it('codex target has mode managed-section', () => {
Expand Down Expand Up @@ -341,11 +352,45 @@ describe('windsurf renders within the rules-file budget', () => {
});
});

describe('renderForTarget("copilot")', () => {
const result = renderForTarget('copilot', 'testsprite-verify', STUB_BODY);

it('returns the .github/instructions path', () => {
expect(result.path).toBe('.github/instructions/testsprite-verify.instructions.md');
});

it('uses the Copilot frontmatter (applyTo + description)', () => {
expect(result.content.startsWith('---\n')).toBe(true);
expect(result.content).toContain(`description: ${SKILL_DESCRIPTION}`);
expect(result.content).toContain("applyTo: '**'");
});

it('does NOT carry the Claude/Cursor/Windsurf frontmatter keys', () => {
const match = /^---\n([\s\S]*?)\n---/.exec(result.content);
const fm = match?.[1] ?? '';
expect(fm).not.toContain('name:'); // claude key
expect(fm).not.toContain('alwaysApply:'); // cursor .mdc key
expect(fm).not.toContain('trigger:'); // windsurf Cascade key
});

it('renders the compact verify body (applyTo:** is always-on, so keep it small)', () => {
// Uses the REAL bodies (no stub): copilot always-injects, so like windsurf it
// ships the trimmed verify body while keeping the load-bearing command.
const copilot = renderForTarget('copilot', 'testsprite-verify');
const claude = renderForTarget('claude', 'testsprite-verify');
expect(copilot.content.length).toBeLessThan(claude.content.length);
expect(copilot.content).not.toContain('The verification loop that flies');
expect(copilot.content).toContain('testsprite test run');
});
});

// ---------------------------------------------------------------------------
// Content integrity — load-bearing command strings must survive any body trim
// ---------------------------------------------------------------------------

describe('content integrity — own-file targets', () => {
// Full-body own-file targets. Compact-body targets (windsurf, copilot) are
// excluded — they render the trimmed verify body; see their dedicated tests.
const ownFileTargets: Array<'claude' | 'cursor' | 'cline' | 'antigravity' | 'kiro'> = [
'claude',
'cursor',
Expand Down
30 changes: 29 additions & 1 deletion src/lib/agent-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type AgentTarget =
| 'antigravity'
| 'codex'
| 'kiro'
| 'windsurf';
| 'windsurf'
| 'copilot';

export interface TargetSpec {
status: 'ga' | 'experimental';
Expand Down Expand Up @@ -149,6 +150,19 @@ function wrapWindsurf(_name: string, description: string, body: string): string
return `---\ntrigger: model_decision\ndescription: ${description}\n---\n\n${body}\n`;
}

/**
* GitHub Copilot reads path-specific custom instructions from
* `.github/instructions/*.instructions.md` (VS Code / Visual Studio / GitHub
* Copilot Chat). Each file carries YAML frontmatter with `applyTo` — a glob that
* scopes when the instructions attach. `applyTo: '**'` attaches the guidance to
* every request in the repo, which is what a persistent verification skill wants
* (there is no on-demand "model decides" mode for Copilot instruction files, so
* always-apply is the correct idiom). `description` is surfaced in Copilot's UI.
*/
function wrapCopilot(_name: string, description: string, body: string): string {
return `---\ndescription: ${description}\napplyTo: '**'\n---\n\n${body}\n`;
}

// ---------------------------------------------------------------------------
// Landing paths
// ---------------------------------------------------------------------------
Expand All @@ -173,6 +187,8 @@ export function pathFor(target: AgentTarget, skill: string): string {
return `.kiro/skills/${skill}/SKILL.md`;
case 'windsurf':
return `.windsurf/rules/${skill}.md`;
case 'copilot':
return `.github/instructions/${skill}.instructions.md`;
case 'codex':
return 'AGENTS.md';
}
Expand Down Expand Up @@ -220,6 +236,18 @@ export const TARGETS: Record<AgentTarget, TargetSpec> = {
compactBody: true,
wrap: wrapWindsurf,
},
copilot: {
status: 'experimental',
path: pathFor('copilot', SKILL_NAME),
mode: 'own-file',
// GitHub Copilot path-specific instructions: frontmatter carries `applyTo`.
// `applyTo: '**'` means the file is ALWAYS injected into Copilot requests
// (there is no on-demand "model decides" mode like Cursor/Windsurf), so
// render the compact body to keep the always-on context cost small — the
// same reasoning that drives windsurf's compact render.
compactBody: true,
wrap: wrapCopilot,
},
/**
* codex target — managed-section mode.
*
Expand Down
Loading
Loading