@@ -52,6 +52,15 @@ assert_dir_exists() {
5252 fi
5353}
5454
55+ assert_dir_not_exists () {
56+ local dir=" $1 " label=" ${2:- $1 } "
57+ if [ -d " $dir " ]; then
58+ fail " $label unexpectedly exists: $dir "
59+ else
60+ pass " $label absent"
61+ fi
62+ }
63+
5564assert_no_placeholders () {
5665 local dir=" $1 " label=" $2 "
5766 local found
@@ -337,54 +346,50 @@ assert_managed_files() {
337346 assert_file_contains " $gi " ' ^\.data/$' \
338347 " $tag .gitignore has common .data/ entry"
339348
340- # Per-lang assertions. `scaffold.sh` selects the polyglot
341- # dist (which composes all langs into `.gitignore`) whenever
342- # 2+ langs are passed; only single-lang scaffolds emit a
343- # `.gitignore` that excludes the other langs. So exclusion
344- # assertions are gated on single-lang mode.
345- local lang_count
346- lang_count=" $( echo " ${lang_csv// [[:space:]]/ } " | tr ' ,' ' \n' | grep -c .) "
347- local is_polyglot=false
348- [ " ${lang_count:- 0} " -gt 1 ] && is_polyglot=true
349+ # Per-lang assertions. Post-T-0867, `scaffold.sh` honors the
350+ # `--langs` subset: polyglot mode composes only the opted-in
351+ # lang sections into `.gitignore`. Exclusion assertions fire
352+ # for every lang NOT in the subset, regardless of polyglot vs.
353+ # single-lang mode.
349354
350355 # vendor/ is shared by go + php, so php's discriminator is
351356 # composer.lock.
352357 if [[ " $lang_norm " == * ,go,* ]] || [[ " $lang_norm " == * ,php,* ]]; then
353358 assert_file_contains " $gi " ' ^vendor/$' \
354359 " $tag .gitignore has vendor/ (go|php)"
355- elif ! $is_polyglot ; then
360+ else
356361 assert_file_excludes " $gi " ' ^vendor/$' \
357362 " $tag .gitignore omits vendor/ (no go|php)"
358363 fi
359364
360365 if [[ " $lang_norm " == * ,ts,* ]]; then
361366 assert_file_contains " $gi " ' ^node_modules/$' \
362367 " $tag .gitignore has ts node_modules/"
363- elif ! $is_polyglot ; then
368+ else
364369 assert_file_excludes " $gi " ' ^node_modules/$' \
365370 " $tag .gitignore omits ts node_modules/"
366371 fi
367372
368373 if [[ " $lang_norm " == * ,py,* ]]; then
369374 assert_file_contains " $gi " ' ^__pycache__/$' \
370375 " $tag .gitignore has py __pycache__/"
371- elif ! $is_polyglot ; then
376+ else
372377 assert_file_excludes " $gi " ' ^__pycache__/$' \
373378 " $tag .gitignore omits py __pycache__/"
374379 fi
375380
376381 if [[ " $lang_norm " == * ,rs,* ]]; then
377382 assert_file_contains " $gi " ' ^target/$' \
378383 " $tag .gitignore has rs target/"
379- elif ! $is_polyglot ; then
384+ else
380385 assert_file_excludes " $gi " ' ^target/$' \
381386 " $tag .gitignore omits rs target/"
382387 fi
383388
384389 if [[ " $lang_norm " == * ,php,* ]]; then
385390 assert_file_contains " $gi " ' ^composer\.lock$' \
386391 " $tag .gitignore has php composer.lock"
387- elif ! $is_polyglot ; then
392+ else
388393 assert_file_excludes " $gi " ' ^composer\.lock$' \
389394 " $tag .gitignore omits php composer.lock"
390395 fi
@@ -418,23 +423,17 @@ assert_managed_files() {
418423 assert_file_contains " $ga " ' ^\*\.png[[:space:]][[:space:]]*binary$' \
419424 " $tag .gitattributes has common *.png binary rule"
420425
421- # Per-lang assertions. `scaffold.sh` selects the polyglot
422- # dist (which composes all langs into `.gitattributes`) whenever
423- # 2+ langs are passed; only single-lang scaffolds emit a
424- # `.gitattributes` that excludes the other langs. So exclusion
425- # assertions are gated on single-lang mode (mirrors the
426- # `.gitignore` block above).
427- local ga_lang_count
428- ga_lang_count=" $( echo " ${lang_csv// [[:space:]]/ } " | tr ' ,' ' \n' | grep -c .) "
429- local ga_is_polyglot=false
430- [ " ${ga_lang_count:- 0} " -gt 1 ] && ga_is_polyglot=true
426+ # Per-lang assertions. Post-T-0867, `scaffold.sh` honors the
427+ # `--langs` subset for polyglot composition. Exclusion
428+ # assertions fire for every lang NOT in the subset (mirrors
429+ # the `.gitignore` block above).
431430
432431 if [[ " $lang_norm " == * ,go,* ]]; then
433432 assert_file_contains " $ga " ' ^\*\.go[[:space:]][[:space:]]*text eol=lf$' \
434433 " $tag .gitattributes has go *.go text rule"
435434 assert_file_contains " $ga " ' ^go\.sum[[:space:]][[:space:]]*linguist-generated=true$' \
436435 " $tag .gitattributes has go go.sum linguist-generated"
437- elif ! $ga_is_polyglot ; then
436+ else
438437 assert_file_excludes " $ga " ' go\.sum' \
439438 " $tag .gitattributes omits go go.sum"
440439 fi
@@ -444,7 +443,7 @@ assert_managed_files() {
444443 " $tag .gitattributes has ts *.ts text rule"
445444 assert_file_contains " $ga " ' ^pnpm-lock\.yaml[[:space:]][[:space:]]*linguist-generated=true$' \
446445 " $tag .gitattributes has ts pnpm-lock.yaml linguist-generated"
447- elif ! $ga_is_polyglot ; then
446+ else
448447 assert_file_excludes " $ga " ' pnpm-lock\.yaml' \
449448 " $tag .gitattributes omits ts pnpm-lock.yaml"
450449 fi
@@ -454,7 +453,7 @@ assert_managed_files() {
454453 " $tag .gitattributes has py *.py text rule"
455454 assert_file_contains " $ga " ' ^\*\*/uv\.lock[[:space:]][[:space:]]*linguist-generated=true$' \
456455 " $tag .gitattributes has py **/uv.lock linguist-generated"
457- elif ! $ga_is_polyglot ; then
456+ else
458457 assert_file_excludes " $ga " ' \*\*/uv\.lock' \
459458 " $tag .gitattributes omits py **/uv.lock"
460459 fi
@@ -464,7 +463,7 @@ assert_managed_files() {
464463 " $tag .gitattributes has rs *.rs text rule"
465464 assert_file_contains " $ga " ' ^Cargo\.lock[[:space:]][[:space:]]*linguist-generated=true$' \
466465 " $tag .gitattributes has rs Cargo.lock linguist-generated"
467- elif ! $ga_is_polyglot ; then
466+ else
468467 assert_file_excludes " $ga " ' Cargo\.lock' \
469468 " $tag .gitattributes omits rs Cargo.lock"
470469 fi
@@ -474,7 +473,7 @@ assert_managed_files() {
474473 " $tag .gitattributes has php *.php text rule"
475474 assert_file_contains " $ga " ' ^composer\.lock[[:space:]][[:space:]]*linguist-generated=true$' \
476475 " $tag .gitattributes has php composer.lock linguist-generated"
477- elif ! $ga_is_polyglot ; then
476+ else
478477 assert_file_excludes " $ga " ' composer\.lock' \
479478 " $tag .gitattributes omits php composer.lock"
480479 fi
@@ -845,6 +844,151 @@ bash "$SCRIPT_DIR/scaffold.sh" test-app-with-svc \
845844 --forge none --no-push --no-tlc
846845assert_managed_files " $TEST13C_DIR " " go" " postgres,redis" " test-app-with-svc"
847846
847+ # ======================================================
848+ # Test 14: --langs subset go,ts (2-lang polyglot)
849+ # ======================================================
850+ #
851+ # Post-T-0867 polyglot honors --langs subset. Verify go + ts
852+ # present, py + rs + php excluded from lang dirs and managed
853+ # files. Mirrors TEST2 (go,ts,py) shape.
854+
855+ info " Test 14: --langs subset go,ts"
856+ TEST14_DIR=" $TMPDIR_BASE /test14app"
857+
858+ bash " $SCRIPT_DIR /scaffold.sh" test14app \
859+ --output " $TEST14_DIR " \
860+ --lang go,ts \
861+ --description " Test go,ts subset" \
862+ --license apache \
863+ --author " Test Author" \
864+ --email " test@example.com" \
865+ --module-prefix " github.com/testorg" \
866+ --forge none \
867+ --no-push --no-tlc
868+
869+ assert_dir_exists " $TEST14_DIR " " test14 project dir"
870+ assert_dir_exists " $TEST14_DIR /go" " test14 go/ dir"
871+ assert_dir_exists " $TEST14_DIR /ts" " test14 ts/ dir"
872+ assert_dir_not_exists " $TEST14_DIR /py" " test14 py/ dir"
873+ assert_dir_not_exists " $TEST14_DIR /rs" " test14 rs/ dir"
874+ assert_dir_not_exists " $TEST14_DIR /php" " test14 php/ dir"
875+ assert_file_exists " $TEST14_DIR /Makefile" " test14 root Makefile"
876+ assert_file_contains " $TEST14_DIR /Makefile" ' MAKE) -C go' \
877+ " test14 Makefile delegates to go"
878+ assert_file_contains " $TEST14_DIR /Makefile" ' MAKE) -C ts' \
879+ " test14 Makefile delegates to ts"
880+ assert_no_placeholders " $TEST14_DIR " " test14"
881+ assert_managed_files " $TEST14_DIR " " go,ts" " " " test14app"
882+
883+ # ======================================================
884+ # Test 15: --langs subset php,go (out-of-order, section ordering)
885+ # ======================================================
886+ #
887+ # Section-order convention (PR #103, documented in
888+ # templates/shared/README.md): per-lang sections in `.gitignore`
889+ # and `.gitattributes` appear in LANG_ARRAY pass order. Verify
890+ # php block precedes go block when invoked as --lang php,go.
891+
892+ info " Test 15: --langs subset php,go (section order = pass order)"
893+ TEST15_DIR=" $TMPDIR_BASE /test15app"
894+
895+ bash " $SCRIPT_DIR /scaffold.sh" test15app \
896+ --output " $TEST15_DIR " \
897+ --lang php,go \
898+ --description " Test php,go ordering" \
899+ --license apache \
900+ --author " Test Author" \
901+ --email " test@example.com" \
902+ --module-prefix " github.com/testorg" \
903+ --forge none \
904+ --no-push --no-tlc
905+
906+ assert_dir_exists " $TEST15_DIR " " test15 project dir"
907+ assert_dir_exists " $TEST15_DIR /php" " test15 php/ dir"
908+ assert_dir_exists " $TEST15_DIR /go" " test15 go/ dir"
909+ assert_dir_not_exists " $TEST15_DIR /ts" " test15 ts/ dir"
910+ assert_dir_not_exists " $TEST15_DIR /py" " test15 py/ dir"
911+ assert_dir_not_exists " $TEST15_DIR /rs" " test15 rs/ dir"
912+ assert_managed_files " $TEST15_DIR " " php,go" " " " test15app"
913+
914+ # Section-ordering check: composer.lock (php discriminator) must
915+ # appear BEFORE go.sum-ish anchors in .gitattributes. For
916+ # .gitignore, php's composer.lock must appear BEFORE the go-only
917+ # discriminator. (vendor/ is shared by go+php so it's not a
918+ # usable anchor.)
919+ TEST15_GI=" $TEST15_DIR /.gitignore"
920+ TEST15_GA=" $TEST15_DIR /.gitattributes"
921+
922+ if [ -f " $TEST15_GI " ]; then
923+ # composer.lock (php) and Cargo-or-go anchor. Go has no
924+ # unique .gitignore line beyond vendor/ (shared). Use the
925+ # cli-php section marker if present, else fall back to
926+ # composer.lock line-number comparison vs. end-of-file.
927+ php_line=" $( grep -n ' ^composer\.lock$' " $TEST15_GI " | head -1 | cut -d: -f1 || true) "
928+ if [ -n " $php_line " ]; then
929+ pass " test15 .gitignore contains php composer.lock at line $php_line "
930+ else
931+ fail " test15 .gitignore missing php composer.lock line"
932+ fi
933+ fi
934+
935+ if [ -f " $TEST15_GA " ]; then
936+ # composer.lock = php-only, go.sum = go-only. Both lang
937+ # sections must be present and php must precede go.
938+ php_ga_line=" $( grep -n ' ^composer\.lock[[:space:]]' " $TEST15_GA " | head -1 | cut -d: -f1 || true) "
939+ go_ga_line=" $( grep -n ' ^go\.sum[[:space:]]' " $TEST15_GA " | head -1 | cut -d: -f1 || true) "
940+ if [ -n " $php_ga_line " ] && [ -n " $go_ga_line " ]; then
941+ if [ " $php_ga_line " -lt " $go_ga_line " ]; then
942+ pass " test15 .gitattributes section order: php (L$php_ga_line ) before go (L$go_ga_line )"
943+ else
944+ fail " test15 .gitattributes section order: php (L$php_ga_line ) NOT before go (L$go_ga_line )"
945+ fi
946+ else
947+ fail " test15 .gitattributes missing php composer.lock or go go.sum anchor (php_ga_line='$php_ga_line ' go_ga_line='$go_ga_line ')"
948+ fi
949+ fi
950+
951+ # ======================================================
952+ # Test 16: --langs full set go,ts,py,rs,php (5-lang regression)
953+ # ======================================================
954+ #
955+ # Regression: full lang set produces all five lang dirs and
956+ # every per-lang managed-file section. Mirrors TEST12 (4-lang)
957+ # extended to include php.
958+
959+ info " Test 16: --langs full set go,ts,py,rs,php"
960+ TEST16_DIR=" $TMPDIR_BASE /test16app"
961+
962+ bash " $SCRIPT_DIR /scaffold.sh" test16app \
963+ --output " $TEST16_DIR " \
964+ --lang go,ts,py,rs,php \
965+ --description " Test full 5-lang polyglot" \
966+ --license apache \
967+ --author " Test Author" \
968+ --email " test@example.com" \
969+ --module-prefix " github.com/testorg" \
970+ --forge none \
971+ --no-push --no-tlc
972+
973+ assert_dir_exists " $TEST16_DIR " " test16 project dir"
974+ assert_dir_exists " $TEST16_DIR /go" " test16 go/ dir"
975+ assert_dir_exists " $TEST16_DIR /ts" " test16 ts/ dir"
976+ assert_dir_exists " $TEST16_DIR /py" " test16 py/ dir"
977+ assert_dir_exists " $TEST16_DIR /rs" " test16 rs/ dir"
978+ assert_dir_exists " $TEST16_DIR /php" " test16 php/ dir"
979+ assert_file_exists " $TEST16_DIR /Makefile" " test16 root Makefile"
980+ assert_file_contains " $TEST16_DIR /Makefile" ' MAKE) -C go' \
981+ " test16 Makefile delegates to go"
982+ assert_file_contains " $TEST16_DIR /Makefile" ' MAKE) -C ts' \
983+ " test16 Makefile delegates to ts"
984+ assert_file_contains " $TEST16_DIR /Makefile" ' MAKE) -C py' \
985+ " test16 Makefile delegates to py"
986+ assert_file_contains " $TEST16_DIR /Makefile" ' MAKE) -C rs' \
987+ " test16 Makefile delegates to rs"
988+ assert_file_contains " $TEST16_DIR /Makefile" ' MAKE) -C php' \
989+ " test16 Makefile delegates to php"
990+ assert_managed_files " $TEST16_DIR " " go,ts,py,rs,php" " " " test16app"
991+
848992# ======================================================
849993# Summary
850994# ======================================================
0 commit comments