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
4 changes: 2 additions & 2 deletions src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,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 +1072,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 on lines 1073 to +1075

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document Gemini in the managed-section --force behavior.

The target help includes gemini, but the existing --force description still says only codex is managed-section. Update it to mention both targets or all managed-section targets, while retaining the no-clobber guarantee.

As per path instructions, managed-section targets’ user content outside the sentinels must remain explicitly protected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/agent.ts` around lines 1073 - 1075, Update the --force option
description in the agent command to document that both Gemini and Codex use
managed sections, while explicitly preserving the guarantee that user content
outside the managed-section sentinels is never overwritten.

Source: Path instructions

collect,
[],
)
Expand Down
34 changes: 31 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,31 @@ 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).
const gemini = renderForTarget('gemini', 'testsprite-verify');
expect(gemini.content).toContain('testsprite test run');
});
});

// ---------------------------------------------------------------------------
// Content integrity — load-bearing command strings must survive any body trim
// ---------------------------------------------------------------------------
Expand Down
30 changes: 28 additions & 2 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 on lines +254 to +276

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a target-neutral managed-section sentinel.

Gemini uses the shared managed-section writer, but MANAGED_SECTION_BEGIN currently says testsprite agent install codex. A generated GEMINI.md will therefore label its managed content as Codex, which is misleading for users and future cleanup tooling. Rename the shared marker to be target-neutral while preserving compatibility with existing Codex files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/agent-targets.ts` around lines 254 - 276, Update the shared
managed-section sentinel used by the managed-section writer to a target-neutral
label instead of “testsprite agent install codex,” while preserving recognition
of the existing Codex marker for backward compatibility. Locate the sentinel
constants and managed-section parsing/writing logic, and ensure new GEMINI.md
and Codex files use the neutral marker without breaking cleanup or updates of
legacy Codex files.

