951 lines
25 KiB
Go
951 lines
25 KiB
Go
package execute
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestMain points OLLIE_CFG_PATH at testdata so loadSandboxConfig uses the
|
|
// minimal test sandbox config rather than the user's ~/.config/ollie/sandbox/.
|
|
func TestMain(m *testing.M) {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
os.Setenv("OLLIE_CFG_PATH", filepath.Join(wd, "testdata"))
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// newServer returns a Server with Confirm always returning true.
|
|
func newServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
s := New(t.TempDir())
|
|
s.Confirm = func(string) bool { return true }
|
|
return s
|
|
}
|
|
|
|
// callCode is a helper that invokes execute_code via Dispatch.
|
|
func callCode(t *testing.T, s *Server, steps []map[string]any, extra ...map[string]any) (string, error) {
|
|
t.Helper()
|
|
payload := map[string]any{"steps": steps}
|
|
if len(extra) > 0 {
|
|
for k, v := range extra[0] {
|
|
payload[k] = v
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(payload)
|
|
return s.Dispatch(context.Background(), "execute_code", raw)
|
|
}
|
|
|
|
// ---- detectLanguage ----
|
|
|
|
func TestDetectLanguage(t *testing.T) {
|
|
cases := []struct{ code, want string }{
|
|
{"echo hi", "bash"},
|
|
{"#!/bin/bash\necho hi", "bash"},
|
|
{"#!/usr/bin/env python3\nprint(1)", "python3"},
|
|
{"#!/usr/bin/python\nprint(1)", "python3"},
|
|
{"#!/usr/bin/perl\nprint 1", "perl"},
|
|
{"#!/usr/bin/awk -f\n{print}", "awk"},
|
|
{"#!/usr/bin/env gawk\n{print}", "awk"},
|
|
{"#!/usr/bin/sed -f\ns/a/b/", "sed"},
|
|
{"#!/usr/bin/ed\n,p", "ed"},
|
|
{"#!/usr/bin/env jq\n.", "jq"},
|
|
{"#!/usr/bin/env lua\nprint(1)", "lua"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := detectLanguage(c.code); got != c.want {
|
|
t.Errorf("detectLanguage(%q) = %q, want %q", c.code[:min(20, len(c.code))], got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ---- ansiCEscape ----
|
|
|
|
func TestAnsiCEscape(t *testing.T) {
|
|
got := ansiCEscape("a\\b'c\nd\te")
|
|
want := `a\\b\'c\nd\te`
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// ---- injectArgs ----
|
|
|
|
func TestInjectArgsBash(t *testing.T) {
|
|
out := injectArgs("bash", "myscript", []string{"hello", "world"}, "echo $1 $2")
|
|
if !strings.HasPrefix(out, "set -- ") {
|
|
t.Errorf("bash inject should start with 'set --', got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "echo $1 $2") {
|
|
t.Errorf("bash inject should contain original code")
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsPython(t *testing.T) {
|
|
out := injectArgs("python3", "s", []string{"a"}, "print(sys.argv)")
|
|
if !strings.HasPrefix(out, "import sys\n") {
|
|
t.Errorf("python inject should start with 'import sys', got: %q", out)
|
|
}
|
|
}
|
|
|
|
// ---- ToolsPath / PluginsPath ----
|
|
|
|
func TestToolsPathEnv(t *testing.T) {
|
|
t.Setenv("OLLIE_TOOLS_PATH", "/custom/tools:/other")
|
|
if got := ToolsPath(); got != "/custom/tools" {
|
|
t.Errorf("got %q, want /custom/tools", got)
|
|
}
|
|
}
|
|
|
|
func TestPluginsPathEnv(t *testing.T) {
|
|
t.Setenv("OLLIE_PLUGINS_PATH", "/custom/plugins:/other")
|
|
if got := PluginsPath(); got != "/custom/plugins" {
|
|
t.Errorf("got %q, want /custom/plugins", got)
|
|
}
|
|
}
|
|
|
|
// ---- ReadTool ----
|
|
|
|
func TestReadTool(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("OLLIE_TOOLS_PATH", dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "mytool"), []byte("#!/bin/bash\necho ok"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, err := ReadTool("mytool")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(code, "echo ok") {
|
|
t.Errorf("unexpected content: %q", code)
|
|
}
|
|
}
|
|
|
|
func TestReadToolNotFound(t *testing.T) {
|
|
t.Setenv("OLLIE_TOOLS_PATH", t.TempDir())
|
|
_, err := ReadTool("nonexistent")
|
|
if err == nil || !strings.Contains(err.Error(), "tool not found") {
|
|
t.Errorf("expected 'tool not found' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadToolInvalidName(t *testing.T) {
|
|
for _, name := range []string{"../etc/passwd", "foo/bar"} {
|
|
_, err := ReadTool(name)
|
|
if err == nil {
|
|
t.Errorf("expected error for name %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- ValidateCode ----
|
|
|
|
func TestValidateCodeDangerous(t *testing.T) {
|
|
s := newServer(t)
|
|
cases := []struct{ code, lang string }{
|
|
{"sudo rm -rf /", "bash"},
|
|
{"rm -rf /home", "bash"},
|
|
{"mkfs /dev/sda", "bash"},
|
|
{"dd if=/dev/zero of=/dev/sda", "bash"},
|
|
{"shutil.rmtree('/')", "python3"},
|
|
}
|
|
for _, c := range cases {
|
|
if err := s.ValidateCode(c.code, c.lang); err == nil {
|
|
t.Errorf("expected dangerous pattern error for %q (%s)", c.code, c.lang)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateCodeSafe(t *testing.T) {
|
|
s := newServer(t)
|
|
if err := s.ValidateCode("echo hello", "bash"); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---- allowed ----
|
|
|
|
func TestAllowedWithConfirm(t *testing.T) {
|
|
s := New(t.TempDir())
|
|
s.Confirm = func(string) bool { return true }
|
|
if !s.allowed("execute_code", "echo hi") {
|
|
t.Error("expected allowed=true when Confirm returns true")
|
|
}
|
|
}
|
|
|
|
func TestAllowedDeniedNoConfirm(t *testing.T) {
|
|
s := New(t.TempDir())
|
|
if s.allowed("execute_code", "echo hi") {
|
|
t.Error("expected allowed=false when Confirm is nil")
|
|
}
|
|
}
|
|
|
|
func TestAllowedTrustedTool(t *testing.T) {
|
|
s := New(t.TempDir())
|
|
s.SetTrustedTools([]string{"execute_code"})
|
|
if !s.allowed("execute_code", "anything") {
|
|
t.Error("expected trusted tool to be allowed without Confirm")
|
|
}
|
|
}
|
|
|
|
// ---- Execute (integration, requires bash) ----
|
|
|
|
func TestExecuteSimpleBash(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.Execute(context.Background(), "echo hello", "bash", 10, "default", true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "hello" {
|
|
t.Errorf("got %q, want %q", out, "hello")
|
|
}
|
|
}
|
|
|
|
func TestExecuteStdin(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.executeWithStdin(context.Background(), "cat", "bash", 10, "default", true, "piped input")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "piped input" {
|
|
t.Errorf("got %q, want %q", out, "piped input")
|
|
}
|
|
}
|
|
|
|
func TestExecuteTimeout(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.Execute(context.Background(), "sleep 10", "bash", 1, "default", true)
|
|
if err == nil || !strings.Contains(err.Error(), "timeout") {
|
|
t.Errorf("expected timeout error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteNonZeroExit(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.Execute(context.Background(), "exit 1", "bash", 10, "default", true)
|
|
if err == nil {
|
|
t.Error("expected error for non-zero exit")
|
|
}
|
|
}
|
|
|
|
func TestExecuteUnsupportedLanguage(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.Execute(context.Background(), "code", "cobol", 10, "default", true)
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported language") {
|
|
t.Errorf("expected unsupported language error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// ---- Dispatch / execute_code ----
|
|
|
|
func TestDispatchUnknownTool(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.Dispatch(context.Background(), "unknown_tool", json.RawMessage(`{}`))
|
|
if err == nil {
|
|
t.Error("expected error for unknown tool")
|
|
}
|
|
}
|
|
|
|
func TestDispatchNoSteps(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := callCode(t, s, []map[string]any{})
|
|
if err == nil || !strings.Contains(err.Error(), "at least one step") {
|
|
t.Errorf("expected 'at least one step' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDispatchDenied(t *testing.T) {
|
|
s := New(t.TempDir()) // no Confirm, not trusted → denied
|
|
_, err := callCode(t, s, []map[string]any{{"code": "echo hi"}})
|
|
if err == nil || !strings.Contains(err.Error(), "denied") {
|
|
t.Errorf("expected denied error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDispatchSingleStep(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := callCode(t, s, []map[string]any{{"code": "echo single"}})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(out, "single") {
|
|
t.Errorf("got %q, want output containing 'single'", out)
|
|
}
|
|
}
|
|
|
|
func TestDispatchPipeline(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := callCode(t, s, []map[string]any{
|
|
{"code": "printf 'a\\nb\\nc'"},
|
|
{"code": "grep b"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "b" {
|
|
t.Errorf("got %q, want %q", out, "b")
|
|
}
|
|
}
|
|
|
|
func TestDispatchParallel(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := callCode(t, s, []map[string]any{
|
|
{"parallel": []map[string]any{
|
|
{"code": "echo A"},
|
|
{"code": "echo B"},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(out, "A") || !strings.Contains(out, "B") {
|
|
t.Errorf("parallel output missing A or B: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestDispatchToolStep(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("OLLIE_TOOLS_PATH", dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "greet"), []byte("#!/bin/bash\necho hello-from-tool"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := newServer(t)
|
|
out, err := callCode(t, s, []map[string]any{{"tool": "greet"}})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(out, "hello-from-tool") {
|
|
t.Errorf("got %q, want output containing 'hello-from-tool'", out)
|
|
}
|
|
}
|
|
|
|
// ---- limitedWriter ----
|
|
|
|
func TestLimitedWriter(t *testing.T) {
|
|
var buf strings.Builder
|
|
lw := &limitedWriter{w: &buf, limit: 5}
|
|
lw.Write([]byte("hello world"))
|
|
if buf.String() != "hello" {
|
|
t.Errorf("got %q, want %q", buf.String(), "hello")
|
|
}
|
|
if !lw.truncated {
|
|
t.Error("expected truncated=true")
|
|
}
|
|
}
|
|
|
|
// ---- rate limiting ----
|
|
|
|
func TestRateLimitBlocks(t *testing.T) {
|
|
s := newServer(t)
|
|
// Trigger maxFailures validation failures.
|
|
for i := 0; i < maxFailures; i++ {
|
|
s.recordValidationFailure()
|
|
}
|
|
if err := s.checkRateLimit(); err == nil {
|
|
t.Error("expected rate limit error after max failures")
|
|
}
|
|
}
|
|
|
|
// ---- SetEnv / SetCWD ----
|
|
|
|
func TestSetEnvInjected(t *testing.T) {
|
|
s := newServer(t)
|
|
s.SetEnv("MY_TEST_VAR", "injected_value")
|
|
out, err := s.Execute(context.Background(), "echo $MY_TEST_VAR", "bash", 10, "default", true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "injected_value" {
|
|
t.Errorf("got %q, want %q", out, "injected_value")
|
|
}
|
|
}
|
|
|
|
func TestSetCWD(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := newServer(t)
|
|
s.SetCWD(dir)
|
|
out, err := s.Execute(context.Background(), "pwd", "bash", 10, "default", true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// TempDir may use symlinks; compare base names.
|
|
if !strings.Contains(out, filepath.Base(dir)) {
|
|
t.Errorf("pwd output %q doesn't contain expected dir %q", out, dir)
|
|
}
|
|
}
|
|
|
|
// ---- Server contract (ListTools / CallTool / Close) ----
|
|
|
|
func TestServerContract(t *testing.T) {
|
|
s := newServer(t)
|
|
tools, err := s.ListTools()
|
|
if err != nil {
|
|
t.Fatalf("ListTools: %v", err)
|
|
}
|
|
if len(tools) == 0 {
|
|
t.Fatal("ListTools returned no tools")
|
|
}
|
|
for _, ti := range tools {
|
|
if ti.Name == "" {
|
|
t.Error("tool has empty Name")
|
|
}
|
|
if ti.Description == "" {
|
|
t.Errorf("tool %q has empty Description", ti.Name)
|
|
}
|
|
if len(ti.InputSchema) == 0 {
|
|
t.Errorf("tool %q has empty InputSchema", ti.Name)
|
|
}
|
|
}
|
|
// CallTool success path
|
|
args, _ := json.Marshal(map[string]any{"steps": []map[string]any{{"code": "echo contract"}}})
|
|
res, err := s.CallTool(context.Background(), "execute_code", args)
|
|
if err != nil {
|
|
t.Fatalf("CallTool: %v", err)
|
|
}
|
|
if !strings.Contains(string(res), "contract") {
|
|
t.Errorf("CallTool result missing expected output: %s", res)
|
|
}
|
|
// CallTool error path (unknown tool)
|
|
res, err = s.CallTool(context.Background(), "bogus", json.RawMessage(`{}`))
|
|
if err != nil {
|
|
t.Fatalf("CallTool error path should marshal, not return error: %v", err)
|
|
}
|
|
if !strings.Contains(string(res), "isError") {
|
|
t.Errorf("CallTool error path should contain isError: %s", res)
|
|
}
|
|
}
|
|
|
|
// ---- Decl factory ----
|
|
|
|
func TestDecl(t *testing.T) {
|
|
factory := Decl(t.TempDir())
|
|
srv := factory()
|
|
if srv == nil {
|
|
t.Fatal("Decl factory returned nil")
|
|
}
|
|
}
|
|
|
|
// ---- executeElevated (plugin not found) ----
|
|
|
|
func TestExecuteElevatedNoPlugin(t *testing.T) {
|
|
s := newServer(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", t.TempDir()) // empty dir, no elevate script
|
|
_, err := s.executeElevated(context.Background(), "echo hi", t.TempDir(), 10)
|
|
if err == nil || !strings.Contains(err.Error(), "elevation not available") {
|
|
t.Errorf("expected 'elevation not available' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// ---- injectArgs: all languages ----
|
|
|
|
func TestInjectArgsPerl(t *testing.T) {
|
|
got := injectArgs("perl", "test.pl", []string{"a", "b"}, "print @ARGV;")
|
|
if !strings.Contains(got, "@ARGV") || !strings.Contains(got, "'a'") {
|
|
t.Errorf("perl inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsAwk(t *testing.T) {
|
|
got := injectArgs("awk", "test.awk", []string{"file.txt"}, "{print $1}")
|
|
if !strings.Contains(got, "gawk") || !strings.Contains(got, "file.txt") {
|
|
t.Errorf("awk inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsSed(t *testing.T) {
|
|
got := injectArgs("sed", "test.sed", []string{"file.txt"}, "s/a/b/g")
|
|
if !strings.Contains(got, "sed -e") || !strings.Contains(got, "file.txt") {
|
|
t.Errorf("sed inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsEd(t *testing.T) {
|
|
got := injectArgs("ed", "test.ed", []string{"file.txt"}, ",p")
|
|
if !strings.Contains(got, "ed -s") || !strings.Contains(got, "file.txt") {
|
|
t.Errorf("ed inject = %q", got)
|
|
}
|
|
// No args
|
|
got2 := injectArgs("ed", "test.ed", nil, ",p")
|
|
if strings.Contains(got2, "file") {
|
|
t.Errorf("ed inject no args = %q", got2)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsJq(t *testing.T) {
|
|
got := injectArgs("jq", "test.jq", []string{"data.json"}, ".name")
|
|
if !strings.Contains(got, "jq") || !strings.Contains(got, "data.json") {
|
|
t.Errorf("jq inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsExpect(t *testing.T) {
|
|
got := injectArgs("expect", "test.exp", nil, "spawn ls")
|
|
if !strings.Contains(got, "expect -") {
|
|
t.Errorf("expect inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsBc(t *testing.T) {
|
|
got := injectArgs("bc", "test.bc", nil, "2+2")
|
|
if !strings.Contains(got, "bc -ql") {
|
|
t.Errorf("bc inject = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInjectArgsLua(t *testing.T) {
|
|
got := injectArgs("lua", "test.lua", []string{"x", "y"}, "print(arg[1])")
|
|
if !strings.Contains(got, "arg={") || !strings.Contains(got, "\"x\"") {
|
|
t.Errorf("lua inject = %q", got)
|
|
}
|
|
}
|
|
|
|
// ---- ansiCEscape: \r branch ----
|
|
|
|
func TestAnsiCEscapeCarriageReturn(t *testing.T) {
|
|
got := ansiCEscape("a\rb")
|
|
if got != `a\rb` {
|
|
t.Errorf("ansiCEscape(a\\rb) = %q; want a\\rb", got)
|
|
}
|
|
}
|
|
|
|
// ---- resolveCodeStep ----
|
|
|
|
func TestResolveCodeStepToolError(t *testing.T) {
|
|
_, _, _, err := resolveCodeStep(CodeStep{Tool: "nonexistent-tool-xyz"})
|
|
if err == nil {
|
|
t.Error("expected error for missing tool")
|
|
}
|
|
}
|
|
|
|
func TestResolveCodeStepToolWithArgs(t *testing.T) {
|
|
// Create a tool that looks like awk
|
|
dir := t.TempDir()
|
|
t.Setenv("OLLIE_TOOLS_PATH", dir)
|
|
os.WriteFile(filepath.Join(dir, "mytool"), []byte("#!/usr/bin/awk -f\n{print $1}"), 0755)
|
|
|
|
code, lang, trusted, err := resolveCodeStep(CodeStep{Tool: "mytool", Args: []string{"file.txt"}})
|
|
if err != nil {
|
|
t.Fatalf("resolveCodeStep: %v", err)
|
|
}
|
|
if !trusted {
|
|
t.Error("tool should be trusted")
|
|
}
|
|
// awk with args switches to bash
|
|
if lang != "bash" {
|
|
t.Errorf("lang = %q; want bash (awk with args)", lang)
|
|
}
|
|
if !strings.Contains(code, "gawk") {
|
|
t.Errorf("code = %q; want gawk wrapper", code)
|
|
}
|
|
}
|
|
|
|
func TestResolveCodeStepInlineDefault(t *testing.T) {
|
|
code, lang, trusted, err := resolveCodeStep(CodeStep{Code: "echo hi"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if lang != "bash" || trusted || code != "echo hi" {
|
|
t.Errorf("got lang=%q trusted=%v code=%q", lang, trusted, code)
|
|
}
|
|
}
|
|
|
|
// ---- runStage ----
|
|
|
|
func TestRunStageEmptyStage(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.runStage(context.Background(), 0, CodeStep{}, 5, "default", "")
|
|
if err == nil || !strings.Contains(err.Error(), "requires code, tool, or parallel") {
|
|
t.Errorf("expected empty stage error; got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStageParallelError(t *testing.T) {
|
|
s := newServer(t)
|
|
stage := CodeStep{
|
|
Parallel: []CodeStep{
|
|
{Code: "echo ok"},
|
|
{Code: "exit 1"},
|
|
},
|
|
}
|
|
_, err := s.runStage(context.Background(), 0, stage, 5, "default", "")
|
|
if err == nil {
|
|
t.Error("expected error from parallel stage with exit 1")
|
|
}
|
|
}
|
|
|
|
func TestRunStageParallelResolveError(t *testing.T) {
|
|
s := newServer(t)
|
|
stage := CodeStep{
|
|
Parallel: []CodeStep{
|
|
{Tool: "nonexistent-tool-xyz"},
|
|
},
|
|
}
|
|
_, err := s.runStage(context.Background(), 0, stage, 5, "default", "")
|
|
if err == nil {
|
|
t.Error("expected error from parallel stage with bad tool")
|
|
}
|
|
}
|
|
|
|
// ---- dispatchExecuteCode ----
|
|
|
|
func TestDispatchBadArgs(t *testing.T) {
|
|
s := newServer(t)
|
|
_, err := s.Dispatch(context.Background(), "execute_code", json.RawMessage(`{invalid`))
|
|
if err == nil {
|
|
t.Error("expected error for bad JSON")
|
|
}
|
|
}
|
|
|
|
func TestDispatchPipelineError(t *testing.T) {
|
|
s := newServer(t)
|
|
args := `{"steps":[{"code":"echo hello"},{"code":"exit 1"}]}`
|
|
_, err := s.Dispatch(context.Background(), "execute_code", json.RawMessage(args))
|
|
if err == nil {
|
|
t.Error("expected error from pipeline with failing step")
|
|
}
|
|
}
|
|
|
|
// ---- limitedWriter ----
|
|
|
|
func TestLimitedWriterAlreadyAtLimit(t *testing.T) {
|
|
var buf strings.Builder
|
|
lw := &limitedWriter{w: &buf, limit: 5, written: 5}
|
|
n, err := lw.Write([]byte("overflow"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 8 {
|
|
t.Errorf("n = %d; want 8", n)
|
|
}
|
|
if !lw.truncated {
|
|
t.Error("should be truncated")
|
|
}
|
|
if buf.Len() != 0 {
|
|
t.Errorf("buf = %q; want empty", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestLimitedWriterError(t *testing.T) {
|
|
lw := &limitedWriter{w: &errWriter{}, limit: 100}
|
|
_, err := lw.Write([]byte("data"))
|
|
if err == nil {
|
|
t.Error("expected write error")
|
|
}
|
|
}
|
|
|
|
type errWriter struct{}
|
|
|
|
func (errWriter) Write([]byte) (int, error) { return 0, os.ErrClosed }
|
|
|
|
// ---- ToolsPath / PluginsPath defaults ----
|
|
|
|
func TestToolsPathDefault(t *testing.T) {
|
|
t.Setenv("OLLIE_TOOLS_PATH", "")
|
|
got := ToolsPath()
|
|
if !strings.HasSuffix(got, "/tools") {
|
|
t.Errorf("ToolsPath() = %q; want suffix /tools", got)
|
|
}
|
|
}
|
|
|
|
func TestPluginsPathDefault(t *testing.T) {
|
|
t.Setenv("OLLIE_PLUGINS_PATH", "")
|
|
got := PluginsPath()
|
|
if !strings.HasSuffix(got, "/scripts/x") {
|
|
t.Errorf("PluginsPath() = %q; want suffix /scripts/x", got)
|
|
}
|
|
}
|
|
|
|
func TestToolsPathColonSeparated(t *testing.T) {
|
|
t.Setenv("OLLIE_TOOLS_PATH", "/first:/second")
|
|
if got := ToolsPath(); got != "/first" {
|
|
t.Errorf("ToolsPath() = %q; want /first", got)
|
|
}
|
|
}
|
|
|
|
func TestPluginsPathColonSeparated(t *testing.T) {
|
|
t.Setenv("OLLIE_PLUGINS_PATH", "/first:/second")
|
|
if got := PluginsPath(); got != "/first" {
|
|
t.Errorf("PluginsPath() = %q; want /first", got)
|
|
}
|
|
}
|
|
|
|
// ---- ReadTool: permission error ----
|
|
|
|
func TestReadToolPermissionError(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("OLLIE_TOOLS_PATH", dir)
|
|
path := filepath.Join(dir, "noperm")
|
|
os.WriteFile(path, []byte("#!/bin/sh"), 0000)
|
|
// On some systems root can still read; skip if so
|
|
if _, err := os.ReadFile(path); err == nil {
|
|
t.Skip("running as root, cannot test permission error")
|
|
}
|
|
_, err := ReadTool("noperm")
|
|
if err == nil {
|
|
t.Error("expected permission error")
|
|
}
|
|
}
|
|
|
|
// ---- loadSandboxConfig ----
|
|
|
|
func TestLoadSandboxConfigDefault(t *testing.T) {
|
|
cfg, err := loadSandboxConfig("")
|
|
if err != nil {
|
|
t.Fatalf("loadSandboxConfig default: %v", err)
|
|
}
|
|
if cfg == nil {
|
|
t.Error("expected non-nil config")
|
|
}
|
|
}
|
|
|
|
func TestLoadSandboxConfigNotFound(t *testing.T) {
|
|
_, err := loadSandboxConfig("nonexistent-sandbox-xyz")
|
|
if err == nil {
|
|
t.Error("expected error for missing sandbox config")
|
|
}
|
|
}
|
|
|
|
// ---- detectLanguage: remaining shebangs ----
|
|
|
|
func TestDetectLanguageEnvShebang(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
shebang, want string
|
|
}{
|
|
{"#!/usr/bin/env perl\n", "perl"},
|
|
{"#!/usr/bin/env lua\n", "lua"},
|
|
{"#!/usr/bin/env lua5.4\n", "lua"},
|
|
{"#!/usr/bin/env gawk\n", "awk"},
|
|
{"#!/usr/bin/env gsed\n", "sed"},
|
|
{"#!/usr/bin/env jq\n", "jq"},
|
|
{"#!/usr/bin/env expect\n", "expect"},
|
|
{"#!/usr/bin/env bc\n", "bc"},
|
|
{"#!/usr/bin/env ed\n", "ed"},
|
|
{"#!\n", "bash"},
|
|
} {
|
|
got := detectLanguage(tc.shebang + "code")
|
|
if got != tc.want {
|
|
t.Errorf("detectLanguage(%q) = %q; want %q", tc.shebang, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- executeWithStdin: language branches ----
|
|
|
|
func TestExecutePython(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.Execute(context.Background(), "print('hello')", "python3", 5, "default", false)
|
|
if err != nil {
|
|
t.Fatalf("python3: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "hello" {
|
|
t.Errorf("python3 output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecutePerl(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.Execute(context.Background(), "print 42", "perl", 5, "default", false)
|
|
if err != nil {
|
|
t.Fatalf("perl: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "42" {
|
|
t.Errorf("perl output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteBc(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.Execute(context.Background(), "2+3\n", "bc", 5, "default", false)
|
|
if err != nil {
|
|
t.Fatalf("bc: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "5" {
|
|
t.Errorf("bc output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteJq(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.executeWithStdin(context.Background(), ".x", "jq", 5, "default", false, `{"x":1}`)
|
|
if err != nil {
|
|
t.Fatalf("jq: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "1" {
|
|
t.Errorf("jq output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteAwk(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.executeWithStdin(context.Background(), "{print $1}", "awk", 5, "default", false, "hello world")
|
|
if err != nil {
|
|
t.Fatalf("awk: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "hello" {
|
|
t.Errorf("awk output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSed(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.executeWithStdin(context.Background(), "s/a/b/", "sed", 5, "default", false, "abc")
|
|
if err != nil {
|
|
t.Fatalf("sed: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "bbc" {
|
|
t.Errorf("sed output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteEd(t *testing.T) {
|
|
s := newServer(t)
|
|
s.SetCWD(t.TempDir())
|
|
f := filepath.Join(s.cwd, "test.txt")
|
|
os.WriteFile(f, []byte("hello\n"), 0644)
|
|
out, err := s.Execute(context.Background(), ",p\nq", "ed", 5, "default", false)
|
|
// ed without a file arg just reads commands; may error, that's fine
|
|
_ = out
|
|
_ = err
|
|
}
|
|
|
|
func TestExecuteLua(t *testing.T) {
|
|
s := newServer(t)
|
|
out, err := s.Execute(context.Background(), "print(1+1)", "lua", 5, "default", false)
|
|
if err != nil {
|
|
t.Skipf("lua not available: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "2" {
|
|
t.Errorf("lua output = %q", out)
|
|
}
|
|
}
|
|
|
|
// ---- dispatchExecuteCode: multi-step pipeline error mid-pipeline ----
|
|
|
|
func TestDispatchPipelineMidError(t *testing.T) {
|
|
s := newServer(t)
|
|
args := `{"steps":[{"code":"echo hello"},{"code":"exit 1"},{"code":"cat"}]}`
|
|
result, err := s.Dispatch(context.Background(), "execute_code", json.RawMessage(args))
|
|
// The error should be wrapped with step index
|
|
_ = result
|
|
if err == nil {
|
|
t.Error("expected pipeline error")
|
|
}
|
|
}
|
|
|
|
// ---- executeElevated ----
|
|
|
|
func fakeElevateDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
// Fake elevate script: runs the command after "--"
|
|
script := "#!/bin/sh\nshift # skip --\neval \"$@\"\n"
|
|
os.WriteFile(filepath.Join(dir, "elevate"), []byte(script), 0755)
|
|
return dir
|
|
}
|
|
|
|
func TestExecuteElevatedSuccess(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
|
|
out, err := s.executeElevated(context.Background(), "echo elevated", t.TempDir(), 5)
|
|
if err != nil {
|
|
t.Fatalf("executeElevated: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "elevated" {
|
|
t.Errorf("output = %q; want elevated", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteElevatedStderr(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
|
|
out, err := s.executeElevated(context.Background(), "echo out && echo err >&2", t.TempDir(), 5)
|
|
if err != nil {
|
|
t.Fatalf("executeElevated: %v", err)
|
|
}
|
|
if !strings.Contains(out, "out") || !strings.Contains(out, "err") {
|
|
t.Errorf("output = %q; want both stdout and stderr", out)
|
|
}
|
|
}
|
|
|
|
func TestExecuteElevatedNonZeroExit(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
|
|
_, err := s.executeElevated(context.Background(), "exit 42", t.TempDir(), 5)
|
|
if err == nil {
|
|
t.Error("expected error for non-zero exit")
|
|
}
|
|
if !strings.Contains(err.Error(), "exit 42") {
|
|
t.Errorf("error = %q; want to contain 'exit 42'", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStageElevated(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
s.SetCWD(t.TempDir())
|
|
|
|
out, err := s.runStage(context.Background(), 0, CodeStep{Code: "echo elev", Elevated: true}, 5, "default", "")
|
|
if err != nil {
|
|
t.Fatalf("runStage elevated: %v", err)
|
|
}
|
|
if !strings.Contains(out, "elev") {
|
|
t.Errorf("output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunStageParallelElevated(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
s.SetCWD(t.TempDir())
|
|
|
|
stage := CodeStep{
|
|
Parallel: []CodeStep{
|
|
{Code: "echo a", Elevated: true},
|
|
{Code: "echo b", Elevated: true},
|
|
},
|
|
}
|
|
out, err := s.runStage(context.Background(), 0, stage, 5, "default", "")
|
|
if err != nil {
|
|
t.Fatalf("parallel elevated: %v", err)
|
|
}
|
|
if !strings.Contains(out, "a") || !strings.Contains(out, "b") {
|
|
t.Errorf("output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestDispatchSingleElevated(t *testing.T) {
|
|
s := newServer(t)
|
|
dir := fakeElevateDir(t)
|
|
t.Setenv("OLLIE_PLUGINS_PATH", dir)
|
|
s.SetCWD(t.TempDir())
|
|
|
|
args := `{"steps":[{"code":"echo elev","elevated":true}]}`
|
|
result, err := s.Dispatch(context.Background(), "execute_code", json.RawMessage(args))
|
|
if err != nil {
|
|
t.Fatalf("dispatch elevated: %v", err)
|
|
}
|
|
if !strings.Contains(string(result), "elev") {
|
|
t.Errorf("result = %q", result)
|
|
}
|
|
}
|