ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
281 lines
9.2 KiB
Go
281 lines
9.2 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
pgdb "github.com/Autumn-27/artex/db"
|
|
"github.com/Autumn-27/artex/traffic"
|
|
)
|
|
|
|
func TestSeedAssociatesTargetAssetWithTask(t *testing.T) {
|
|
m, err := NewManager(t.TempDir(), "")
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) - skipping", err)
|
|
}
|
|
defer m.Close()
|
|
task, err := m.CreateTask("seed ownership", "seed ownership", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = m.DeleteTask(task.ID, DeleteTaskOptions{DeleteAssets: true})
|
|
})
|
|
|
|
host := fmt.Sprintf("seed-%s.example.test", task.ID)
|
|
s := &Server{m: m}
|
|
s.seed(task, "https://"+host)
|
|
taskID := mustTaskID(t, task.ID)
|
|
assets, err := m.Assets().QueryByTask(taskID, "", 20, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, asset := range assets {
|
|
if asset.Domain == host || asset.URL == "https://"+host {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("seed target was not associated with task %s: %+v", task.ID, assets)
|
|
}
|
|
}
|
|
|
|
func TestDeleteTaskFilesRemovesOnlyOwnedWorkspaceAndTranscripts(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
|
|
removed := []struct {
|
|
path string
|
|
isDir bool
|
|
}{
|
|
{path: filepath.Join("tasks", "77"), isDir: true},
|
|
{path: filepath.Join("transcripts", "exp12-main.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp12-planner.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp12-worker-i9.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp12-main"), isDir: true},
|
|
{path: filepath.Join("transcripts", "exp12-worker-i9"), isDir: true},
|
|
}
|
|
kept := []struct {
|
|
path string
|
|
isDir bool
|
|
}{
|
|
{path: filepath.Join("tasks", "78"), isDir: true},
|
|
{path: filepath.Join("transcripts", "exp123-main.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp123-main"), isDir: true},
|
|
{path: filepath.Join("transcripts", "exp1-main.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp12main.jsonl")},
|
|
{path: filepath.Join("transcripts", "exp12-main.jsonl.bak")},
|
|
{path: filepath.Join("transcripts", "exp12-main.txt")},
|
|
{path: filepath.Join("transcripts", "conv-12.jsonl")},
|
|
{path: filepath.Join("transcripts", "unrelated-session"), isDir: true},
|
|
}
|
|
|
|
for _, fixture := range append(removed, kept...) {
|
|
createDeleteFixture(t, dataDir, fixture.path, fixture.isDir)
|
|
}
|
|
|
|
deleted, err := deleteTaskFiles(dataDir, "77", 12)
|
|
if err != nil {
|
|
t.Fatalf("delete task files: %v", err)
|
|
}
|
|
if !deleted {
|
|
t.Fatal("expected deletion to be reported")
|
|
}
|
|
for _, fixture := range removed {
|
|
assertPathMissing(t, filepath.Join(dataDir, fixture.path))
|
|
}
|
|
for _, fixture := range kept {
|
|
assertPathExists(t, filepath.Join(dataDir, fixture.path))
|
|
}
|
|
}
|
|
|
|
func TestDeleteTaskFilesReportsTranscriptOnlyDeletion(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
path := filepath.Join("transcripts", "exp44-main.jsonl")
|
|
createDeleteFixture(t, dataDir, path, false)
|
|
|
|
deleted, err := deleteTaskFiles(dataDir, "missing-task", 44)
|
|
if err != nil {
|
|
t.Fatalf("delete task files: %v", err)
|
|
}
|
|
if !deleted {
|
|
t.Fatal("expected transcript deletion to be reported")
|
|
}
|
|
assertPathMissing(t, filepath.Join(dataDir, path))
|
|
}
|
|
|
|
func TestDeleteTaskFilesMissingTargetsIsIdempotent(t *testing.T) {
|
|
deleted, err := deleteTaskFiles(t.TempDir(), "77", 12)
|
|
if err != nil {
|
|
t.Fatalf("delete missing task files: %v", err)
|
|
}
|
|
if deleted {
|
|
t.Fatal("missing targets must not be reported as deleted")
|
|
}
|
|
}
|
|
|
|
func TestStageTaskFilesRollbackRestoresWorkspaceAndTranscripts(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
paths := []string{
|
|
filepath.Join("tasks", "77", "notes.txt"),
|
|
filepath.Join("transcripts", "exp12-main.jsonl"),
|
|
filepath.Join("transcripts", "exp12-worker-i9", "sidechain.jsonl"),
|
|
}
|
|
for _, path := range paths {
|
|
createDeleteFixture(t, dataDir, path, false)
|
|
}
|
|
|
|
stage, err := stageTaskFiles(dataDir, "77", 12)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !stage.deleted {
|
|
t.Fatal("expected files to be staged")
|
|
}
|
|
for _, path := range paths {
|
|
assertPathMissing(t, filepath.Join(dataDir, path))
|
|
}
|
|
if err := stage.rollback(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range paths {
|
|
assertPathExists(t, filepath.Join(dataDir, path))
|
|
}
|
|
}
|
|
|
|
func TestStageTaskFilesRollbackReportsRestoreFailure(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
workspace := filepath.Join(dataDir, "tasks", "77")
|
|
createDeleteFixture(t, dataDir, filepath.Join("tasks", "77"), true)
|
|
|
|
stage, err := stageTaskFiles(dataDir, "77", 12)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(workspace, []byte("conflict"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cause := errors.New("forced PostgreSQL delete failure")
|
|
err = rollbackTaskDelete(cause, nil, stage)
|
|
if err == nil || !errors.Is(err, cause) || !strings.Contains(err.Error(), "destination already exists") {
|
|
t.Fatalf("rollback err=%v, want original and restore errors", err)
|
|
}
|
|
if _, statErr := os.Stat(stage.stageDir); statErr != nil {
|
|
t.Fatalf("staging was removed after failed restore: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestManagerDeleteTaskRestoresFilesAndTrafficWhenPostgresDeleteFails(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
m, err := NewManager(dataDir, "")
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) - skipping", err)
|
|
}
|
|
t.Cleanup(func() { _ = m.Close() })
|
|
|
|
task, err := m.CreateTask("delete rollback", "delete rollback", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
taskID := mustTaskID(t, task.ID)
|
|
triggerName := fmt.Sprintf("test_fail_task_delete_%d", taskID)
|
|
functionName := triggerName + "_fn"
|
|
cleanupDB := func() {
|
|
_, _ = m.pg.Exec(fmt.Sprintf(`DROP TRIGGER IF EXISTS %s ON tasks`, triggerName))
|
|
_, _ = m.pg.Exec(fmt.Sprintf(`DROP FUNCTION IF EXISTS %s()`, functionName))
|
|
_, _ = m.DeleteTask(task.ID, DeleteTaskOptions{DeleteAssets: true, DeleteTraffic: true, DeleteFiles: true})
|
|
}
|
|
t.Cleanup(cleanupDB)
|
|
|
|
host := fmt.Sprintf("delete-rollback-%d.example.test", taskID)
|
|
if _, err := m.Assets().UpsertHTTPService(pgdb.UpsertHTTPServiceReq{URL: "https://" + host, TaskID: taskID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createDeleteFixture(t, dataDir, filepath.Join("tasks", task.ID), true)
|
|
transcriptPath := filepath.Join(dataDir, "transcripts", fmt.Sprintf("exp%d-main.jsonl", task.ExpID))
|
|
createDeleteFixture(t, dataDir, filepath.Join("transcripts", filepath.Base(transcriptPath)), false)
|
|
|
|
tr, err := traffic.Open(filepath.Join(dataDir, "traffic"), "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.traffic = tr
|
|
trafficTree := filepath.Join(dataDir, "traffic", host, "GET", "1-0001")
|
|
if err := os.MkdirAll(trafficTree, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
trafficMarker := filepath.Join(trafficTree, "request.http")
|
|
if err := os.WriteFile(trafficMarker, []byte("original traffic"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tr.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, "1-0001", 1, host, "GET", "/", "https://"+host+"/", 200, "text/plain", 0, 0, host+"/GET/1-0001"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := m.pg.Exec(fmt.Sprintf(`CREATE FUNCTION %s() RETURNS trigger LANGUAGE plpgsql AS $body$
|
|
BEGIN RAISE EXCEPTION 'forced task delete failure'; END $body$`, functionName)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := m.pg.Exec(fmt.Sprintf(`CREATE TRIGGER %s BEFORE DELETE ON tasks
|
|
FOR EACH ROW WHEN (OLD.id = %d) EXECUTE FUNCTION %s()`, triggerName, taskID, functionName)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = m.DeleteTask(task.ID, DeleteTaskOptions{DeleteTraffic: true, DeleteFiles: true})
|
|
if err == nil || !strings.Contains(err.Error(), "forced task delete failure") {
|
|
t.Fatalf("delete err=%v, want injected PostgreSQL failure", err)
|
|
}
|
|
if got, err := m.pg.GetTask(taskID); err != nil || got == nil {
|
|
t.Fatalf("task row was lost after failed delete: task=%+v err=%v", got, err)
|
|
}
|
|
assertPathExists(t, filepath.Join(dataDir, "tasks", task.ID))
|
|
assertPathExists(t, transcriptPath)
|
|
if got, err := os.ReadFile(trafficMarker); err != nil || string(got) != "original traffic" {
|
|
t.Fatalf("traffic tree was not restored: content=%q err=%v", got, err)
|
|
}
|
|
var trafficRows int
|
|
if err := tr.DB().QueryRow(`SELECT COUNT(*) FROM exchanges WHERE host=?`, host).Scan(&trafficRows); err != nil || trafficRows != 1 {
|
|
t.Fatalf("traffic index was not restored: rows=%d err=%v", trafficRows, err)
|
|
}
|
|
}
|
|
|
|
func createDeleteFixture(t *testing.T, root, relative string, isDir bool) {
|
|
t.Helper()
|
|
path := filepath.Join(root, relative)
|
|
if isDir {
|
|
if err := os.MkdirAll(filepath.Join(path, "subagents"), 0o755); err != nil {
|
|
t.Fatalf("create fixture directory %s: %v", path, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "subagents", "agent-test.jsonl"), []byte("test"), 0o644); err != nil {
|
|
t.Fatalf("create fixture sidechain %s: %v", path, err)
|
|
}
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("create fixture parent %s: %v", path, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
|
t.Fatalf("create fixture file %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func assertPathMissing(t *testing.T, path string) {
|
|
t.Helper()
|
|
if _, err := os.Lstat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("expected %s to be removed, stat err=%v", path, err)
|
|
}
|
|
}
|
|
|
|
func assertPathExists(t *testing.T, path string) {
|
|
t.Helper()
|
|
if _, err := os.Lstat(path); err != nil {
|
|
t.Fatalf("expected %s to remain: %v", path, err)
|
|
}
|
|
}
|