From 3df4a37677d6bba386a665b94b298b19fe6c747f Mon Sep 17 00:00:00 2001 From: Tasfia-17 Date: Sat, 11 Jul 2026 03:20:36 +0600 Subject: [PATCH 1/4] feat(agent): add Gemini CLI as an install target Adds `gemini` to the supported agent-install targets. Gemini CLI reads project-level context from a GEMINI.md file in the repo root, which is loaded at the start of every session (always-on, no on-demand mode). Because all installed skills share a single GEMINI.md file in the project root, the target uses managed-section mode -- the same approach as the codex/AGENTS.md target -- writing only a sentinel-delimited section so any existing user content in GEMINI.md is never clobbered. The landing path is GEMINI.md (repo root). Both testsprite-verify and testsprite-onboard are aggregated into one managed section, consistent with how codex aggregates skills into AGENTS.md. Slots into the existing TARGETS machinery: agent list, setup --agent, and skill-nudge install-detection all pick it up automatically. Updates the AgentTarget union, pathFor, TARGETS, agent command description, help text, unit tests, e2e matrix guards, and the help snapshot. Contributes to CONTRIBUTING.md accepted target: gemini --- src/commands/agent.test.ts | 4 +- src/commands/agent.ts | 6 +-- src/lib/agent-targets.test.ts | 34 ++++++++++++-- src/lib/agent-targets.ts | 30 ++++++++++++- test/__snapshots__/help.snapshot.test.ts.snap | 18 ++++---- test/e2e/agent-install.e2e.test.ts | 44 +++++++++++++++++-- test/e2e/setup.e2e.test.ts | 1 + 7 files changed, 115 insertions(+), 22 deletions(-) diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 7326aebd..276faae9 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -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'); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 7d7e7c85..24291290 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -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 @@ -1072,7 +1072,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command { ) .option( '--target ', - '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)', collect, [], ) @@ -1086,7 +1086,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( diff --git a/src/lib/agent-targets.test.ts b/src/lib/agent-targets.test.ts index 73feee3c..003ce8ab 100644 --- a/src/lib/agent-targets.test.ts +++ b/src/lib/agent-targets.test.ts @@ -82,7 +82,7 @@ testsprite test artifact get --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', @@ -91,6 +91,7 @@ describe('TARGETS', () => { 'codex', 'copilot', 'cursor', + 'gemini', 'kiro', 'windsurf', ]); @@ -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'); @@ -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', () => { @@ -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 // --------------------------------------------------------------------------- diff --git a/src/lib/agent-targets.ts b/src/lib/agent-targets.ts index 7e6f94da..e7eeae8e 100644 --- a/src/lib/agent-targets.ts +++ b/src/lib/agent-targets.ts @@ -10,7 +10,8 @@ export type AgentTarget = | 'codex' | 'kiro' | 'windsurf' - | 'copilot'; + | 'copilot' + | 'gemini'; export interface TargetSpec { status: 'ga' | 'experimental'; @@ -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'; } @@ -243,11 +246,34 @@ export const TARGETS: Record = { // 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, + }, /** * codex target — managed-section mode. * diff --git a/test/__snapshots__/help.snapshot.test.ts.snap b/test/__snapshots__/help.snapshot.test.ts.snap index 55a7252f..1e13152a 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, Kiro, Windsurf, Copilot, Codex) +Cline, Antigravity, Kiro, Windsurf, Copilot, Gemini, Codex) Options: -h, --help display help for command @@ -29,15 +29,15 @@ into a project for a coding agent Options: --target 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 Skill(s) to install: testsprite-verify, testsprite-onboard (comma-separated or repeated; default: all) (default: []) --dir 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): @@ -116,8 +116,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, 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 @@ -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, diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index 4fb45847..3f9ad3c4 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -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 @@ -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 @@ -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'); + + // (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); + }); +}); // --------------------------------------------------------------------------- // 3. Idempotent re-run // --------------------------------------------------------------------------- @@ -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); @@ -848,6 +885,7 @@ describe('matrix coverage guard', () => { 'kiro', 'windsurf', 'copilot', + 'gemini', 'codex', ]); }); diff --git a/test/e2e/setup.e2e.test.ts b/test/e2e/setup.e2e.test.ts index 5a074346..3ad17cb4 100644 --- a/test/e2e/setup.e2e.test.ts +++ b/test/e2e/setup.e2e.test.ts @@ -230,6 +230,7 @@ describe('matrix coverage guard', () => { 'kiro', 'windsurf', 'copilot', + 'gemini', 'codex', ]); }); From cdfb146adca4abfb2ca08d0fe7ef62e9321b0086 Mon Sep 17 00:00:00 2001 From: Tasfia-17 Date: Fri, 24 Jul 2026 05:26:39 +0600 Subject: [PATCH 2/4] fix(agent): address CodeRabbit comments on Gemini target - Make MANAGED_SECTION_BEGIN target-neutral ('testsprite agent install') instead of 'testsprite agent install codex'; add MANAGED_SECTION_BEGIN_LEGACY for backward-compat recognition of existing AGENTS.md files - classifySection now accepts both the new and legacy BEGIN sentinel - Add exit-code and JSON output assertions to Gemini e2e install test - Add managed-section user-content preservation test: pre-populates GEMINI.md, runs install twice, asserts prefix/suffix intact and section byte-identical across runs - Clarify renderForTarget unit test: sentinel wrapping is an install-time concern, not a renderForTarget concern --- src/commands/agent.ts | 4 +- src/lib/agent-targets.test.ts | 5 +++ src/lib/agent-targets.ts | 13 +++++- test/e2e/agent-install.e2e.test.ts | 69 +++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 24291290..0617fe4a 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -22,6 +22,7 @@ import { renderForTarget, renderOwnFileWithMarker, MANAGED_SECTION_BEGIN, + MANAGED_SECTION_BEGIN_LEGACY, MANAGED_SECTION_END, } from '../lib/agent-targets.js'; @@ -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); } diff --git a/src/lib/agent-targets.test.ts b/src/lib/agent-targets.test.ts index 003ce8ab..7b3edf5e 100644 --- a/src/lib/agent-targets.test.ts +++ b/src/lib/agent-targets.test.ts @@ -409,8 +409,13 @@ describe('renderForTarget("gemini")', () => { 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); }); }); diff --git a/src/lib/agent-targets.ts b/src/lib/agent-targets.ts index e7eeae8e..83bd19b8 100644 --- a/src/lib/agent-targets.ts +++ b/src/lib/agent-targets.ts @@ -300,11 +300,20 @@ export const TARGETS: Record = { }, }; -/** 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 = - ''; + ''; export const MANAGED_SECTION_END = ''; +/** + * 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 = + ''; + // --------------------------------------------------------------------------- // Install marker (stale-skill detection, issue #123) // --------------------------------------------------------------------------- diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index 3f9ad3c4..da34eb96 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -274,7 +274,23 @@ describe('content integrity', () => { 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 result = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + + // Exit code must be 0 + expect(result.status, 'gemini install: exit code').toBe(0); + + // stdout must be valid JSON with the expected entry + const parsed = JSON.parse(result.stdout) as Array<{ + target: string; + path: string; + action: string; + skills: string[]; + }>; + expect(Array.isArray(parsed), 'output should be a JSON array').toBe(true); + const entry = parsed.find(r => r.target === 'gemini'); + expect(entry, 'gemini entry in JSON output').toBeDefined(); + expect(entry!.action, 'gemini action').toBe('section-installed'); + expect(entry!.path).toBe(TARGETS.gemini.path); const filePath = join(tmpDir, TARGETS.gemini.path); const content = readFileSync(filePath, 'utf8'); @@ -302,6 +318,57 @@ describe('content integrity', () => { // (d) No frontmatter fence -- GEMINI.md is plain prose expect(content.startsWith('---'), 'gemini: must NOT start with ---').toBe(false); }); + + it('gemini managed-section preserves user content outside sentinels across re-runs', () => { + const tmpDir = freshTmpDir(); + const filePath = join(tmpDir, TARGETS.gemini.path); + + // Pre-populate GEMINI.md with user content above and below where the + // managed section will land. + const userPrefix = '# My Project\n\nSome hand-written instructions.\n\n'; + const userSuffix = '\n## Extra notes\n\nMore user content here.\n'; + writeFileSync(filePath, userPrefix + userSuffix, 'utf8'); + + // First install: merges managed section into the file + const first = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + expect(first.status, 'first install: exit code').toBe(0); + + const afterFirst = readFileSync(filePath, 'utf8'); + expect(afterFirst).toContain(userPrefix.trimEnd()); + expect(afterFirst).toContain(userSuffix.trim()); + expect(afterFirst).toContain(MANAGED_SECTION_BEGIN); + expect(afterFirst).toContain(MANAGED_SECTION_END); + + // Capture the managed section content for comparison across runs + const sectionAfterFirst = afterFirst.slice( + afterFirst.indexOf(MANAGED_SECTION_BEGIN), + afterFirst.indexOf(MANAGED_SECTION_END) + MANAGED_SECTION_END.length, + ); + + // Second install (re-run): must be idempotent and preserve user content + const second = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + expect(second.status, 'second install: exit code').toBe(0); + + const afterSecond = readFileSync(filePath, 'utf8'); + + // User content outside sentinels must be intact + expect(afterSecond).toContain(userPrefix.trimEnd()); + expect(afterSecond).toContain(userSuffix.trim()); + + // Exactly one pair of sentinels after re-run + const escRe2 = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const beginCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_BEGIN), 'g')) ?? []).length; + const endCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_END), 'g')) ?? []).length; + expect(beginCount2, 'exactly one BEGIN sentinel after re-run').toBe(1); + expect(endCount2, 'exactly one END sentinel after re-run').toBe(1); + + // Managed section content must be byte-identical between runs + const sectionAfterSecond = afterSecond.slice( + afterSecond.indexOf(MANAGED_SECTION_BEGIN), + afterSecond.indexOf(MANAGED_SECTION_END) + MANAGED_SECTION_END.length, + ); + expect(sectionAfterSecond).toBe(sectionAfterFirst); + }); }); // --------------------------------------------------------------------------- // 3. Idempotent re-run From 97fe93e5d96d488a58c17aee3a729af42c57feee Mon Sep 17 00:00:00 2001 From: Tasfia-17 Date: Fri, 24 Jul 2026 05:47:27 +0600 Subject: [PATCH 3/4] test(agent): address remaining CodeRabbit comments on Gemini target - Add two-line assertion that sectionAfterSecond does not contain user prefix/suffix, proving user content stayed outside the managed section and was not absorbed into it during re-run - Add dedicated sentinel-contract test for managed-section targets: verifies MANAGED_SECTION_BEGIN is target-neutral (no 'codex') and that MANAGED_SECTION_BEGIN_LEGACY exists for backward compat - Import MANAGED_SECTION_BEGIN_LEGACY in agent-targets.test.ts --- src/lib/agent-targets.test.ts | 22 +++++++++++++++++----- test/e2e/agent-install.e2e.test.ts | 3 +++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib/agent-targets.test.ts b/src/lib/agent-targets.test.ts index 7b3edf5e..a2048914 100644 --- a/src/lib/agent-targets.test.ts +++ b/src/lib/agent-targets.test.ts @@ -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, @@ -409,13 +410,24 @@ describe('renderForTarget("gemini")', () => { 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. + // 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'); - // 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); + }); + + 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'); }); }); diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index da34eb96..80c2b85a 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -367,6 +367,9 @@ describe('content integrity', () => { afterSecond.indexOf(MANAGED_SECTION_BEGIN), afterSecond.indexOf(MANAGED_SECTION_END) + MANAGED_SECTION_END.length, ); + // User content must be OUTSIDE the managed section, not absorbed into it + expect(sectionAfterSecond).not.toContain(userPrefix.trimEnd()); + expect(sectionAfterSecond).not.toContain(userSuffix.trim()); expect(sectionAfterSecond).toBe(sectionAfterFirst); }); }); From a40bbf0ba8edc14ac92f7824f95016af4adc54d7 Mon Sep 17 00:00:00 2001 From: Tasfia-17 Date: Fri, 24 Jul 2026 05:49:57 +0600 Subject: [PATCH 4/4] style: fix prettier formatting in agent-install e2e test --- test/e2e/agent-install.e2e.test.ts | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index 80c2b85a..6b84dfc0 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -274,7 +274,15 @@ describe('content integrity', () => { it('gemini GEMINI.md contains ONE managed section with verify and onboard content', () => { const tmpDir = freshTmpDir(); - const result = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + const result = runCli([ + 'agent', + 'install', + '--target=gemini', + '--dir', + tmpDir, + '--output', + 'json', + ]); // Exit code must be 0 expect(result.status, 'gemini install: exit code').toBe(0); @@ -330,7 +338,15 @@ describe('content integrity', () => { writeFileSync(filePath, userPrefix + userSuffix, 'utf8'); // First install: merges managed section into the file - const first = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + const first = runCli([ + 'agent', + 'install', + '--target=gemini', + '--dir', + tmpDir, + '--output', + 'json', + ]); expect(first.status, 'first install: exit code').toBe(0); const afterFirst = readFileSync(filePath, 'utf8'); @@ -346,7 +362,15 @@ describe('content integrity', () => { ); // Second install (re-run): must be idempotent and preserve user content - const second = runCli(['agent', 'install', '--target=gemini', '--dir', tmpDir, '--output', 'json']); + const second = runCli([ + 'agent', + 'install', + '--target=gemini', + '--dir', + tmpDir, + '--output', + 'json', + ]); expect(second.status, 'second install: exit code').toBe(0); const afterSecond = readFileSync(filePath, 'utf8'); @@ -357,8 +381,10 @@ describe('content integrity', () => { // Exactly one pair of sentinels after re-run const escRe2 = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const beginCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_BEGIN), 'g')) ?? []).length; - const endCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_END), 'g')) ?? []).length; + const beginCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_BEGIN), 'g')) ?? []) + .length; + const endCount2 = (afterSecond.match(new RegExp(escRe2(MANAGED_SECTION_END), 'g')) ?? []) + .length; expect(beginCount2, 'exactly one BEGIN sentinel after re-run').toBe(1); expect(endCount2, 'exactly one END sentinel after re-run').toBe(1);