Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
452 lines
13 KiB
Go
452 lines
13 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestVerifiedProcessExecutableFixture(_ *testing.T) {
|
|
if os.Getenv("TUTTI_TEST_VERIFIED_PROCESS_EXECUTABLE") == "1" {
|
|
fmt.Print("verified-original")
|
|
}
|
|
if os.Getenv("TUTTI_TEST_BOUNDED_PROCESS_EXECUTABLE") == "ok" {
|
|
fmt.Print("ok")
|
|
_, _ = fmt.Fprint(os.Stderr, "secret-stderr")
|
|
os.Exit(0)
|
|
}
|
|
if os.Getenv("TUTTI_TEST_BOUNDED_PROCESS_EXECUTABLE") == "overflow" {
|
|
fmt.Print("too-large")
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
func TestPrepareProcessExecutableExecutesVerifiedDescriptorAfterPathReplacement(t *testing.T) {
|
|
path, identity := copyCurrentExecutableWithIdentity(t)
|
|
prepared, err := prepareProcessExecutable(path, identity)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = prepared.Close() }()
|
|
if err := os.Rename(path, path+".original"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("replacement"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd := exec.Command(prepared.path, "-test.run=TestVerifiedProcessExecutableFixture")
|
|
if prepared.file != nil {
|
|
cmd.ExtraFiles = []*os.File{prepared.file}
|
|
}
|
|
cmd.Env = append(os.Environ(), "TUTTI_TEST_VERIFIED_PROCESS_EXECUTABLE=1")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil || !strings.Contains(string(output), "verified-original") {
|
|
t.Fatalf("descriptor execution output = %q, error = %v", output, err)
|
|
}
|
|
}
|
|
|
|
func TestRunVerifiedExecutableUsesVerifiedIdentity(t *testing.T) {
|
|
path, identity := copyCurrentExecutableWithIdentity(t)
|
|
t.Setenv("TUTTI_TEST_VERIFIED_PROCESS_EXECUTABLE", "1")
|
|
output, err := RunVerifiedExecutable(
|
|
context.Background(), path, []string{"-test.run=TestVerifiedProcessExecutableFixture"}, identity,
|
|
)
|
|
if err != nil || !strings.Contains(string(output), "verified-original") {
|
|
t.Fatalf("verified execution output = %q, error = %v", output, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("changed executable"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := RunVerifiedExecutable(context.Background(), path, nil, identity); err == nil ||
|
|
!strings.Contains(err.Error(), "expected identity") {
|
|
t.Fatalf("changed verified execution error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunVerifiedExecutableBoundedCapturesOnlyBoundedStdout(t *testing.T) {
|
|
path, identity := copyCurrentExecutableWithIdentity(t)
|
|
t.Setenv("TUTTI_TEST_BOUNDED_PROCESS_EXECUTABLE", "ok")
|
|
|
|
output, err := RunVerifiedExecutableBounded(
|
|
context.Background(), path, []string{"-test.run=TestVerifiedProcessExecutableFixture"}, identity, 8,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(output) != "ok" {
|
|
t.Fatalf("output = %q", output)
|
|
}
|
|
|
|
t.Setenv("TUTTI_TEST_BOUNDED_PROCESS_EXECUTABLE", "overflow")
|
|
if _, err := RunVerifiedExecutableBounded(
|
|
context.Background(), path, []string{"-test.run=TestVerifiedProcessExecutableFixture"}, identity, 3,
|
|
); err == nil {
|
|
t.Fatal("RunVerifiedExecutableBounded() overflow error = nil")
|
|
}
|
|
}
|
|
|
|
func TestRunVerifiedNodeScriptBoundedExecutesVerifiedScript(t *testing.T) {
|
|
nodePath, err := exec.LookPath("node")
|
|
if err != nil {
|
|
t.Skip("node is unavailable")
|
|
}
|
|
nodePath, err = filepath.EvalSymlinks(nodePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nodeIdentity := fileIdentity(t, nodePath)
|
|
scriptPath := filepath.Join(t.TempDir(), "probe.cjs")
|
|
script := []byte("process.stderr.write('secret'); process.stdout.write(JSON.stringify({arg: process.argv[2]}));\n")
|
|
if err := os.WriteFile(scriptPath, script, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scriptIdentity := fileIdentity(t, scriptPath)
|
|
|
|
output, err := RunVerifiedNodeScriptBounded(
|
|
context.Background(), nodePath, scriptPath, []string{"expected"}, nodeIdentity, scriptIdentity, 128,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(output) != `{"arg":"expected"}` {
|
|
t.Fatalf("output = %q", output)
|
|
}
|
|
|
|
if err := os.WriteFile(scriptPath, []byte("process.stdout.write('changed')\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := RunVerifiedNodeScriptBounded(
|
|
context.Background(), nodePath, scriptPath, nil, nodeIdentity, scriptIdentity, 128,
|
|
); err == nil || !strings.Contains(err.Error(), "expected identity") {
|
|
t.Fatalf("changed script error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestContextReaderStopsAfterCancellationDuringRead(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
reader := &contextReader{
|
|
ctx: ctx,
|
|
reader: cancelingReader{
|
|
cancel: cancel,
|
|
data: []byte("partial"),
|
|
},
|
|
}
|
|
data, err := io.ReadAll(reader)
|
|
if !errors.Is(err, context.Canceled) || string(data) != "partial" {
|
|
t.Fatalf("canceled read = %q, error = %v", data, err)
|
|
}
|
|
}
|
|
|
|
type cancelingReader struct {
|
|
cancel context.CancelFunc
|
|
data []byte
|
|
}
|
|
|
|
func (reader cancelingReader) Read(value []byte) (int, error) {
|
|
count := copy(value, reader.data)
|
|
reader.cancel()
|
|
return count, nil
|
|
}
|
|
|
|
func TestLocalProcessTransportRejectsChangedExpectedExecutable(t *testing.T) {
|
|
path, identity := copyCurrentExecutableWithIdentity(t)
|
|
if err := os.WriteFile(path, []byte("changed executable"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
|
|
Command: []string{path}, ExecutableIdentity: identity,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "expected identity") {
|
|
t.Fatalf("changed executable start error = %v", err)
|
|
}
|
|
}
|
|
|
|
func copyCurrentExecutableWithIdentity(t *testing.T) (string, *ExecutableIdentity) {
|
|
t.Helper()
|
|
source, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "verified-runtime")
|
|
if err := os.WriteFile(path, data, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
return path, &ExecutableIdentity{SHA256: hex.EncodeToString(digest[:]), SizeBytes: int64(len(data))}
|
|
}
|
|
|
|
func fileIdentity(t *testing.T, path string) *ExecutableIdentity {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
return &ExecutableIdentity{SHA256: hex.EncodeToString(digest[:]), SizeBytes: int64(len(data))}
|
|
}
|
|
|
|
func TestLocalProcessTransportOutlivesStartContext(t *testing.T) {
|
|
catPath, err := exec.LookPath("cat")
|
|
if err != nil {
|
|
t.Skip("cat is unavailable")
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
conn, err := NewLocalProcessTransport().Start(ctx, ProcessSpec{
|
|
Command: []string{catPath},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("start transport: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = conn.Close()
|
|
}()
|
|
|
|
cancel()
|
|
if err := conn.Send([]byte("hello\n")); err != nil {
|
|
t.Fatalf("send after start context cancel: %v", err)
|
|
}
|
|
|
|
done := make(chan ProcessFrame, 1)
|
|
errs := make(chan error, 1)
|
|
go func() {
|
|
frame, err := conn.Recv()
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
done <- frame
|
|
}()
|
|
|
|
select {
|
|
case frame := <-done:
|
|
if string(frame.Stdout) != "hello\n" {
|
|
t.Fatalf("stdout = %q, want echo", string(frame.Stdout))
|
|
}
|
|
case err := <-errs:
|
|
t.Fatalf("recv after start context cancel: %v", err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for process stdout")
|
|
}
|
|
}
|
|
|
|
func TestLocalProcessTransportFindsKnownNodeGlobalBin(t *testing.T) {
|
|
home := t.TempDir()
|
|
binDir := filepath.Join(home, ".nvm", "versions", "node", "v24.12.0", "bin")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
writeRuntimeExecutable(t, filepath.Join(binDir, "fake-acp"), "#!/bin/sh\nhelper-cli\n")
|
|
writeRuntimeExecutable(t, filepath.Join(binDir, "helper-cli"), "#!/bin/sh\nprintf 'nested-ok\\n'\n")
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("PATH", "/usr/bin:/bin")
|
|
|
|
conn, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
|
|
Command: []string{"fake-acp"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("start transport: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = conn.Close()
|
|
}()
|
|
|
|
frame := receiveRuntimeStdoutFrame(t, conn)
|
|
if string(frame.Stdout) != "nested-ok\n" {
|
|
t.Fatalf("stdout = %q, want nested-ok", string(frame.Stdout))
|
|
}
|
|
}
|
|
|
|
func TestLocalProcessTransportDeliversFinalStdoutBeforeExit(t *testing.T) {
|
|
shPath, err := exec.LookPath("sh")
|
|
if err != nil {
|
|
t.Skip("sh is unavailable")
|
|
}
|
|
|
|
conn, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
|
|
Command: []string{shPath, "-c", "printf 'final-output'"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("start transport: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = conn.Close()
|
|
}()
|
|
|
|
var stdout string
|
|
for {
|
|
frame, err := conn.Recv()
|
|
if err != nil {
|
|
t.Fatalf("recv frame: %v", err)
|
|
}
|
|
stdout += string(frame.Stdout)
|
|
if frame.ExitCode == nil {
|
|
continue
|
|
}
|
|
if *frame.ExitCode != 0 {
|
|
t.Fatalf("exit code = %d, want 0", *frame.ExitCode)
|
|
}
|
|
break
|
|
}
|
|
if stdout != "final-output" {
|
|
t.Fatalf("stdout before exit = %q, want final-output", stdout)
|
|
}
|
|
}
|
|
|
|
func TestLocalProcessTransportPreservesFinalStderrBeforeClose(t *testing.T) {
|
|
shPath, err := exec.LookPath("sh")
|
|
if err != nil {
|
|
t.Skip("sh is unavailable")
|
|
}
|
|
|
|
conn, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
|
|
Command: []string{shPath, "-c", "printf 'final-stderr\\n' >&2; exit 1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("start transport: %v", err)
|
|
}
|
|
if err := conn.Close(); err != nil {
|
|
t.Fatalf("close transport: %v", err)
|
|
}
|
|
|
|
var stderr string
|
|
var exitCode *int
|
|
for {
|
|
frame, err := conn.Recv()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("recv frame: %v", err)
|
|
}
|
|
stderr += string(frame.Stderr)
|
|
if frame.ExitCode != nil {
|
|
exitCode = frame.ExitCode
|
|
}
|
|
}
|
|
if stderr != "final-stderr\n" {
|
|
t.Fatalf("stderr before close = %q, want final-stderr", stderr)
|
|
}
|
|
if exitCode == nil || *exitCode != 1 {
|
|
t.Fatalf("exit code = %v, want 1", exitCode)
|
|
}
|
|
}
|
|
|
|
func TestLocalProcessTransportCloseKillsProcessAfterGracefulShutdownFails(t *testing.T) {
|
|
shPath, err := exec.LookPath("sh")
|
|
if err != nil {
|
|
t.Skip("sh is unavailable")
|
|
}
|
|
|
|
conn, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
|
|
Command: []string{shPath, "-c", "trap '' TERM; while true; do sleep 1; done"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("start transport: %v", err)
|
|
}
|
|
|
|
done := make(chan error, 1)
|
|
startedAt := time.Now()
|
|
go func() {
|
|
done <- conn.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
case <-time.After(4 * time.Second):
|
|
t.Fatal("Close did not force-kill process after graceful shutdown failed")
|
|
}
|
|
if elapsed := time.Since(startedAt); elapsed < 900*time.Millisecond {
|
|
t.Fatalf("Close returned after %s, want SIGTERM grace before kill fallback", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestProcessStartEnvDiagnosticsSummarizesFinalPath(t *testing.T) {
|
|
tuttiBin := filepath.Join(string(os.PathSeparator), "Users", "Sun", ".tutti", "bin")
|
|
managedBin := filepath.Join(string(os.PathSeparator), "managed", "node", "bin")
|
|
env := []string{
|
|
"PATH=" + managedBin + string(os.PathListSeparator) + tuttiBin + string(os.PathListSeparator) + "/usr/bin",
|
|
"TUTTI_APP_NODE=" + filepath.Join(managedBin, "node"),
|
|
"TUTTI_AGENT_SESSION_ID=agent-session-1",
|
|
}
|
|
diag := processStartEnvDiagnostics(ProcessSpec{
|
|
Provider: ProviderClaudeCode,
|
|
AgentSessionID: "agent-session-1",
|
|
Env: []string{
|
|
"PATH=" + tuttiBin + string(os.PathListSeparator) + "/usr/bin",
|
|
"PATH=" + managedBin + string(os.PathListSeparator) + "/usr/bin",
|
|
},
|
|
}, env)
|
|
|
|
if got := diag["path_override_count"]; got != 2 {
|
|
t.Fatalf("path_override_count = %v, want 2", got)
|
|
}
|
|
if got := diag["path_contains_tutti_bin"]; got != true {
|
|
t.Fatalf("path_contains_tutti_bin = %v, want true", got)
|
|
}
|
|
if got := diag["path_contains_app_node_bin"]; got != true {
|
|
t.Fatalf("path_contains_app_node_bin = %v, want true", got)
|
|
}
|
|
if got := diag["agent_session_env_present"]; got != true {
|
|
t.Fatalf("agent_session_env_present = %v, want true", got)
|
|
}
|
|
wantHead := []string{managedBin, tuttiBin, "/usr/bin"}
|
|
if got := diag["path_head"]; !reflect.DeepEqual(got, wantHead) {
|
|
t.Fatalf("path_head = %#v, want %#v", got, wantHead)
|
|
}
|
|
}
|
|
|
|
func receiveRuntimeStdoutFrame(t *testing.T, conn ProcessConnection) ProcessFrame {
|
|
t.Helper()
|
|
deadline := time.After(5 * time.Second)
|
|
for {
|
|
done := make(chan ProcessFrame, 1)
|
|
errs := make(chan error, 1)
|
|
go func() {
|
|
frame, err := conn.Recv()
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
done <- frame
|
|
}()
|
|
select {
|
|
case frame := <-done:
|
|
if len(frame.Stdout) > 0 {
|
|
return frame
|
|
}
|
|
if frame.ExitCode != nil {
|
|
t.Fatalf("process exited before stdout, exit code %d", *frame.ExitCode)
|
|
}
|
|
case err := <-errs:
|
|
t.Fatalf("recv stdout: %v", err)
|
|
case <-deadline:
|
|
t.Fatal("timed out waiting for process stdout")
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeRuntimeExecutable(t *testing.T, path string, contents string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(contents), 0o755); err != nil {
|
|
t.Fatalf("write executable %s: %v", path, err)
|
|
}
|
|
}
|