package gptfs
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
)
func TestGptfsAsyncProcessing(t *testing.T) {
tempDir, err := os.MkdirTemp("", "gptfs-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
mountPath := tempDir
var gotReqBody []byte
var requestCount int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
if r.Method != http.MethodPost {
t.Fatalf("want POST, got %s", r.Method)
}
if auth := r.Header.Get("Authorization"); auth != "Bearer test-key" {
t.Fatalf("unexpected Authorization header: %q", auth)
}
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Fatalf("unexpected Content-Type: %q", ct)
}
b, _ := io.ReadAll(r.Body)
_ = r.Body.Close()
gotReqBody = b
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"hello world"}}]}`))
}))
defer ts.Close()
config := map[string]interface{}{
"api_host": ts.URL,
"api_key": "test-key",
"mount_path": mountPath,
"workers": 1,
}
g := NewGptfs()
if err := g.Validate(config); err != nil {
t.Fatalf("Validate config: %v", err)
}
if err := g.Initialize(config); err != nil {
t.Fatalf("Initialize GPTFS: %v", err)
}
defer g.Shutdown()
fs := g.GetFileSystem()
payload := map[string]any{
"model": "gpt-4o-mini",
"messages": []map[string]string{{"role": "user", "content": "ping"}},
}
data, _ := json.Marshal(payload)
requestPath := "inbox/request.json"
if _, err := fs.Write(requestPath, data, -1, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
t.Fatalf("write request.json: %v", err)
}
if requestCount != 0 {
t.Fatalf("expected async processing, but API was called immediately")
}
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var responseContent string
var statusContent string
for {
select {
case <-timeout:
outboxPath := "outbox"
if files, err := fs.ReadDir(outboxPath); err == nil {
t.Logf("Files in outbox: %+v", files)
}
t.Fatalf("timeout waiting for response. Response: %q, Status: %q", responseContent, statusContent)
case <-ticker.C:
responsePath := "outbox/request_response.txt"
if response, err := fs.Read(responsePath, 0, -1); err == nil {
responseContent = string(response)
t.Logf("Found response content: %q", responseContent)
} else if err == io.EOF && response != nil {
responseContent = string(response)
t.Logf("Found response content before EOF: %q", responseContent)
} else if err == io.EOF {
t.Logf("Response file exists but empty")
} else {
t.Logf("Response file read error: %v", err)
}
statusPath := "outbox/request_status.json"
if status, err := fs.Read(statusPath, 0, -1); err == nil {
statusContent = string(status)
t.Logf("Found status content: %q", statusContent)
} else if err == io.EOF && status != nil {
statusContent = string(status)
t.Logf("Found status content before EOF: %q", statusContent)
} else if err == io.EOF {
t.Logf("Status file exists but empty")
} else {
t.Logf("Status file read error: %v", err)
}
if responseContent != "" {
t.Logf("Got response content, considering test successful")
goto done
}
}
}
done:
if responseContent != "hello world" {
t.Fatalf("unexpected response: %q", responseContent)
}
if statusContent == "" {
t.Fatalf("expected status content, got empty")
}
if requestCount != 1 {
t.Fatalf("expected 1 API call, got %d", requestCount)
}
if len(gotReqBody) == 0 {
t.Fatalf("server did not receive request body")
}
}
func TestGptfsMultipleRequests(t *testing.T) {
tempDir, err := os.MkdirTemp("", "gptfs-test-multiple")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
mountPath := filepath.Join(tempDir, "mount")
if err := os.MkdirAll(mountPath, 0755); err != nil {
t.Fatalf("Failed to create mount dir: %v", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"response"}}]}`))
}))
defer ts.Close()
config := map[string]interface{}{
"api_host": ts.URL,
"api_key": "test-key",
"mount_path": mountPath,
"workers": 2,
}
g := NewGptfs()
if err := g.Initialize(config); err != nil {
t.Fatalf("Initialize GPTFS: %v", err)
}
defer g.Shutdown()
fs := g.GetFileSystem()
requests := []string{"query1.json", "query2.txt", "query3.md"}
for _, req := range requests {
requestPath := filepath.Join("inbox", req)
data := []byte("test content " + req)
if _, err := fs.Write(requestPath, data, -1, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
t.Fatalf("write %s: %v", req, err)
}
}
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
responses := make(map[string]bool)
for {
select {
case <-timeout:
t.Fatalf("timeout waiting for responses")
case <-ticker.C:
for _, req := range requests {
if responses[req] {
continue
}
baseName := req[:len(req)-len(filepath.Ext(req))]
responsePath := filepath.Join("outbox", baseName+"_response.txt")
if response, err := fs.Read(responsePath, 0, -1); err == nil || (err == io.EOF && response != nil) {
responses[req] = true
}
}
if len(responses) == len(requests) {
goto done
}
}
}
done:
for _, req := range requests {
if !responses[req] {
t.Fatalf("missing response for %s", req)
}
}
}
func TestGptfsErrorHandling(t *testing.T) {
tempDir, err := os.MkdirTemp("", "gptfs-test-error")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
mountPath := filepath.Join(tempDir, "mount")
if err := os.MkdirAll(mountPath, 0755); err != nil {
t.Fatalf("Failed to create mount dir: %v", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server error", http.StatusInternalServerError)
}))
defer ts.Close()
config := map[string]interface{}{
"api_host": ts.URL,
"api_key": "test-key",
"mount_path": mountPath,
"workers": 1,
}
g := NewGptfs()
if err := g.Initialize(config); err != nil {
t.Fatalf("Initialize GPTFS: %v", err)
}
defer g.Shutdown()
fs := g.GetFileSystem()
requestPath := filepath.Join("inbox", "error_test.json")
data := []byte(`{"test": "error"}`)
if _, err := fs.Write(requestPath, data, -1, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
t.Fatalf("write request.json: %v", err)
}
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-timeout:
responsePath := filepath.Join("outbox", "error_test_response.txt")
if _, err := fs.Read(responsePath, 0, -1); err == nil {
t.Fatalf("unexpected response file present despite API error")
}
if _, err := fs.Read(filepath.Join("outbox", "error_test_status.json"), 0, -1); err != nil && err != io.EOF {
t.Fatalf("expected pending status file to exist: %v", err)
}
goto done
case <-ticker.C:
statusPath := filepath.Join("outbox", "job_status.json")
if statusData, err := fs.Read(statusPath, 0, -1); err == nil {
var status JobRequest
if err := json.Unmarshal(statusData, &status); err == nil {
if status.Status == "failed" {
goto done
}
}
}
}
}
done:
}
func TestGptfsValidate(t *testing.T) {
g := NewGptfs()
validConfig := map[string]interface{}{
"api_key": "test-key",
"api_host": "http://example.com",
"mount_path": "/tmp",
}
if err := g.Validate(validConfig); err != nil {
t.Fatalf("Validate valid config: %v", err)
}
invalidConfig1 := map[string]interface{}{
"api_host": "http://example.com",
"mount_path": "/tmp",
}
if err := g.Validate(invalidConfig1); err == nil {
t.Fatalf("expected error for missing api_key")
}
invalidConfig2 := map[string]interface{}{
"api_key": "test-key",
"mount_path": "/tmp",
}
if err := g.Validate(invalidConfig2); err == nil {
t.Fatalf("expected error for missing api_host")
}
invalidConfig3 := map[string]interface{}{
"api_key": "test-key",
"api_host": "http://example.com",
}
if err := g.Validate(invalidConfig3); err == nil {
t.Fatalf("expected error for missing data_dir")
}
invalidConfig4 := map[string]interface{}{
"api_key": "test-key",
"api_host": "http://example.com",
"mount_path": "/tmp",
"unknown": "key",
}
if err := g.Validate(invalidConfig4); err == nil {
t.Fatalf("expected error for unknown keys")
}
}
func TestGptfsGetReadme(t *testing.T) {
g := NewGptfs()
readme := g.GetReadme()
expectedStrings := []string{
"GPTFS Plugin",
"Async GPT Processing",
"Persistent Storage",
"inbox/",
"outbox/",
"_response.txt",
"_status.json",
"workflow",
"configuration",
}
for _, expected := range expectedStrings {
if !strings.Contains(strings.ToLower(readme), strings.ToLower(expected)) {
t.Fatalf("readme missing expected string: %q", expected)
}
}
}
func TestGptfsGetConfigParams(t *testing.T) {
g := NewGptfs()
params := g.GetConfigParams()
expectedParams := map[string]bool{
"api_key": false,
"api_host": false,
"data_dir": false,
"workers": false,
}
if len(params) != len(expectedParams) {
t.Fatalf("expected %d config params, got %d", len(expectedParams), len(params))
}
for _, param := range params {
if _, exists := expectedParams[param.Name]; !exists {
t.Fatalf("unexpected config param: %q", param.Name)
}
expectedParams[param.Name] = true
}
for param, found := range expectedParams {
if !found {
t.Fatalf("missing config param: %q", param)
}
}
}
func TestGptfsRegularWriteDelegation(t *testing.T) {
tempDir, err := os.MkdirTemp("", "gptfs-test-regular")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
mountPath := filepath.Join(tempDir, "mount")
if err := os.MkdirAll(mountPath, 0755); err != nil {
t.Fatalf("Failed to create mount dir: %v", err)
}
config := map[string]interface{}{
"api_host": "http://127.0.0.1:0",
"api_key": "test-key",
"mount_path": mountPath,
"workers": 1,
}
g := NewGptfs()
if err := g.Initialize(config); err != nil {
t.Fatalf("Initialize GPTFS: %v", err)
}
defer g.Shutdown()
fs := g.GetFileSystem()
testPath := "regular.txt"
testContent := "test content"
if _, err := fs.Write(testPath, []byte(testContent), -1, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
t.Fatalf("write regular file: %v", err)
}
out, err := fs.Read(testPath, 0, -1)
if err != nil && err != io.EOF {
t.Fatalf("read regular file: %v", err)
}
if string(out) != testContent {
t.Fatalf("unexpected content: expected %q, got %q", testContent, string(out))
}
info, err := fs.Stat(testPath)
if err != nil {
t.Fatalf("stat regular file: %v", err)
}
if info.Size != int64(len(testContent)) {
t.Fatalf("unexpected size: expected %d, got %d", len(testContent), info.Size)
}
}