Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions src/commands/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,8 +779,8 @@ describe('runList', () => {

const json = JSON.parse(capture.stdout.join('\n')) as ListResult[];
expect(Array.isArray(json)).toBe(true);
// 8 targets × 2 default skills = 16 rows
expect(json).toHaveLength(16);
// 9 targets x 2 default skills = 18 rows
expect(json).toHaveLength(18);
const targets = json.map(r => r.target);
expect(targets).toContain('claude');
expect(targets).toContain('cursor');
Expand Down
10 changes: 6 additions & 4 deletions src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
renderForTarget,
renderOwnFileWithMarker,
MANAGED_SECTION_BEGIN,
MANAGED_SECTION_BEGIN_LEGACY,
MANAGED_SECTION_END,
} from '../lib/agent-targets.js';

Expand Down Expand Up @@ -206,7 +207,8 @@ function classifySection(existing: string, section: string): SectionState {

for (let i = 0; i < lines.length; i++) {
const stripped = (lines[i] ?? '').trimEnd();
if (stripped === MANAGED_SECTION_BEGIN) beginLines.push(i);
if (stripped === MANAGED_SECTION_BEGIN || stripped === MANAGED_SECTION_BEGIN_LEGACY)
beginLines.push(i);
else if (stripped === MANAGED_SECTION_END) endLines.push(i);
}

Expand Down Expand Up @@ -1062,7 +1064,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, Kiro, Windsurf, Copilot, Codex)',
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Antigravity, Kiro, Windsurf, Copilot, Gemini, Codex)',
);

