Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions shortcuts/common/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,13 @@ func (ctx *RuntimeContext) InputResolvedFromSource(name string) bool {
return ctx.inputResolved[name]
}

func (ctx *RuntimeContext) markInputResolved(name string) {
// MarkInputResolved records that the named flag now holds content read from an
// external source rather than a value typed inline, setting the bit
// InputResolvedFromSource reports. resolveInputFlags calls it for @file and
// stdin; a domain calls it when it resolves a source itself (sheets reads the
// path a +csv-put caller passed as --file), so the shape guards downstream
// treat the result as content, exactly as they would for --csv @<path>.
func (ctx *RuntimeContext) MarkInputResolved(name string) {
if ctx.inputResolved == nil {
ctx.inputResolved = map[string]bool{}
}
Expand Down Expand Up @@ -1312,7 +1318,7 @@ func resolveInputFlags(rctx *RuntimeContext, flags []Flag) error {
// strip a leading UTF-8 BOM so it can't corrupt the first CSV
// cell or break JSON parsing downstream.
rctx.Cmd.Flags().Set(fl.Name, StripUTF8BOM(string(data)))
rctx.markInputResolved(fl.Name)
rctx.MarkInputResolved(fl.Name)
continue
}

Expand Down Expand Up @@ -1349,7 +1355,7 @@ func resolveInputFlags(rctx *RuntimeContext, flags []Flag) error {
// strip a leading UTF-8 BOM so it
// can't corrupt the first CSV cell or break JSON parsing downstream.
rctx.Cmd.Flags().Set(fl.Name, StripUTF8BOM(string(data)))
rctx.markInputResolved(fl.Name)
rctx.MarkInputResolved(fl.Name)
continue
}
}
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/common/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
// domain tests can exercise guards that branch on InputResolvedFromSource
// without wiring the full resolveInputFlags path.
func TestMarkInputResolved(rctx *RuntimeContext, name string) {
rctx.markInputResolved(name)
rctx.MarkInputResolved(name)

Check warning on line 46 in shortcuts/common/testing.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/common/testing.go#L46

Added line #L46 was not covered by tests
}

// TestNewRuntimeContextForAPI creates a RuntimeContext ready for HTTP tests:
Expand Down
156 changes: 156 additions & 0 deletions shortcuts/sheets/csv_put_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,159 @@ func TestGuardCSVValueIsNotFilePath_PassesThrough(t *testing.T) {
}
}
}

// newCSVFileAliasRuntime is newCSVGuardRuntime plus the record chainFlagAliases
// leaves when the value was typed as --file.
func newCSVFileAliasRuntime(csvVal string) *common.RuntimeContext {
rctx := newCSVGuardRuntime(csvVal)
rctx.Cmd.Annotations = map[string]string{aliasSourceAnnotation("csv"): "file"}
return rctx
}

