@@ -172,6 +172,15 @@ func runCoreExpectError(t *testing.T, bin, dir string, args ...string) string {
172172 return string (out )
173173}
174174
175+ // patchConfig reads the config, applies a mutation, and writes it back.
176+ func patchConfig (t * testing.T , repoPath string , mutate func (map [string ]interface {})) {
177+ t .Helper ()
178+ cfg := readConfig (t , repoPath )
179+ mutate (cfg )
180+ data , _ := json .MarshalIndent (cfg , "" , " " )
181+ os .WriteFile (filepath .Join (repoPath , ".codemob" , "config.json" ), append (data , '\n' ), 0644 )
182+ }
183+
175184// readConfig reads and parses .codemob/config.json from the repo.
176185func readConfig (t * testing.T , repoPath string ) map [string ]interface {} {
177186 t .Helper ()
@@ -205,6 +214,9 @@ func TestInit(t *testing.T) {
205214 if cfg ["base_branch" ] != "main" {
206215 t .Errorf ("expected base_branch=main, got %v" , cfg ["base_branch" ])
207216 }
217+ if _ , ok := cfg ["post_create_script" ]; ! ok {
218+ t .Error ("expected post_create_script field to be present in config" )
219+ }
208220
209221 // then -> .codemob/mobs/ dir should exist
210222 if _ , err := os .Stat (filepath .Join (repoPath , ".codemob" , "mobs" )); err != nil {
@@ -334,6 +346,30 @@ func TestInitIdempotent(t *testing.T) {
334346 }
335347}
336348
349+ func TestInitAddsNewFieldsToExistingConfig (t * testing.T ) {
350+ bin := buildCore (t )
351+ _ , repoPath := setupTestRepo (t )
352+ initRepo (t , bin , repoPath )
353+
354+ // given -> remove post_create_script from config (simulates pre-feature config)
355+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
356+ delete (cfg , "post_create_script" )
357+ })
358+ cfg := readConfig (t , repoPath )
359+ if _ , ok := cfg ["post_create_script" ]; ok {
360+ t .Fatal ("precondition: post_create_script should have been removed" )
361+ }
362+
363+ // when -> run init again (not reinit)
364+ initRepo (t , bin , repoPath )
365+
366+ // then -> field should be present
367+ cfg = readConfig (t , repoPath )
368+ if _ , ok := cfg ["post_create_script" ]; ! ok {
369+ t .Error ("expected init to add post_create_script to existing config" )
370+ }
371+ }
372+
337373func TestNewMob (t * testing.T ) {
338374 bin := buildCore (t )
339375 _ , repoPath := setupTestRepo (t )
@@ -1519,6 +1555,151 @@ func TestPurgeExternalThenListAndNew(t *testing.T) {
15191555 }
15201556}
15211557
1558+ func TestPostCreateScriptRuns (t * testing.T ) {
1559+ bin := buildCore (t )
1560+ _ , repoPath := setupTestRepo (t )
1561+ initRepo (t , bin , repoPath )
1562+
1563+ // given -> a post_create_script that creates a marker file
1564+ scriptPath := filepath .Join (repoPath , "setup.sh" )
1565+ os .WriteFile (scriptPath , []byte ("#!/bin/sh\n touch .setup-done\n " ), 0755 )
1566+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
1567+ cfg ["post_create_script" ] = scriptPath
1568+ })
1569+
1570+ // when -> create a mob
1571+ runCore (t , bin , repoPath , "new" , "script-test" , "--no-launch" )
1572+
1573+ // then -> marker file should exist in the worktree
1574+ worktreePath := filepath .Join (repoPath , ".codemob" , "mobs" , "script-test" )
1575+ if _ , err := os .Stat (filepath .Join (worktreePath , ".setup-done" )); err != nil {
1576+ t .Errorf ("post_create_script did not run: marker file missing: %v" , err )
1577+ }
1578+ }
1579+
1580+ func TestPostCreateScriptRelativePath (t * testing.T ) {
1581+ bin := buildCore (t )
1582+ _ , repoPath := setupTestRepo (t )
1583+ initRepo (t , bin , repoPath )
1584+
1585+ // given -> a post_create_script with a relative path
1586+ scriptPath := filepath .Join (repoPath , "setup.sh" )
1587+ os .WriteFile (scriptPath , []byte ("#!/bin/sh\n touch .setup-done\n " ), 0755 )
1588+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
1589+ cfg ["post_create_script" ] = "setup.sh"
1590+ })
1591+
1592+ // when -> create a mob
1593+ runCore (t , bin , repoPath , "new" , "rel-path-test" , "--no-launch" )
1594+
1595+ // then -> marker file should exist in the worktree
1596+ worktreePath := filepath .Join (repoPath , ".codemob" , "mobs" , "rel-path-test" )
1597+ if _ , err := os .Stat (filepath .Join (worktreePath , ".setup-done" )); err != nil {
1598+ t .Errorf ("post_create_script with relative path did not run: %v" , err )
1599+ }
1600+ }
1601+
1602+ func TestPostCreateScriptFailureBlocksMob (t * testing.T ) {
1603+ bin := buildCore (t )
1604+ _ , repoPath := setupTestRepo (t )
1605+ initRepo (t , bin , repoPath )
1606+
1607+ // given -> a post_create_script that fails
1608+ scriptPath := filepath .Join (repoPath , "bad-setup.sh" )
1609+ os .WriteFile (scriptPath , []byte ("#!/bin/sh\n exit 1\n " ), 0755 )
1610+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
1611+ cfg ["post_create_script" ] = scriptPath
1612+ })
1613+
1614+ // when -> create a mob
1615+ out := runCoreExpectError (t , bin , repoPath , "new" , "fail-test" , "--no-launch" )
1616+
1617+ // then -> error message should mention the script
1618+ if ! strings .Contains (out , "post_create_script failed" ) {
1619+ t .Errorf ("expected error about post_create_script, got: %s" , out )
1620+ }
1621+
1622+ // then -> worktree should be cleaned up
1623+ worktreePath := filepath .Join (repoPath , ".codemob" , "mobs" , "fail-test" )
1624+ if _ , err := os .Stat (worktreePath ); err == nil {
1625+ t .Error ("worktree should have been removed after script failure" )
1626+ }
1627+
1628+ // then -> mob should not exist in config
1629+ cfg := readConfig (t , repoPath )
1630+ mobs , _ := cfg ["mobs" ].([]interface {})
1631+ for _ , m := range mobs {
1632+ entry , _ := m .(map [string ]interface {})
1633+ if entry ["name" ] == "fail-test" {
1634+ t .Error ("mob should have been removed from config after script failure" )
1635+ }
1636+ }
1637+ }
1638+
1639+ func TestPostCreateScriptMissingFileError (t * testing.T ) {
1640+ bin := buildCore (t )
1641+ _ , repoPath := setupTestRepo (t )
1642+ initRepo (t , bin , repoPath )
1643+
1644+ // given -> post_create_script pointing to a nonexistent file
1645+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
1646+ cfg ["post_create_script" ] = "/nonexistent/setup.sh"
1647+ })
1648+
1649+ // when -> create a mob
1650+ out := runCoreExpectError (t , bin , repoPath , "new" , "missing-script" , "--no-launch" )
1651+
1652+ // then -> error message should mention "not found"
1653+ if ! strings .Contains (out , "not found" ) {
1654+ t .Errorf ("expected 'not found' error, got: %s" , out )
1655+ }
1656+ }
1657+
1658+ func TestPostCreateScriptNotExecutableError (t * testing.T ) {
1659+ bin := buildCore (t )
1660+ _ , repoPath := setupTestRepo (t )
1661+ initRepo (t , bin , repoPath )
1662+
1663+ // given -> a post_create_script without the executable bit
1664+ scriptPath := filepath .Join (repoPath , "setup.sh" )
1665+ os .WriteFile (scriptPath , []byte ("#!/bin/sh\n touch .setup-done\n " ), 0644 )
1666+ patchConfig (t , repoPath , func (cfg map [string ]interface {}) {
1667+ cfg ["post_create_script" ] = scriptPath
1668+ })
1669+
1670+ // when -> create a mob
1671+ out := runCoreExpectError (t , bin , repoPath , "new" , "noexec-test" , "--no-launch" )
1672+
1673+ // then -> error message should mention "not executable"
1674+ if ! strings .Contains (out , "not executable" ) {
1675+ t .Errorf ("expected 'not executable' error, got: %s" , out )
1676+ }
1677+ }
1678+
1679+ func TestPostCreateScriptEmptyIsNoop (t * testing.T ) {
1680+ bin := buildCore (t )
1681+ _ , repoPath := setupTestRepo (t )
1682+ initRepo (t , bin , repoPath )
1683+
1684+ // given -> no post_create_script configured (empty string default)
1685+ // when -> create a mob
1686+ runCore (t , bin , repoPath , "new" , "no-script-test" , "--no-launch" )
1687+
1688+ // then -> mob should be created successfully
1689+ cfg := readConfig (t , repoPath )
1690+ mobs , _ := cfg ["mobs" ].([]interface {})
1691+ found := false
1692+ for _ , m := range mobs {
1693+ mob , _ := m .(map [string ]interface {})
1694+ if mob ["name" ] == "no-script-test" {
1695+ found = true
1696+ }
1697+ }
1698+ if ! found {
1699+ t .Error ("mob should have been created when post_create_script is empty" )
1700+ }
1701+ }
1702+
15221703func TestSlashCommandsCopiedToExternalWorktree (t * testing.T ) {
15231704 bin := buildCore (t )
15241705 _ , repoPath := setupTestRepo (t )
0 commit comments