Skip to content
Open
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
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
51 changes: 48 additions & 3 deletions src/lib/agent-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { VERSION } from '../version.js';
import {
DEFAULT_SKILLS,
MANAGED_SECTION_BEGIN,
MANAGED_SECTION_BEGIN_LEGACY,
MANAGED_SECTION_END,
ONBOARD_CODEX_LINE,
SKILL_DESCRIPTION,
Expand Down Expand Up @@ -82,7 +83,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 +92,7 @@ describe('TARGETS', () => {
'codex',
'copilot',
'cursor',
'gemini',
'kiro',
'windsurf',
]);
Expand All @@ -100,11 +102,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 +130,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 +390,47 @@ 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 intentionally returns the unwrapped
// body without sentinels — sentinel wrapping is an install-time concern
// performed by buildSection in agent.ts, not by renderForTarget.
const gemini = renderForTarget('gemini', 'testsprite-verify');
expect(gemini.content).toContain('testsprite test run');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('gemini managed-section body is wrapped in sentinels when installed (sentinel contract)', () => {
// Verify the sentinel contract at the layer where it is enforced:
// the install command wraps the body from renderForTarget in MANAGED_SECTION_BEGIN/END.
// We test this indirectly via the exported constants to confirm the sentinel
// strings are defined, well-formed HTML comments, and target-neutral.
expect(MANAGED_SECTION_BEGIN).toContain('BEGIN TESTSPRITE AGENT SECTION');
expect(MANAGED_SECTION_END).toContain('END TESTSPRITE AGENT SECTION');
// Sentinel must be target-neutral — must NOT say 'codex'
expect(MANAGED_SECTION_BEGIN).not.toContain('codex');
// Legacy sentinel exists for backward compat with existing AGENTS.md files
expect(MANAGED_SECTION_BEGIN_LEGACY).toContain('codex');
});
});

// ---------------------------------------------------------------------------
// 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