Skip to content
Open
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
1 change: 1 addition & 0 deletions internal/agentrepo/prose.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func proseCommand(code *int) *cobra.Command {
*code = p.execute(args)
},
})
group.AddCommand(proseCommandPreflight(code))
return group
}

Expand Down
53 changes: 53 additions & 0 deletions internal/agentrepo/prose_preflight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Tony Burns

package agentrepo

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"
)

func proseCommandPreflight(code *int) *cobra.Command {
return &cobra.Command{
Use: "preflight TARGET [LINT COMMAND...]",
Short: "Report whether a prose run can start",
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
p := proseState{out: cmd.OutOrStdout(), stderr: cmd.ErrOrStderr(), run: diagnosisRunner(cmd.Context())}
*code = p.preflight(args)
},
}
}

// preflight reports the three argument-handling BLOCKED outcomes that precede any probe.
func (p *proseState) preflight(args []string) int {
if p.checkout() != "" {
fmt.Fprintln(p.out, "verdict: BLOCKED (not a git repository)")
return 0
}
target := ""
if len(args) > 0 {
target = args[0]
}
fmt.Fprintf(p.out, "== fix-prose preflight ==\nroot: %s\n", p.root)
if target == "" {
fmt.Fprintln(p.out, "target: (none given)")
fmt.Fprintln(p.out, "verdict: BLOCKED (no target in the arguments; ask which file)")
return 0
}
// The shell changes to the checkout root first. A relative target resolves there.
rel, _ := strings.CutPrefix(target, p.root+string(filepath.Separator))
if info, err := os.Stat(p.path(rel)); err != nil || !info.Mode().IsRegular() {
fmt.Fprintf(p.out, "target: %s (missing)\n", rel)
fmt.Fprintln(p.out, "verdict: BLOCKED (no such file)")
return 0
}
fmt.Fprintln(p.stderr, "preflight: scope probes are not ported yet")
const deferred = 2
return deferred
}
118 changes: 118 additions & 0 deletions internal/agentrepo/prose_preflight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Tony Burns

package agentrepo_test

import (
"bytes"
"errors"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"

"github.com/tbhb-dev/devtools/internal/agentrepo"
)

const proseBlockedHeader = "== fix-prose preflight ==\n"

func proseGoPreflight(t *testing.T, dir string, args ...string) proseResult {
t.Helper()
t.Chdir(dir)
var out, stderr bytes.Buffer
code := agentrepo.Run(append([]string{"prose", "preflight"}, args...), nil, &out, &stderr)
return proseResult{code, out.String(), stderr.String()}
}

func proseShellPreflight(t *testing.T, dir string, args ...string) proseResult {
t.Helper()
_, source, _, ok := runtime.Caller(0)
require.True(t, ok)
script := filepath.Join(filepath.Dir(source), "../..",
"packages/agents/source/skills/fix-prose/shared/scripts/preflight.sh")
var out, stderr bytes.Buffer
c := exec.CommandContext(t.Context(), "bash")
c.Args = append(append(c.Args, script), args...)
c.Dir, c.Stdout, c.Stderr = dir, &out, &stderr
code := 0
if err := c.Run(); err != nil {
exit, isExit := errors.AsType[*exec.ExitError](err)
require.True(t, isExit, "%v", err)
code = exit.ExitCode()
}
return proseResult{code, out.String(), stderr.String()}
}

// proseOracle compares a Go report with the executed shell report where bash is available.
func proseOracle(t *testing.T, root, dir string, got proseResult, args ...string) {
t.Helper()
if runtime.GOOS == "windows" {
return
}
want := proseShellPreflight(t, dir, args...)
require.Equal(t, want.Code, got.Code)
require.Equal(t, proseNormalize(root, want.Out), proseNormalize(root, got.Out))
require.Equal(t, proseNormalize(root, want.Err), proseNormalize(root, got.Err))
}

func TestProsePreflightOutsideRepository(t *testing.T) {
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
dir := t.TempDir()
// Stop Git's upward search at the temporary directory's parent, whatever TMPDIR holds.
t.Setenv("GIT_CEILING_DIRECTORIES", filepath.Dir(dir))
got := proseGoPreflight(t, dir, "DRAFT.md")
require.Equal(t, proseResult{0, "verdict: BLOCKED (not a git repository)\n", ""}, got)
proseOracle(t, dir, dir, got, "DRAFT.md")
}

func TestProsePreflightNoTarget(t *testing.T) {
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
f := newProseFixture(t, "bare")
got := proseGoPreflight(t, f.root)
want := proseBlockedHeader + "root: " + f.root + "\n" +
"target: (none given)\n" +
"verdict: BLOCKED (no target in the arguments; ask which file)\n"
require.Equal(t, proseResult{0, want, ""}, got)
proseOracle(t, f.root, f.root, got)
}

func TestProsePreflightMissingTarget(t *testing.T) {
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
f := newProseFixture(t, "missing")
f.write(t, "sub/keep.md", "kept\n")
for name, c := range map[string]struct{ dir, target, shown string }{
"relative": {f.root, "nope.md", "nope.md"},
"absolute": {f.root, filepath.Join(f.root, "nope.md"), "nope.md"},
"relative to root": {filepath.Join(f.root, "sub"), "keep.md", "keep.md"},
"absolute outside": {f.root, filepath.Join(filepath.Dir(f.root), "nope.md"), filepath.Join(filepath.Dir(f.root), "nope.md")},
"directory": {f.root, "sub", "sub"},
"extra lint config": {f.root, "nope.md", "nope.md"},
} {
t.Run(name, func(t *testing.T) {
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
args := []string{c.target}
if name == "extra lint config" {
args = append(args, "vale", "--no-global")
}
got := proseGoPreflight(t, c.dir, args...)
want := proseBlockedHeader + "root: " + f.root + "\n" +
"target: " + c.shown + " (missing)\n" +
"verdict: BLOCKED (no such file)\n"
require.Equal(t, proseResult{0, want, ""}, got)
proseOracle(t, f.root, c.dir, got, args...)
})
}
}

func TestProsePreflightExistingTargetIsNotPortedYet(t *testing.T) {
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
f := newProseFixture(t, "existing")
want := proseResult{
2,
proseBlockedHeader + "root: " + f.root + "\n",
"preflight: scope probes are not ported yet\n",
}
require.Equal(t, want, proseGoPreflight(t, f.root, "DRAFT.md"))
}
Loading