/**
* codex target — managed-section mode.
*
Expand Down
12 changes: 6 additions & 6 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,8 +29,8 @@ 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)
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 kept)
--dir <path> Project root for the skill install (default: current
Expand Down Expand Up @@ -680,7 +680,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
44 changes: 41 additions & 3 deletions test/e2e/agent-install.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,12 @@ describe('content integrity', () => {
expect(content.startsWith('---'), `copilot: should start with ---`).toBe(true);
expect(content).toContain("applyTo: '**'");
expect(content).toContain('description:');
} else if (target === 'gemini') {
// GEMINI.md is plain Markdown -- no frontmatter expected.
expect(content.startsWith('---'), `gemini: must NOT start with ---`).toBe(false);
expect(content).not.toContain('applyTo:');
expect(content).not.toContain('trigger:');
}

// (b) branding — the renamed H1 must be present in every body variant
expect(content).toContain('TestSprite Verification Loop');
// The full-body intro line lives only in the FULL body; compact-body targets
Expand Down Expand Up @@ -219,6 +223,9 @@ describe('content integrity', () => {
} else if (target === 'copilot') {
expect(content.startsWith('---'), `copilot/onboard: should start with ---`).toBe(true);
expect(content).toContain("applyTo: '**'");
} else if (target === 'gemini') {
// GEMINI.md is plain Markdown -- no frontmatter.
expect(content.startsWith('---'), `gemini/onboard: must NOT start with ---`).toBe(false);
}

// Load-bearing onboard string: the skill body must reference setup
Expand Down Expand Up @@ -264,8 +271,38 @@ describe('content integrity', () => {
// (d) No frontmatter fence — AGENTS.md is plain prose
expect(content.startsWith('---'), 'codex: must NOT start with ---').toBe(false);
});
});

it('gemini GEMINI.md contains ONE managed section with verify and onboard content', () => {
const tmpDir = freshTmpDir();
runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']);

const filePath = join(tmpDir, TARGETS.gemini.path);
const content = readFileSync(filePath, 'utf8');
Comment on lines +275 to +280

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assert the CLI exit code and JSON output.

The runCli result is ignored, so a failed install that leaves a readable file could still let this test pass. Assert a zero exit status, parse stdout as JSON, and verify diagnostics remain on stderr.

As per path instructions, tests must cover exit-code paths and preserve machine-readable JSON output discipline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/agent-install.e2e.test.ts` around lines 275 - 280, The test `gemini
GEMINI.md contains ONE managed section with verify and onboard content` ignores
the CLI result. Capture the `runCli` return value, assert a zero exit status,
parse `stdout` as JSON and validate the expected response, and assert
diagnostics are emitted only on `stderr` before reading the generated file.

Source: Path instructions


// (a) Exactly ONE pair of sentinels
const escRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const beginCount = (content.match(new RegExp(escRe(MANAGED_SECTION_BEGIN), 'g')) ?? []).length;
const endCount = (content.match(new RegExp(escRe(MANAGED_SECTION_END), 'g')) ?? []).length;
expect(beginCount, 'exactly one BEGIN sentinel').toBe(1);
expect(endCount, 'exactly one END sentinel').toBe(1);

// BEGIN must come before END
expect(content.indexOf(MANAGED_SECTION_BEGIN)).toBeLessThan(
content.indexOf(MANAGED_SECTION_END),
);

// (b) Load-bearing command strings from the compact verify body
expect(content).toContain('testsprite test run');
expect(content).toContain('--wait');
expect(content).toContain('test artifact get');

// (c) Onboard one-liner must be present
expect(content).toContain('First-time setup');

// (d) No frontmatter fence -- GEMINI.md is plain prose
expect(content.startsWith('---'), 'gemini: must NOT start with ---').toBe(false);
});
Comment on lines +275 to +304

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cover managed-section preservation and reruns.

This test starts from an empty GEMINI.md and verifies only a single install. Add coverage that pre-populates user content outside the sentinels, installs both skills, then reruns installation and asserts that user content remains intact, exactly one section exists, and only the managed section is replaced.

As per path instructions, tests must cover the new behavior deterministically and offline.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 283-283: Do not use variable for regular expressions
Context: new RegExp(escRe(MANAGED_SECTION_BEGIN), 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity. Security best practice.

(regexp-non-literal-typescript)


[warning] 284-284: Do not use variable for regular expressions
Context: new RegExp(escRe(MANAGED_SECTION_END), 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity. Security best practice.

(regexp-non-literal-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/agent-install.e2e.test.ts` around lines 275 - 304, Add deterministic
offline coverage around the existing Gemini installation test by pre-populating
GEMINI.md with user content before installing both skills, then rerun
installation and assert the user content remains unchanged, exactly one
MANAGED_SECTION_BEGIN/END pair exists, and only the content between those
sentinels is replaced. Reuse the existing runCli, freshTmpDir, TARGETS, and
sentinel constants, and compare the preserved prefix/suffix or expected managed
section across runs.

Source: Path instructions

});
// ---------------------------------------------------------------------------
// 3. Idempotent re-run
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -811,7 +848,7 @@ describe('agent list', () => {
}>;
expect(Array.isArray(parsed)).toBe(true);

// Expected: 8 targets × 2 skills = 16 rows
// Expected: targets x 2 skills per target = total rows (computed dynamically)
const expectedCount = Object.keys(TARGETS).length * DEFAULT_SKILLS.length;
expect(parsed.length).toBe(expectedCount);

Expand Down Expand Up @@ -848,6 +885,7 @@ describe('matrix coverage guard', () => {
'kiro',
'windsurf',
'copilot',
'gemini',
'codex',
]);
});
Expand Down
1 change: 1 addition & 0 deletions test/e2e/setup.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ describe('matrix coverage guard', () => {
'kiro',
'windsurf',
'copilot',
'gemini',
'codex',
]);
});
Expand Down
Loading