@@ -274,7 +274,23 @@ describe('content integrity', () => {
274274
275275 it ( 'gemini GEMINI.md contains ONE managed section with verify and onboard content' , ( ) => {
276276 const tmpDir = freshTmpDir ( ) ;
277- runCli ( [ 'agent' , 'install' , '--target=gemini' , '--dir' , tmpDir , '--output' , 'json' ] ) ;
277+ const result = runCli ( [ 'agent' , 'install' , '--target=gemini' , '--dir' , tmpDir , '--output' , 'json' ] ) ;
278+
279+ // Exit code must be 0
280+ expect ( result . status , 'gemini install: exit code' ) . toBe ( 0 ) ;
281+
282+ // stdout must be valid JSON with the expected entry
283+ const parsed = JSON . parse ( result . stdout ) as Array < {
284+ target : string ;
285+ path : string ;
286+ action : string ;
287+ skills : string [ ] ;
288+ } > ;
289+ expect ( Array . isArray ( parsed ) , 'output should be a JSON array' ) . toBe ( true ) ;
290+ const entry = parsed . find ( r => r . target === 'gemini' ) ;
291+ expect ( entry , 'gemini entry in JSON output' ) . toBeDefined ( ) ;
292+ expect ( entry ! . action , 'gemini action' ) . toBe ( 'section-installed' ) ;
293+ expect ( entry ! . path ) . toBe ( TARGETS . gemini . path ) ;
278294
279295 const filePath = join ( tmpDir , TARGETS . gemini . path ) ;
280296 const content = readFileSync ( filePath , 'utf8' ) ;
@@ -302,6 +318,57 @@ describe('content integrity', () => {
302318 // (d) No frontmatter fence -- GEMINI.md is plain prose
303319 expect ( content . startsWith ( '---' ) , 'gemini: must NOT start with ---' ) . toBe ( false ) ;
304320 } ) ;
321+
322+ it ( 'gemini managed-section preserves user content outside sentinels across re-runs' , ( ) => {
323+ const tmpDir = freshTmpDir ( ) ;
324+ const filePath = join ( tmpDir , TARGETS . gemini . path ) ;
325+
326+ // Pre-populate GEMINI.md with user content above and below where the
327+ // managed section will land.
328+ const userPrefix = '# My Project\n\nSome hand-written instructions.\n\n' ;
329+ const userSuffix = '\n## Extra notes\n\nMore user content here.\n' ;
330+ writeFileSync ( filePath , userPrefix + userSuffix , 'utf8' ) ;
331+
332+ // First install: merges managed section into the file
333+ const first = runCli ( [ 'agent' , 'install' , '--target=gemini' , '--dir' , tmpDir , '--output' , 'json' ] ) ;
334+ expect ( first . status , 'first install: exit code' ) . toBe ( 0 ) ;
335+
336+ const afterFirst = readFileSync ( filePath , 'utf8' ) ;
337+ expect ( afterFirst ) . toContain ( userPrefix . trimEnd ( ) ) ;
338+ expect ( afterFirst ) . toContain ( userSuffix . trim ( ) ) ;
339+ expect ( afterFirst ) . toContain ( MANAGED_SECTION_BEGIN ) ;
340+ expect ( afterFirst ) . toContain ( MANAGED_SECTION_END ) ;
341+
342+ // Capture the managed section content for comparison across runs
343+ const sectionAfterFirst = afterFirst . slice (
344+ afterFirst . indexOf ( MANAGED_SECTION_BEGIN ) ,
345+ afterFirst . indexOf ( MANAGED_SECTION_END ) + MANAGED_SECTION_END . length ,
346+ ) ;
347+
348+ // Second install (re-run): must be idempotent and preserve user content
349+ const second = runCli ( [ 'agent' , 'install' , '--target=gemini' , '--dir' , tmpDir , '--output' , 'json' ] ) ;
350+ expect ( second . status , 'second install: exit code' ) . toBe ( 0 ) ;
351+
352+ const afterSecond = readFileSync ( filePath , 'utf8' ) ;
353+
354+ // User content outside sentinels must be intact
355+ expect ( afterSecond ) . toContain ( userPrefix . trimEnd ( ) ) ;
356+ expect ( afterSecond ) . toContain ( userSuffix . trim ( ) ) ;
357+
358+ // Exactly one pair of sentinels after re-run
359+ const escRe2 = ( s : string ) => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
360+ const beginCount2 = ( afterSecond . match ( new RegExp ( escRe2 ( MANAGED_SECTION_BEGIN ) , 'g' ) ) ?? [ ] ) . length ;
361+ const endCount2 = ( afterSecond . match ( new RegExp ( escRe2 ( MANAGED_SECTION_END ) , 'g' ) ) ?? [ ] ) . length ;
362+ expect ( beginCount2 , 'exactly one BEGIN sentinel after re-run' ) . toBe ( 1 ) ;
363+ expect ( endCount2 , 'exactly one END sentinel after re-run' ) . toBe ( 1 ) ;
364+
365+ // Managed section content must be byte-identical between runs
366+ const sectionAfterSecond = afterSecond . slice (
367+ afterSecond . indexOf ( MANAGED_SECTION_BEGIN ) ,
368+ afterSecond . indexOf ( MANAGED_SECTION_END ) + MANAGED_SECTION_END . length ,
369+ ) ;
370+ expect ( sectionAfterSecond ) . toBe ( sectionAfterFirst ) ;
371+ } ) ;
305372} ) ;
306373// ---------------------------------------------------------------------------
307374// 3. Idempotent re-run
0 commit comments