@@ -0,0 +1,199 @@
7+ "github.com/stretchr/testify/require"
10+func TestExpandValue_Success(t *testing.T) {
20+ name: "plain string round trip",
21+ value: "hello world",
25+ name: "plain var from env",
27+ env: []string{"FOO=bar"},
31+ name: "braced var from env",
32+ value: "pre-${FOO}-post",
33+ env: []string{"FOO=bar"},
34+ want: "pre-bar-post",
37+ name: "default syntax on unset",
38+ value: "${MISSING:-fallback}",
42+ name: "default syntax on set preserves value",
43+ value: "${SET:-fallback}",
44+ env: []string{"SET=used"},
48+ name: "command substitution",
53+ name: "command substitution preserves internal spaces",
54+ value: `$(echo "a b")`,
58+ name: "command substitution strips only trailing newline",
59+ value: "$(printf 'a\\nb\\n')",
63+ name: "literal spaces around cmdsubst are preserved",
64+ value: " $(echo v) ",
68+ name: "paren inside quoted arg to echo",
69+ value: `$(echo ")")`,
73+ name: "nested command substitution",
74+ value: "$(echo $(echo hi))",
78+ name: "glob-like input round trips unchanged",
83+ name: "tilde round trips unchanged",
88+ name: "env var inside cmdsubst",
89+ value: `$(printf '%s' "$FOO")`,
90+ env: []string{"FOO=bar"},
95+ for _, tc := range tests {
96+ t.Run(tc.name, func(t *testing.T) {
98+ got, err := ExpandValue(t.Context(), tc.value, tc.env)
99+ require.NoError(t, err)
100+ require.Equal(t, tc.want, got)
105+func TestExpandValue_Errors(t *testing.T) {
108+ t.Run("unset var is an error, not empty", func(t *testing.T) {
110+ _, err := ExpandValue(t.Context(), "$MISSING", nil)
111+ require.Error(t, err)
114+ t.Run("unset var inside braces is an error", func(t *testing.T) {
116+ _, err := ExpandValue(t.Context(), "${MISSING}", nil)
117+ require.Error(t, err)
120+ t.Run("unset var inside cmdsubst is an error", func(t *testing.T) {
122+ _, err := ExpandValue(t.Context(), `$(printf '%s' "$MISSING")`, nil)
123+ require.Error(t, err)
126+ t.Run("bad syntax returns error", func(t *testing.T) {
128+ _, err := ExpandValue(t.Context(), "$(", nil)
129+ require.Error(t, err)
132+ t.Run("inner non-zero exit returns error with exit code", func(t *testing.T) {
134+ _, err := ExpandValue(t.Context(), "$(false)", nil)
135+ require.Error(t, err)
136+ require.Contains(t, err.Error(), "exit status 1")
139+ t.Run("inner explicit exit code is surfaced", func(t *testing.T) {
141+ _, err := ExpandValue(t.Context(), "$(exit 7)", nil)
142+ require.Error(t, err)
143+ require.Contains(t, err.Error(), "exit status 7")
146+ t.Run("inner stderr is surfaced", func(t *testing.T) {
148+ _, err := ExpandValue(
150+ `$(printf 'boom\n' 1>&2; exit 1)`,
153+ require.Error(t, err)
154+ require.Contains(t, err.Error(), "boom")
157+ t.Run("inner stderr is truncated to byte budget", func(t *testing.T) {
159+ // Emit more than maxInnerStderrBytes bytes of 'X' on stderr.
160+ long := strings.Repeat("X", maxInnerStderrBytes*2)
161+ _, err := ExpandValue(
163+ `$(printf '`+long+`' 1>&2; exit 1)`,
166+ require.Error(t, err)
167+ require.NotContains(
170+ strings.Repeat("X", maxInnerStderrBytes+1),
171+ "stderr should be bounded",
176+func TestSanitizeStderr(t *testing.T) {
179+ t.Run("trims trailing newlines", func(t *testing.T) {
181+ require.Equal(t, "hi", sanitizeStderr([]byte("hi\n\n")))
184+ t.Run("preserves tabs and embedded newlines", func(t *testing.T) {
186+ require.Equal(t, "a\tb\nc", sanitizeStderr([]byte("a\tb\nc")))
189+ t.Run("replaces control characters", func(t *testing.T) {
191+ require.Equal(t, "a?b", sanitizeStderr([]byte{'a', 0x01, 'b'}))
194+ t.Run("bounds output", func(t *testing.T) {
196+ got := sanitizeStderr([]byte(strings.Repeat("x", maxInnerStderrBytes*2)))
197+ require.Len(t, got, maxInnerStderrBytes)