@@ -105,22 +105,25 @@ func TestExpandValue_Success(t *testing.T) {
105105func TestExpandValue_Errors(t *testing.T) {
106106 t.Parallel()
107107
108− t.Run("unset var is an error, not empty", func(t *testing.T) {
108+ t.Run("unset var expands to empty under lenient default", func(t *testing.T) {
109109 t.Parallel()
110− _, err := ExpandValue(t.Context(), "$MISSING", nil)
111− require.Error(t, err)
110+ got, err := ExpandValue(t.Context(), "$MISSING", nil)
111+ require.NoError(t, err)
112+ require.Equal(t, "", got)
112113 })
113114
114− t.Run("unset var inside braces is an error", func(t *testing.T) {
115+ t.Run("unset var inside braces expands to empty", func(t *testing.T) {
115116 t.Parallel()
116− _, err := ExpandValue(t.Context(), "${MISSING}", nil)
117− require.Error(t, err)
117+ got, err := ExpandValue(t.Context(), "${MISSING}", nil)
118+ require.NoError(t, err)
119+ require.Equal(t, "", got)
118120 })
119121
120− t.Run("unset var inside cmdsubst is an error", func(t *testing.T) {
122+ t.Run("unset var inside cmdsubst expands to empty", func(t *testing.T) {
121123 t.Parallel()
122− _, err := ExpandValue(t.Context(), `$(printf '%s' "$MISSING")`, nil)
123− require.Error(t, err)
124+ got, err := ExpandValue(t.Context(), `$(printf '%s' "$MISSING")`, nil)
125+ require.NoError(t, err)
126+ require.Equal(t, "", got)
124127 })
125128
126129 t.Run("bad syntax returns error", func(t *testing.T) {
@@ -173,6 +176,45 @@ func TestExpandValue_Errors(t *testing.T) {
173176 })
174177}
175178
179+// TestExpandValue_StrictToggle pins the NoUnset escape hatch: when a
180+// caller flips strict mode on, bare $UNSET must error instead of
181+// expanding to the empty string. Must not run in parallel: it mutates
182+// the package-level NoUnset atomic, so a parallel peer observing the
183+// flipped value would break the lenient default other tests assume.
184+func TestExpandValue_StrictToggle(t *testing.T) {
185+ NoUnset.Store(true)
186+ t.Cleanup(func() { NoUnset.Store(false) })
187+
188+ _, err := ExpandValue(t.Context(), "$UNSET", nil)
189+ require.Error(t, err)
190+
191+ _, err = ExpandValue(t.Context(), "${UNSET}", nil)
192+ require.Error(t, err)
193+
194+ _, err = ExpandValue(t.Context(), `$(printf '%s' "$UNSET")`, nil)
195+ require.Error(t, err)
196+}
197+
198+// TestExpandValue_RequiredOptIn pins the per-reference opt-in strict
199+// idiom ${VAR:?msg}: it must error whether or not the global NoUnset
200+// toggle is on, so config authors can mark individual credentials as
201+// required without flipping the global default.
202+func TestExpandValue_RequiredOptIn(t *testing.T) {
203+ t.Parallel()
204+
205+ _, err := ExpandValue(t.Context(), "${REQUIRED:?must be set}", nil)
206+ require.Error(t, err)
207+ require.Contains(t, err.Error(), "must be set")
208+
209+ got, err := ExpandValue(
210+ t.Context(),
211+ "${REQUIRED:?must be set}",
212+ []string{"REQUIRED=ok"},
213+ )
214+ require.NoError(t, err)
215+ require.Equal(t, "ok", got)
216+}
217+
176218func TestSanitizeStderr(t *testing.T) {
177219 t.Parallel()
178220