From a56f783a7547fce53ba11804150c86ce3d4e9a89 Mon Sep 17 00:00:00 2001 From: davidson3556 Date: Tue, 30 Jun 2026 16:56:21 -0700 Subject: [PATCH] feat(agent): add windsurf as an install target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windsurf (Cascade) reads workspace rules from `.windsurf/rules/*.md`. Add it as an own-file agent target so `testsprite agent install --target windsurf` (and `setup --agent windsurf`) installs the TestSprite skills into a Windsurf project. Reworked onto the v0.2.0 multi-skill agent-targets API (pathFor / SKILLS / DEFAULT_SKILLS). Rule files use Cascade frontmatter with `trigger: model_decision` — the equivalent of the Cursor `.mdc` `alwaysApply: false` mode (description shown up front; full body pulled in on relevance). Budget handling: a `.windsurf/rules/*.md` file caps at ~12 K characters and Cascade silently truncates beyond it, which would cut the full ~22 KB verify skill in half. The windsurf target therefore renders the COMPACT body per skill (new `compactBody` flag + `compactBodyFor`): a skill that ships a trimmed codex asset (`testsprite-verify` → ~5 KB) uses it, while a skill whose codex contribution is only a one-liner (`testsprite-onboard`, ~6.5 KB full) keeps its full body — both land well under the cap. `agent.ts` and `renderForTarget` select the same body so installed bytes match the render. Everything else derives from the TARGETS map automatically (agent list, the setup --agent choices, skill-nudge install detection). Updated the hardcoded help strings, the --help snapshot, the agent-targets/agent unit tests (incl. Cascade-frontmatter and per-skill budget tests), the e2e matrix guards / content-integrity (gated on compactBody), and the README/DOCUMENTATION target lists (incl. the --force own-file list). --- DOCUMENTATION.md | 7 ++- README.md | 46 +++++++------- src/commands/agent.test.ts | 23 +++---- src/commands/agent.ts | 22 ++++++- src/lib/agent-targets.test.ts | 54 +++++++++++++++- src/lib/agent-targets.ts | 63 ++++++++++++++++++- test/__snapshots__/help.snapshot.test.ts.snap | 12 ++-- test/e2e/agent-install.e2e.test.ts | 20 +++++- test/e2e/setup.e2e.test.ts | 1 + 9 files changed, 193 insertions(+), 55 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 7a89216..8c0cf1a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -112,16 +112,17 @@ testsprite agent install claude # install the skill for Claude Code testsprite agent install codex # install into AGENTS.md for Codex (managed-section) testsprite agent install cursor # .cursor/rules/testsprite-verify.mdc 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 6 targets with status + mode + path +testsprite agent list # list all 7 targets with status + mode + path ``` -Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental). +Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (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) backs up the existing file to `.bak` first. +Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf) backs up the existing file to `.bak` first. ## Command reference diff --git a/README.md b/README.md index 1e20a1d..79f8aea 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ npm install -g @testsprite/testsprite-cli testsprite setup ``` -`testsprite setup` prompts for your [API key](https://www.testsprite.com), verifies it, and installs the verification-loop skill for your coding agent (`claude`, `cursor`, `cline`, `antigravity`, `codex`, etc.) — one command, so your agent is wired to verify its own work. Non-interactive (CI / onboarding scripts): +`testsprite setup` prompts for your [API key](https://www.testsprite.com), verifies it, and installs the verification-loop skill for your coding agent (`claude`, `cursor`, `cline`, `windsurf`, `antigravity`, `codex`, etc.) — one command, so your agent is wired to verify its own work. Non-interactive (CI / onboarding scripts): ```bash TESTSPRITE_API_KEY=sk-... testsprite setup --from-env --yes --agent claude @@ -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 ` runs all tests in a project in wave order | -| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project ` 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` | +| 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 ` runs all tests in a project in wave order | +| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project ` 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` | > 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). diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index f8ed59f..7da57cf 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -769,12 +769,13 @@ describe('runList', () => { const json = JSON.parse(capture.stdout.join('\n')) as ListResult[]; expect(Array.isArray(json)).toBe(true); - // 6 targets × 2 default skills = 12 rows - expect(json).toHaveLength(12); + // 7 targets × 2 default skills = 14 rows + expect(json).toHaveLength(14); 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('antigravity'); expect(targets).toContain('kiro'); expect(targets).toContain('codex'); @@ -916,11 +917,11 @@ describe('createAgentCommand wiring', () => { }); // --------------------------------------------------------------------------- -// All five own-file targets installed at once +// All own-file targets installed at once // --------------------------------------------------------------------------- -describe('runInstall — all five own-file targets', () => { - it('installs all five own-file targets in one invocation', async () => { +describe('runInstall — all own-file targets', () => { + it('installs every own-file target in one invocation', async () => { const { store, fs: agentFs } = makeMemFs(); const { capture, deps } = makeCapture(); @@ -930,7 +931,7 @@ describe('runInstall — all five own-file targets', () => { output: 'text', debug: false, dryRun: false, - target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro'], + target: [...OWN_FILE_TARGETS], skills: ['testsprite-verify'], force: false, }, @@ -948,11 +949,11 @@ describe('runInstall — all five own-file targets', () => { }); // --------------------------------------------------------------------------- -// Dry-run for all five own-file targets +// Dry-run for all six own-file targets // --------------------------------------------------------------------------- describe('runInstall — dry-run all own-file targets', () => { - it('writes nothing for any of the five own-file targets (default 2 skills = 10 would-write lines)', async () => { + it('writes nothing for any of the six own-file targets (default 2 skills = 12 would-write lines)', async () => { const { store, fs: agentFs } = makeMemFs(); const { capture, deps } = makeCapture(); @@ -962,7 +963,7 @@ describe('runInstall — dry-run all own-file targets', () => { output: 'text', debug: false, dryRun: true, - target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro'], + target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf'], force: false, }, { cwd: CWD, fs: agentFs, ...deps }, @@ -972,9 +973,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'); - // 5 targets × 2 default skills = 10 would-write lines + // 6 targets × 2 default skills = 12 would-write lines const wouldWriteLines = stderrOut.split('\n').filter(l => l.includes('would write')); - expect(wouldWriteLines.length).toBe(10); + expect(wouldWriteLines.length).toBe(12); }); }); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 1b4e878..0e80a91 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -15,6 +15,7 @@ import { pathFor, loadSkillBodyFor, bodyHash12, + compactBodyFor, buildCodexAggregate, buildSkillMarker, parseSkillMarker, @@ -443,6 +444,21 @@ export async function runInstall(opts: InstallOptions, deps: AgentDeps = {}): Pr } return b; }; + // Budget-capped own-file targets (e.g. windsurf) render the compact per-skill + // body so the rule file isn't truncated by the agent. Cached separately; must + // match renderForTarget's default selection so written bytes equal the asserted + // render. + const compactBodyCache = new Map(); + const compactBodyForSkill = (skill: string): string => { + let b = compactBodyCache.get(skill); + if (b === undefined) { + b = compactBodyFor(skill); + compactBodyCache.set(skill, b); + } + return b; + }; + const ownFileBodyFor = (t: AgentTarget, skill: string): string => + TARGETS[t].compactBody ? compactBodyForSkill(skill) : bodyForSkill(skill); let codexSectionCache: string | undefined; const getCodexSection = (): string => { if (codexSectionCache === undefined) { @@ -651,7 +667,7 @@ export async function runInstall(opts: InstallOptions, deps: AgentDeps = {}): Pr if (abs !== root && !abs.startsWith(root + path.sep)) { throw new CLIError(`refusing to write outside --dir: ${relPath}`, 5); } - const content = renderForTarget(t, skill, bodyForSkill(skill)).content; + const content = renderForTarget(t, skill, ownFileBodyFor(t, skill)).content; if (opts.dryRun) { // Apply the SAME symlink fail-close guard as the real install path @@ -1045,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, Antigravity, Codex)', + 'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Antigravity, Codex)', ); agent @@ -1055,7 +1071,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command { ) .option( '--target ', - 'Agent target(s): claude, cursor, cline, antigravity, kiro, codex (comma-separated or repeated)', + 'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, codex (comma-separated or repeated)', collect, [], ) diff --git a/src/lib/agent-targets.test.ts b/src/lib/agent-targets.test.ts index 38c74e2..0417166 100644 --- a/src/lib/agent-targets.test.ts +++ b/src/lib/agent-targets.test.ts @@ -80,18 +80,19 @@ testsprite test artifact get --out ./out/ // --------------------------------------------------------------------------- describe('TARGETS', () => { - it('has all six required keys', () => { + it('has all seven required keys', () => { const keys = Object.keys(TARGETS).sort(); - expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kiro']); + expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kiro', 'windsurf']); }); it('claude is GA', () => { expect(TARGETS.claude.status).toBe('ga'); }); - it('cursor, cline, antigravity, kiro, and codex are experimental', () => { + it('cursor, cline, windsurf, 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.antigravity.status).toBe('experimental'); expect(TARGETS.kiro.status).toBe('experimental'); expect(TARGETS.codex.status).toBe('experimental'); @@ -110,6 +111,7 @@ describe('TARGETS', () => { expect(TARGETS.cursor.mode).toBe('own-file'); expect(TARGETS.cline.mode).toBe('own-file'); expect(TARGETS.kiro.mode).toBe('own-file'); + expect(TARGETS.windsurf.mode).toBe('own-file'); }); it('codex target has mode managed-section', () => { @@ -293,6 +295,52 @@ describe('renderForTarget("cline")', () => { }); }); +describe('renderForTarget("windsurf")', () => { + const result = renderForTarget('windsurf', 'testsprite-verify', STUB_BODY); + + it('returns the .windsurf/rules path', () => { + expect(result.path).toBe('.windsurf/rules/testsprite-verify.md'); + }); + + it('uses the Cascade frontmatter (trigger: model_decision + description)', () => { + expect(result.content.startsWith('---\n')).toBe(true); + expect(result.content).toContain('trigger: model_decision'); + expect(result.content).toContain(`description: ${SKILL_DESCRIPTION}`); + }); + + it('does NOT carry the Claude/Cursor 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 + }); +}); + +describe('windsurf renders within the rules-file budget', () => { + // Regression: a `.windsurf/rules/*.md` file caps at ~12 K characters and + // Cascade silently truncates beyond that. The full verify body (~22 KB) would + // be cut in half, so windsurf renders the COMPACT body for verify (its trimmed + // codex asset) and the full body for onboard (which already fits). Uses the + // REAL bodies (no stub) so the size reflects what a user receives. + for (const skill of DEFAULT_SKILLS) { + it(`${skill} fits under 12 000 characters`, () => { + const r = renderForTarget('windsurf', skill); + expect(r.content.length).toBeLessThan(12_000); + }); + } + + it('verify uses the compact body (smaller than the full claude render)', () => { + const windsurf = renderForTarget('windsurf', 'testsprite-verify'); + const claude = renderForTarget('claude', 'testsprite-verify'); + expect(windsurf.content.length).toBeLessThan(claude.content.length); + // The full-body-only intro line is absent from the compact body... + expect(claude.content).toContain('The verification loop that flies'); + expect(windsurf.content).not.toContain('The verification loop that flies'); + // ...but the load-bearing command survives. + expect(windsurf.content).toContain('testsprite test run'); + }); +}); + // --------------------------------------------------------------------------- // Content integrity — load-bearing command strings must survive any body trim // --------------------------------------------------------------------------- diff --git a/src/lib/agent-targets.ts b/src/lib/agent-targets.ts index 0273a04..d9a85d5 100644 --- a/src/lib/agent-targets.ts +++ b/src/lib/agent-targets.ts @@ -2,7 +2,14 @@ import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { VERSION } from '../version.js'; -export type AgentTarget = 'claude' | 'cursor' | 'cline' | 'antigravity' | 'codex' | 'kiro'; +export type AgentTarget = + | 'claude' + | 'cursor' + | 'cline' + | 'antigravity' + | 'codex' + | 'kiro' + | 'windsurf'; export interface TargetSpec { status: 'ga' | 'experimental'; @@ -14,11 +21,19 @@ export interface TargetSpec { */ path: string; /** - * 'own-file': the CLI owns the whole file (claude/cursor/cline/antigravity). + * 'own-file': the CLI owns the whole file (claude/cursor/cline/antigravity/windsurf). * 'managed-section': the CLI writes only a sentinel-delimited section inside * a potentially user-authored file (codex target, AGENTS.md). */ mode: 'own-file' | 'managed-section'; + /** + * When true, render the budget-friendly body (see {@link compactBodyFor}) + * instead of the full own-file skill body. Used for own-file targets whose + * rule files are size-capped — currently `windsurf` (`.windsurf/rules/*.md` + * files cap at ~12 K characters and Cascade silently truncates beyond that, + * which would cut the full ~22 KB verify skill in half). + */ + compactBody?: boolean; /** * Wrap a skill body in this target's frontmatter/header. Takes the skill's * `name`+`description` (own-file targets emit them as frontmatter) and the body. @@ -122,6 +137,18 @@ function wrapMdc(_name: string, description: string, body: string): string { return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}\n`; } +/** + * Windsurf (Cascade) reads workspace rules from `.windsurf/rules/*.md` with YAML + * frontmatter. `trigger: model_decision` is the Cascade equivalent of the Cursor + * `.mdc` `alwaysApply: false` mode: only the `description` is surfaced up front, + * and Cascade pulls in the full rule body when the description shows it is + * relevant — exactly the on-demand activation these skills want. (The other + * triggers are `always_on`, `manual`, and `glob`.) + */ +function wrapWindsurf(_name: string, description: string, body: string): string { + return `---\ntrigger: model_decision\ndescription: ${description}\n---\n\n${body}\n`; +} + // --------------------------------------------------------------------------- // Landing paths // --------------------------------------------------------------------------- @@ -144,6 +171,8 @@ export function pathFor(target: AgentTarget, skill: string): string { return `.clinerules/${skill}.md`; case 'kiro': return `.kiro/skills/${skill}/SKILL.md`; + case 'windsurf': + return `.windsurf/rules/${skill}.md`; case 'codex': return 'AGENTS.md'; } @@ -182,6 +211,15 @@ export const TARGETS: Record = { // claude/antigravity, so it shares the wrapSkill wrapper. wrap: wrapSkill, }, + windsurf: { + status: 'experimental', + path: pathFor('windsurf', SKILL_NAME), + mode: 'own-file', + // Windsurf rules files are budget-capped (~12 K chars per `.windsurf/rules/*.md`), + // so render the compact body per skill (see compactBodyFor). + compactBody: true, + wrap: wrapWindsurf, + }, /** * codex target — managed-section mode. * @@ -318,6 +356,24 @@ export function loadSkillBodyFor(skill: string, read: ReadFn = defaultRead): str return readSkillAsset(spec.bodyFile, read); } +/** + * Budget-friendly body for an own-file target whose rule files are size-capped + * (e.g. windsurf). For a skill that ships a trimmed codex asset (`codex.kind === + * 'full'`, e.g. `testsprite-verify` — full body ~22 KB, codex ~5 KB) we render + * that compact asset so the wrapped file stays under the cap. For skills whose + * codex contribution is only a one-liner (`'line'`/`'none'`, e.g. + * `testsprite-onboard`), the one-liner is useless as a standalone rule and the + * full own-file body (~6.5 KB) already fits the budget — so the full body is + * used. + */ +export function compactBodyFor(skill: string, read: ReadFn = defaultRead): string { + const spec = SKILLS[skill]; + if (!spec) throw new Error(`unknown skill: ${skill}`); + return spec.codex.kind === 'full' + ? readSkillAsset(spec.codex.file, read) + : loadSkillBodyFor(skill, read); +} + /** * Resolve a skill's codex (AGENTS.md) contribution as a Markdown string. * 'full' → read the `*.codex.md` asset; 'line' → the inline one-liner; 'none' → ''. @@ -441,7 +497,8 @@ export function renderForTarget( const resolvedBody = body !== undefined ? body : codexContentFor(skill); return { path, content: spec.wrap(skillSpec.name, skillSpec.description, resolvedBody) }; } - const resolvedBody = body !== undefined ? body : loadSkillBodyFor(skill); + const resolvedBody = + body !== undefined ? body : spec.compactBody ? compactBodyFor(skill) : loadSkillBodyFor(skill); return { path, content: renderOwnFileWithMarker(t, skill, buildSkillMarker(skill, resolvedBody), resolvedBody), diff --git a/test/__snapshots__/help.snapshot.test.ts.snap b/test/__snapshots__/help.snapshot.test.ts.snap index 0c387fa..d89e7dc 100644 --- a/test/__snapshots__/help.snapshot.test.ts.snap +++ b/test/__snapshots__/help.snapshot.test.ts.snap @@ -4,7 +4,7 @@ exports[`--help snapshots > agent 1`] = ` "Usage: testsprite agent [options] [command] Install TestSprite guidance into coding-agent config (Claude Code, Cursor, -Cline, Antigravity, Codex) +Cline, Windsurf, Antigravity, Codex) Options: -h, --help display help for command @@ -29,7 +29,7 @@ into a project for a coding agent Options: --target Agent target(s): claude, cursor, cline, antigravity, kiro, - codex (comma-separated or repeated) (default: []) + windsurf, codex (comma-separated or repeated) (default: []) --skill Skill(s) to install: testsprite-verify, testsprite-onboard (comma-separated or repeated; default: all) (default: []) --dir Project root to write into (default: cwd) @@ -115,8 +115,8 @@ Options: --from-env Read TESTSPRITE_API_KEY from the environment instead of prompting (default: false) --agent Coding-agent target to install: claude, antigravity, - cursor, cline, kiro, codex (default: claude) (default: - "claude") + cursor, cline, kiro, windsurf, codex (default: claude) + (default: "claude") --no-agent Skip the agent skill install (configure credentials only) --force Overwrite an existing skill file (a .bak backup is kept) --dir Project root for the skill install (default: current @@ -604,8 +604,8 @@ Commands: project Manage TestSprite projects test Inspect TestSprite tests agent Install TestSprite guidance into coding-agent - config (Claude Code, Cursor, Cline, Antigravity, - Codex) + config (Claude Code, Cursor, Cline, Windsurf, + Antigravity, Codex) usage|credits Show credit balance and plan/entitlement info (proactive pre-flight before a large test run) help [command] display help for command diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index 0513b8b..094aec2 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -169,11 +169,20 @@ describe('content integrity', () => { content.trimStart().startsWith('#'), `cline: should start with a markdown heading`, ).toBe(true); + } else if (target === 'windsurf') { + // Windsurf Cascade frontmatter: trigger + description (no name/alwaysApply) + expect(content.startsWith('---'), `windsurf: should start with ---`).toBe(true); + expect(content).toContain('trigger: model_decision'); + expect(content).toContain('description:'); } - // (b) branding — the renamed H1 must be present + // (b) branding — the renamed H1 must be present in every body variant expect(content).toContain('TestSprite Verification Loop'); - expect(content).toContain('The verification loop that flies'); + // The full-body intro line lives only in the FULL body; compact-body targets + // (e.g. windsurf, budget-capped) ship the trimmed verify body and omit it. + if (!TARGETS[target].compactBody) { + expect(content).toContain('The verification loop that flies'); + } // (c) Load-bearing command strings expect(content, `${target}: missing 'testsprite test run'`).toContain('testsprite test run'); @@ -198,6 +207,10 @@ describe('content integrity', () => { expect(content).toContain('alwaysApply: false'); } else if (target === 'cline') { expect(content.startsWith('---'), `cline/onboard: must NOT start with ---`).toBe(false); + } else if (target === 'windsurf') { + expect(content.startsWith('---'), `windsurf/onboard: should start with ---`).toBe(true); + expect(content).toContain('trigger: model_decision'); + expect(content).toContain('description:'); } // Load-bearing onboard string: the skill body must reference setup @@ -790,7 +803,7 @@ describe('agent list', () => { }>; expect(Array.isArray(parsed)).toBe(true); - // Expected: 5 targets × 2 skills = 10 rows + // Expected: 7 targets × 2 skills = 14 rows const expectedCount = Object.keys(TARGETS).length * DEFAULT_SKILLS.length; expect(parsed.length).toBe(expectedCount); @@ -825,6 +838,7 @@ describe('matrix coverage guard', () => { 'cursor', 'cline', 'kiro', + 'windsurf', 'codex', ]); }); diff --git a/test/e2e/setup.e2e.test.ts b/test/e2e/setup.e2e.test.ts index a30c3fb..3b35751 100644 --- a/test/e2e/setup.e2e.test.ts +++ b/test/e2e/setup.e2e.test.ts @@ -228,6 +228,7 @@ describe('matrix coverage guard', () => { 'cursor', 'cline', 'kiro', + 'windsurf', 'codex', ]); });