// TestResolveCSVPathFromFileAlias covers the value-side half of the file → csv
// alias: --file names a path, so a value naming a readable file is read like
// `--csv @<path>` instead of being written into the sheet as literal text.
func TestResolveCSVPathFromFileAlias(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("data.csv", []byte("a,b\n1,2\n"), 0644); err != nil {
t.Fatal(err)
}

t.Run("reads the named file", func(t *testing.T) {
rctx := newCSVFileAliasRuntime("./data.csv")
if err := resolveCSVPathFromFileAlias(rctx); err != nil {
t.Fatalf("resolve: %v", err)
}
if got, _ := rctx.Cmd.Flags().GetString("csv"); got != "a,b\n1,2\n" {
t.Errorf("--csv = %q, want the file contents", got)
}
})

t.Run("the contents are marked source-resolved", func(t *testing.T) {
// Contents read from a file may legitimately look like anything,
// including a path — the shape guards downstream must see the same bit
// they would for --csv @<path>, or a one-cell CSV holding "report.csv"
// is rejected as a caller who forgot the @.
if err := os.WriteFile("pathshaped.csv", []byte("report.csv\n"), 0644); err != nil {
t.Fatal(err)
}
rctx := newCSVFileAliasRuntime("./pathshaped.csv")
if err := resolveCSVPathFromFileAlias(rctx); err != nil {
t.Fatalf("resolve: %v", err)
}
if !rctx.InputResolvedFromSource("csv") {
t.Error("the rewritten value must be marked as read from a source")
}
if err := guardCSVValueIsNotFilePath(rctx); err != nil {
t.Errorf("path-shaped file contents must survive the guard, got: %v", err)
}
})

t.Run("an out-of-tree path is rejected toward stdin", func(t *testing.T) {
err := resolveCSVPathFromFileAlias(newCSVFileAliasRuntime("/tmp/data.csv"))
ve := requireValidation(t, err, "relative path")
if ve.Param != "--file" {
t.Errorf("param = %q, want the flag the caller actually typed", ve.Param)
}
if ve.Cause == nil {
t.Error("the underlying path error should be preserved as Cause")
}
if !strings.Contains(ve.Hint, "--csv - <") {
t.Errorf("hint should offer stdin for a file outside the tree, got: %q", ve.Hint)
}
})

t.Run("inline CSV text still passes through", func(t *testing.T) {
// --file holding literal CSV was accepted before this rule existed;
// naming nothing readable, it stays the --csv guard's business.
rctx := newCSVFileAliasRuntime("a,b\n1,2")
if err := resolveCSVPathFromFileAlias(rctx); err != nil {
t.Fatalf("resolve: %v", err)
}
if got, _ := rctx.Cmd.Flags().GetString("csv"); got != "a,b\n1,2" {
t.Errorf("--csv = %q, want the value untouched", got)
}
if rctx.InputResolvedFromSource("csv") {
t.Error("a value that was not read from a file must not be marked resolved")
}
})

t.Run("a value typed as --csv is not re-read as a path", func(t *testing.T) {
// Without the alias record the rule must not fire: --csv promises text,
// and its own guard (not this one) answers a path passed to it.
rctx := newCSVGuardRuntime("./data.csv")
if err := resolveCSVPathFromFileAlias(rctx); err != nil {
t.Fatalf("resolve: %v", err)
}
if got, _ := rctx.Cmd.Flags().GetString("csv"); got != "./data.csv" {
t.Errorf("--csv = %q, want --csv left to its guard", got)
}
})

t.Run("@file and stdin values are already contents", func(t *testing.T) {
rctx := newCSVFileAliasRuntime("./data.csv")
common.TestMarkInputResolved(rctx, "csv")
if err := resolveCSVPathFromFileAlias(rctx); err != nil {
t.Fatalf("resolve: %v", err)
}
if got, _ := rctx.Cmd.Flags().GetString("csv"); got != "./data.csv" {
t.Errorf("--csv = %q, want a resolved value left alone", got)
}
})
}

// TestResolveCSVPathFromFileAlias_UnreadablePaths pins that every unreadable
// path is answered under --file, the flag the caller typed. Handing these to
// the --csv guard instead named the wrong flag and, for a file that exists but
// cannot be opened, prescribed "pass it with @" — advice that routes through
// this very reader and fails identically.
func TestResolveCSVPathFromFileAlias_UnreadablePaths(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)

t.Run("a path-shaped value that names nothing", func(t *testing.T) {
err := resolveCSVPathFromFileAlias(newCSVFileAliasRuntime("./typo.csv"))
ve := requireValidation(t, err, "names no file under the current directory")
if ve.Param != "--file" {
t.Errorf("param = %q, want --file", ve.Param)
}
if ve.Cause == nil {
t.Error("the underlying read error should be preserved as Cause")
}
})

t.Run("a file that exists but cannot be read", func(t *testing.T) {
if err := os.WriteFile("noread.csv", []byte("a,b\n"), 0o000); err != nil {
t.Fatal(err)
}
if _, err := os.ReadFile("noread.csv"); err == nil {
t.Skip("running with rights that ignore file modes")
}
err := resolveCSVPathFromFileAlias(newCSVFileAliasRuntime("./noread.csv"))
ve := requireValidation(t, err, "cannot read file")
if ve.Param != "--file" {
t.Errorf("param = %q, want --file", ve.Param)
}
if ve.Cause == nil {
t.Error("the underlying read error should be preserved as Cause")
}
if strings.Contains(ve.Hint, "@") {
t.Errorf("@file shares this reader, so it cannot be the fix; hint was %q", ve.Hint)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})

t.Run("a directory", func(t *testing.T) {
if err := os.Mkdir("adir", 0o755); err != nil {
t.Fatal(err)
}
err := resolveCSVPathFromFileAlias(newCSVFileAliasRuntime("./adir"))
ve := requireValidation(t, err, "cannot read file")
if ve.Param != "--file" {
t.Errorf("param = %q, want --file", ve.Param)
}
if ve.Cause == nil {
t.Error("the underlying read error should be preserved as Cause")
}
})
}
Loading
Loading