agent
Expand All @@ -1072,7 +1074,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command {
)
.option(
'--target <t>',
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, copilot, codex (comma-separated or repeated)',
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, copilot, gemini, codex (comma-separated or repeated)',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
collect,
[],
)
Expand All @@ -1086,7 +1088,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command {
.option(
'--force',
'For own-file targets: overwrite existing file (a .bak backup is kept). ' +
'For codex (managed-section): replaces the section unconditionally; user content outside the section is never destroyed.',
'For managed-section targets (codex, gemini): replaces the section unconditionally; user content outside the section is never destroyed.',
)
.addHelpText('after', GLOBAL_OPTS_HINT)
.action(
Expand Down
39 changes: 36 additions & 3 deletions src/lib/agent-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ testsprite test artifact get <run-id> --out ./out/
// ---------------------------------------------------------------------------

describe('TARGETS', () => {
it('has all eight required keys', () => {
it('has all nine required keys', () => {
const keys = Object.keys(TARGETS).sort();
expect(keys).toEqual([
'antigravity',
Expand All @@ -91,6 +91,7 @@ describe('TARGETS', () => {
'codex',
'copilot',
'cursor',
'gemini',
'kiro',
'windsurf',
]);
Expand All @@ -100,11 +101,12 @@ describe('TARGETS', () => {
expect(TARGETS.claude.status).toBe('ga');
});

it('cursor, cline, windsurf, copilot, antigravity, kiro, and codex are experimental', () => {
it('cursor, cline, windsurf, copilot, gemini, 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.gemini.status).toBe('experimental');
expect(TARGETS.antigravity.status).toBe('experimental');
expect(TARGETS.kiro.status).toBe('experimental');
expect(TARGETS.codex.status).toBe('experimental');
Expand All @@ -127,8 +129,9 @@ describe('TARGETS', () => {
expect(TARGETS.copilot.mode).toBe('own-file');
});

it('codex target has mode managed-section', () => {
it('codex and gemini targets have mode managed-section', () => {
expect(TARGETS.codex.mode).toBe('managed-section');
expect(TARGETS.gemini.mode).toBe('managed-section');
});

it('codex target path is AGENTS.md', () => {
Expand Down Expand Up @@ -386,6 +389,36 @@ describe('renderForTarget("copilot")', () => {
});
});

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

it('returns GEMINI.md as the landing path', () => {
expect(result.path).toBe('GEMINI.md');
});

it('does not emit YAML frontmatter (plain Markdown, no --- fence)', () => {
expect(result.content.startsWith('---\n')).toBe(false);
expect(result.content).not.toContain('name:');
expect(result.content).not.toContain('applyTo:');
expect(result.content).not.toContain('trigger:');
});

it('contains the stub body verbatim', () => {
expect(result.content).toContain(STUB_BODY);
});

it('renders the verify body content (managed-section, compact codex body)', () => {
// Uses real bodies: gemini uses the codex contribution body (compact).
// renderForTarget for managed-section returns the unwrapped body (no
// sentinels) — sentinel wrapping happens at install time via buildSection.
const gemini = renderForTarget('gemini', 'testsprite-verify');
expect(gemini.content).toContain('testsprite test run');
// Body must NOT be wrapped in sentinels at this layer
expect(gemini.content).not.toContain(MANAGED_SECTION_BEGIN);
expect(gemini.content).not.toContain(MANAGED_SECTION_END);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// ---------------------------------------------------------------------------
// Content integrity — load-bearing command strings must survive any body trim
// ---------------------------------------------------------------------------
Expand Down
43 changes: 39 additions & 4 deletions src/lib/agent-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export type AgentTarget =
| 'codex'
| 'kiro'
| 'windsurf'
| 'copilot';
| 'copilot'
| 'gemini';

export interface TargetSpec {
status: 'ga' | 'experimental';
Expand Down Expand Up @@ -189,6 +190,8 @@ export function pathFor(target: AgentTarget, skill: string): string {
return `.windsurf/rules/${skill}.md`;
case 'copilot':
return `.github/instructions/${skill}.instructions.md`;
case 'gemini':
return 'GEMINI.md';
case 'codex':
return 'AGENTS.md';
}
Expand Down Expand Up @@ -243,11 +246,34 @@ export const TARGETS: Record<AgentTarget, TargetSpec> = {
// 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
// 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,
},
/**
* gemini target -- managed-section mode (GEMINI.md).
*
* Gemini CLI reads project-level instructions from a `GEMINI.md` file in
* the repo root. The file is plain Markdown, loaded at the start of every
* session (always-on). Because all installed skills share this single file
* we use managed-section mode -- the same approach as codex/AGENTS.md --
* writing only a sentinel-delimited section so any existing user content
* in GEMINI.md is never clobbered.
*
* The compact body is used (same reasoning as windsurf/copilot): GEMINI.md
* is always-injected, so keeping it small reduces context cost.
*
* --force replaces the managed section unconditionally but never touches
* content outside the sentinels.
*/
gemini: {
status: 'experimental',
path: pathFor('gemini', SKILL_NAME),
mode: 'managed-section',
// GEMINI.md is plain Markdown; wrap is a no-op (no frontmatter).
wrap: (_name, _description, body) => body,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* codex target — managed-section mode.
*
Expand All @@ -274,11 +300,20 @@ export const TARGETS: Record<AgentTarget, TargetSpec> = {
},
};

/** Sentinel pair that bounds our managed section in AGENTS.md. */
/** Sentinel pair that bounds our managed section in AGENTS.md / GEMINI.md. */
export const MANAGED_SECTION_BEGIN =
'<!-- BEGIN TESTSPRITE AGENT SECTION (testsprite agent install codex) -->';
'<!-- BEGIN TESTSPRITE AGENT SECTION (testsprite agent install) -->';
export const MANAGED_SECTION_END = '<!-- END TESTSPRITE AGENT SECTION -->';

/**
* Legacy sentinel written by versions prior to this change.
* Kept for backward-compatibility: existing AGENTS.md files that contain
* the old Codex-labelled marker are still recognised as a valid managed
* section during classify / replace operations.
*/
export const MANAGED_SECTION_BEGIN_LEGACY =
'<!-- BEGIN TESTSPRITE AGENT SECTION (testsprite agent install codex) -->';

// ---------------------------------------------------------------------------
// Install marker (stale-skill detection, issue #123)
// ---------------------------------------------------------------------------
Expand Down
18 changes: 9 additions & 9 deletions test/__snapshots__/help.snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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, Kiro, Windsurf, Copilot, Codex)
Cline, Antigravity, Kiro, Windsurf, Copilot, Gemini, Codex)

Options:
-h, --help display help for command
Expand All @@ -29,15 +29,15 @@ into a project for a coding agent

Options:
--target <t> Agent target(s): claude, cursor, cline, antigravity, kiro,
windsurf, copilot, codex (comma-separated or repeated)
(default: [])
windsurf, copilot, gemini, codex (comma-separated or
repeated) (default: [])
--skill <name> Skill(s) to install: testsprite-verify, testsprite-onboard
(comma-separated or repeated; default: all) (default: [])
--dir <path> Project root to write into (default: cwd)
--force For own-file targets: overwrite existing file (a .bak backup
is kept). For codex (managed-section): replaces the section
unconditionally; user content outside the section is never
destroyed.
is kept). For managed-section targets (codex, gemini):
replaces the section unconditionally; user content outside
the section is never destroyed.
-h, --help display help for command

Global options (--dry-run, --output, --profile, --endpoint-url, --request-timeout, --verbose, --debug):
Expand Down Expand Up @@ -116,8 +116,8 @@ Options:
--from-env Read TESTSPRITE_API_KEY from the environment instead of
prompting (default: false)
--agent <target> Coding-agent target to install: claude, antigravity,
cursor, cline, kiro, windsurf, copilot, codex (default:
claude) (default: "claude")
cursor, cline, kiro, windsurf, copilot, gemini, 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
Expand Down Expand Up @@ -764,7 +764,7 @@ Commands:
test Inspect TestSprite tests
agent Install TestSprite guidance into coding-agent
config (Claude Code, Cursor, Cline, Antigravity,
Kiro, Windsurf, Copilot, Codex)
Kiro, Windsurf, Copilot, Gemini, Codex)
usage|credits Show credit balance and plan/entitlement info
(proactive pre-flight before a large test run)
doctor Diagnose CLI setup: version, Node, profile,
Expand Down
Loading
Loading