execute: update tests for split execute_code / call_tool / pipe tools
This commit is contained in:
parent
0903254c6c
commit
8cf82f3551
|
|
@ -28,7 +28,7 @@ func newServer(t *testing.T) *Server {
|
|||
return s
|
||||
}
|
||||
|
||||
// callCode is a helper that invokes execute_code via Dispatch.
|
||||
// callCode invokes execute_code via Dispatch with a steps array.
|
||||
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}
|
||||
|
|
@ -41,13 +41,22 @@ func callCode(t *testing.T, s *Server, steps []map[string]any, extra ...map[stri
|
|||
return s.Dispatch(context.Background(), "execute_code", raw)
|
||||
}
|
||||
|
||||
|
||||
func callPipe(t *testing.T, s *Server, pipe []map[string]any) (string, error) {
|
||||
// callTool invokes call_tool via Dispatch with a calls array.
|
||||
func callTool(t *testing.T, s *Server, calls []map[string]any) (string, error) {
|
||||
t.Helper()
|
||||
payload := map[string]any{"pipe": pipe}
|
||||
payload := map[string]any{"calls": calls}
|
||||
raw, _ := json.Marshal(payload)
|
||||
return s.Dispatch(context.Background(), "execute_code", raw)
|
||||
return s.Dispatch(context.Background(), "call_tool", raw)
|
||||
}
|
||||
|
||||
// callPipe invokes the pipe tool via Dispatch with a stages array.
|
||||
func callPipe(t *testing.T, s *Server, stages []map[string]any) (string, error) {
|
||||
t.Helper()
|
||||
payload := map[string]any{"stages": stages}
|
||||
raw, _ := json.Marshal(payload)
|
||||
return s.Dispatch(context.Background(), "pipe", raw)
|
||||
}
|
||||
|
||||
// ---- detectLanguage ----
|
||||
|
||||
func TestDetectLanguage(t *testing.T) {
|
||||
|
|
@ -268,8 +277,8 @@ func TestDispatchUnknownTool(t *testing.T) {
|
|||
func TestDispatchNoSteps(t *testing.T) {
|
||||
s := newServer(t)
|
||||
_, err := callCode(t, s, []map[string]any{})
|
||||
if err == nil || !strings.Contains(err.Error(), "steps or pipe is required") {
|
||||
t.Errorf("expected 'steps or pipe is required' error, got %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "steps is required") {
|
||||
t.Errorf("expected 'steps is required' error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,6 +301,8 @@ func TestDispatchSingleStep(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Dispatch / pipe ----
|
||||
|
||||
func TestDispatchPipeline(t *testing.T) {
|
||||
s := newServer(t)
|
||||
out, err := callPipe(t, s, []map[string]any{
|
||||
|
|
@ -306,6 +317,16 @@ func TestDispatchPipeline(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDispatchPipeNoStages(t *testing.T) {
|
||||
s := newServer(t)
|
||||
_, err := callPipe(t, s, []map[string]any{})
|
||||
if err == nil || !strings.Contains(err.Error(), "stages is required") {
|
||||
t.Errorf("expected 'stages is required' error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Dispatch / execute_code parallel ----
|
||||
|
||||
func TestDispatchParallel(t *testing.T) {
|
||||
s := newServer(t)
|
||||
out, err := callCode(t, s, []map[string]any{
|
||||
|
|
@ -322,7 +343,44 @@ func TestDispatchParallel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDispatchToolStep(t *testing.T) {
|
||||
// ---- Dispatch / call_tool ----
|
||||
|
||||
func TestDispatchCallTool(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 := callTool(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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchCallToolNoCalls(t *testing.T) {
|
||||
s := newServer(t)
|
||||
_, err := callTool(t, s, []map[string]any{})
|
||||
if err == nil || !strings.Contains(err.Error(), "calls is required") {
|
||||
t.Errorf("expected 'calls is required' error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchCallToolInlineRejected(t *testing.T) {
|
||||
s := newServer(t)
|
||||
_, err := callTool(t, s, []map[string]any{{"code": "echo hi"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "tool name is required") {
|
||||
t.Errorf("expected 'tool name is required' error for inline code in call_tool, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The old TestDispatchToolStep tested tool steps via execute_code.
|
||||
// That path still works (execute_code accepts tool steps in its steps array);
|
||||
// but call_tool is now the canonical way. Test both for coverage.
|
||||
func TestDispatchToolStepViaExecuteCode(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 {
|
||||
|
|
@ -374,670 +432,59 @@ func TestSetEnvInjected(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(out) != "injected_value" {
|
||||
t.Errorf("got %q, want %q", out, "injected_value")
|
||||
if !strings.Contains(out, "injected_value") {
|
||||
t.Errorf("got %q, want 'injected_value'", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCWD(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := newServer(t)
|
||||
dir := t.TempDir()
|
||||
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.
|
||||
// TempDir may use a symlink; compare base name only.
|
||||
if !strings.Contains(out, filepath.Base(dir)) {
|
||||
t.Errorf("pwd output %q doesn't contain expected dir %q", out, dir)
|
||||
t.Errorf("got %q, want path containing %q", out, filepath.Base(dir))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Server contract (ListTools / CallTool / Close) ----
|
||||
// ---- Strict mode ----
|
||||
|
||||
func TestServerContract(t *testing.T) {
|
||||
func TestStrictModeRejectsInline(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)
|
||||
s.Strict = true
|
||||
_, err := callCode(t, s, []map[string]any{{"code": "echo hi"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "strict") {
|
||||
t.Errorf("expected strict mode error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
func TestStrictModeAllowsTool(t *testing.T) {
|
||||
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 {
|
||||
if err := os.WriteFile(filepath.Join(dir, "safe"), []byte("#!/bin/bash\necho safe"), 0755); 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 := `{"pipe":[{"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"))
|
||||
s.Strict = true
|
||||
out, err := callCode(t, s, []map[string]any{{"tool": "safe"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("strict mode should allow tool steps: %v", 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())
|
||||
if !strings.Contains(out, "safe") {
|
||||
t.Errorf("got %q, want 'safe'", out)
|
||||
}
|
||||
}
|
||||
|
||||
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 := `{"pipe":[{"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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- flock / locking ----
|
||||
|
||||
func TestDetectParallelClass(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
want lockClass
|
||||
}{
|
||||
{"#!/bin/sh\n# ollie:parallel read\ncat $1\n", lockClassRead},
|
||||
{"#!/bin/sh\n# ollie:parallel write\n", lockClassWrite},
|
||||
{"#!/bin/sh\n# ollie:parallel read extra\ncat\n", lockClassRead},
|
||||
{"#!/bin/sh\n# ollie:parallel write extra\n", lockClassWrite},
|
||||
{"#!/bin/sh\nno annotation\n", lockClassGlobal},
|
||||
{"", lockClassGlobal},
|
||||
// lua-style comment
|
||||
{"#!/usr/bin/env lua\n-- ollie:parallel read\n", lockClassRead},
|
||||
// C-style comment
|
||||
{"// ollie:parallel read\n", lockClassRead},
|
||||
// annotation after line 10 is ignored
|
||||
{strings.Repeat("# filler\n", 10) + "# ollie:parallel read\n", lockClassGlobal},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := detectParallelClass(tt.code)
|
||||
if got != tt.want {
|
||||
t.Errorf("detectParallelClass(code) = %v; want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeLockName(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"path/to/file", "path_to_file"},
|
||||
{"a b:c*d?e<f>g|h\"i\\j", "a_b_c_d_e_f_g_h_i_j"},
|
||||
{"", "unnamed"},
|
||||
{strings.Repeat("x", 100), strings.Repeat("x", 64)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := sanitizeLockName(tt.in)
|
||||
if got != tt.want {
|
||||
t.Errorf("sanitizeLockName(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireFlockDisabled(t *testing.T) {
|
||||
f, err := acquireFlock("", "name", true)
|
||||
if err != nil || f != nil {
|
||||
t.Errorf("acquireFlock with empty dir: f=%v err=%v; want nil,nil", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireFlockShared(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f, err := acquireFlock(dir, "test", false)
|
||||
if err != nil {
|
||||
t.Fatalf("acquireFlock: %v", err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatal("expected non-nil file")
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func TestAcquireFlockExclusive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f, err := acquireFlock(dir, "excl", true)
|
||||
if err != nil {
|
||||
t.Fatalf("acquireFlock exclusive: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
// ---- classifyStep ----
|
||||
|
||||
func TestClassifyStep(t *testing.T) {
|
||||
// inline code → global
|
||||
if got := classifyStep(CodeStep{Code: "echo hi"}); got != lockClassGlobal {
|
||||
t.Errorf("inline code: got %v; want global", got)
|
||||
}
|
||||
// parallel group → global
|
||||
if got := classifyStep(CodeStep{Parallel: []CodeStep{{Code: "x"}}}); got != lockClassGlobal {
|
||||
t.Errorf("parallel group: got %v; want global", got)
|
||||
// read-safe inline code
|
||||
if got := classifyStep(CodeStep{Code: "echo hi"}); got != lockClassRead {
|
||||
t.Errorf("inline echo: got %v; want read", got)
|
||||
}
|
||||
// elevated → global
|
||||
if got := classifyStep(CodeStep{Elevated: true}); got != lockClassGlobal {
|
||||
|
|
|
|||
Reference in New Issue