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
27 changes: 26 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ LAUNCHER_IMAGE := $(REGISTRY):launcher

.PHONY: cli sandbox push-sandbox cli-launcher launcher push-launcher \
test-unit test test-podman test-ocp \
test-go-podman test-go-ocp test-all clean help
test-go-podman test-go-ocp test-all validate clean help

## ── CLI ──────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -81,6 +81,31 @@ test-all: cli sandbox push-launcher
./test/test-flow.sh all --full
./test/test-flow.sh all --full --go

## Full validation: unit tests + bats (both paths) + integration (all 4 combos)
## Run this before every commit.
validate: cli sandbox push-launcher
@echo "=== Unit tests ==="
CGO_ENABLED=0 go test ./...
cd sandbox/launcher && go test ./...
@echo ""
@echo "=== Bats (Python) ==="
bats test/preflight.bats
@echo ""
@echo "=== Bats (Go) ==="
USE_GO=true bats test/preflight.bats
@echo ""
@echo "=== Integration: bash + podman ==="
./test/test-flow.sh podman --full
@echo ""
@echo "=== Integration: Go + podman ==="
./test/test-flow.sh podman --full --go
@echo ""
@echo "=== Integration: bash + OCP ==="
./test/test-flow.sh ocp --full
@echo ""
@echo "=== Integration: Go + OCP ==="
./test/test-flow.sh ocp --full --go

## ── Convenience targets ───────────────────────────────────────────────

## Clean built binaries
Expand Down
8 changes: 4 additions & 4 deletions cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/robbycochran/harness-openshell/internal/profile"
"github.com/robbycochran/harness-openshell/internal/runner"
"github.com/robbycochran/harness-openshell/internal/status"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -119,14 +120,13 @@ func newLocal(opts newLocalOpts) error {
fmt.Printf(" Image: %s\n", cfg.Image)

// 4. Validate providers against profile
fmt.Println()
fmt.Println("=== Providers ===")
status.Section("Providers")
registered, missing := profile.ValidateProviders(cfg.Providers, gw)
for _, name := range registered {
fmt.Printf(" ✓ %s: attached\n", name)
status.OKf("%s: attached", name)
}
for _, name := range missing {
fmt.Printf(" ✗ %s: not registered (skipping)\n", name)
status.Failf("%s: not registered (skipping)", name)
}
if len(missing) > 0 && len(registered) == 0 {
fmt.Println()
Expand Down
4 changes: 4 additions & 0 deletions cmd/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func (m *mockGW) SandboxDelete(name string) error {
m.deletedNames = append(m.deletedNames, name)
return nil
}
func (m *mockGW) CLIVersion() string { return "openshell v0.0.55" }
func (m *mockGW) CLIPath() string { return "/usr/bin/openshell" }
func (m *mockGW) InferenceModel() string { return "" }
func (m *mockGW) ActiveGateway() string { return "" }
func (m *mockGW) SandboxConnect(string) error { return nil }
func (m *mockGW) SandboxUpload(string, string, string) error { return nil }
func (m *mockGW) SandboxExec(string, ...string) error { return nil }
Expand Down
28 changes: 22 additions & 6 deletions cmd/preflight.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
package cmd

import (
"github.com/robbycochran/harness-openshell/internal/runner"
"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/robbycochran/harness-openshell/internal/preflight"
"github.com/spf13/cobra"
)

func NewPreflightCmd(harnessDir string) *cobra.Command {
return &cobra.Command{
Use: "preflight [--strict]",
func NewPreflightCmd(harnessDir, cli string) *cobra.Command {
var strict bool

cmd := &cobra.Command{
Use: "preflight",
Short: "Check environment prerequisites",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runner.RunScript(harnessDir, "preflight.sh", args...)
// Support subcommands: available, names
if len(args) > 0 {
switch args[0] {
case "available":
return preflight.RunAvailable(harnessDir)
case "names":
return preflight.RunNames(harnessDir)
}
}
gw := gateway.NewCLI(cli)
return preflight.RunCheck(harnessDir, gw, strict)
},
}

cmd.Flags().BoolVar(&strict, "strict", false, "Exit 1 if required providers missing")

return cmd
}
50 changes: 50 additions & 0 deletions internal/gateway/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"io"
"os"
"os/exec"
"regexp"
"strings"
"syscall"
)

var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`)

// CLI implements Gateway by shelling out to the openshell binary.
type CLI struct {
bin string // path or name of the openshell binary
Expand All @@ -17,10 +20,57 @@ func NewCLI(bin string) *CLI {
return &CLI{bin: bin}
}

func (c *CLI) CLIVersion() string {
out, err := c.output("--version")
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

func (c *CLI) CLIPath() string {
path, err := exec.LookPath(c.bin)
if err != nil {
return ""
}
return path
}

func (c *CLI) InferenceGet() error {
return c.silent("inference", "get")
}

func (c *CLI) InferenceModel() string {
out, err := c.output("inference", "get")
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
cleaned := ansiRE.ReplaceAllString(line, "")
if strings.Contains(cleaned, "Model:") {
return strings.TrimSpace(strings.SplitN(cleaned, "Model:", 2)[1])
}
}
return ""
}

func (c *CLI) ActiveGateway() string {
out, err := c.output("gateway", "list")
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
cleaned := ansiRE.ReplaceAllString(line, "")
if strings.HasPrefix(cleaned, "*") {
fields := strings.Fields(cleaned)
if len(fields) > 1 {
return fields[1]
}
}
}
return ""
}

func (c *CLI) ProviderGet(name string) error {
return c.silent("provider", "get", name)
}
Expand Down
12 changes: 12 additions & 0 deletions internal/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@ package gateway
// shells out to the openshell binary; a future gRPC implementation will call
// the gateway API directly.
type Gateway interface {
// CLIVersion returns the openshell CLI version string, or empty if not found.
CLIVersion() string

// CLIPath returns the path to the openshell CLI binary, or empty if not found.
CLIPath() string

// InferenceGet checks if the gateway is active and reachable.
InferenceGet() error

// InferenceModel returns the configured inference model name, or empty.
InferenceModel() string

// ActiveGateway returns the name of the currently selected gateway, or empty.
ActiveGateway() string

// ProviderGet checks if a provider is registered. Returns nil if it exists.
ProviderGet(name string) error

Expand Down
Loading