summaryrefslogtreecommitdiff
path: root/internal/api/server_test.go
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-16 01:10:00 +0000
committerClaudomator Agent <agent@claudomator>2026-03-16 01:10:00 +0000
commitd911021b7e4a0c9f77ca9996b0ebdabb03c56696 (patch)
tree9fc5f8ab8bf3497ed25fbae698d7183a9e7c0fbe /internal/api/server_test.go
parent7f6254cdafc6143f80ee9ca8e482c36aff2c197e (diff)
feat: add elaboration_input field to tasks for richer subtask placeholder
- Add ElaborationInput field to Task struct (task.go) - Add DB migration and update CREATE/SELECT/scan in storage/db.go - Update handleCreateTask to accept elaboration_input from API - Update renderSubtaskRollup in app.js to prefer elaboration_input over description - Capture elaborate prompt in createTask() form submission - Update subtask-placeholder tests to cover elaboration_input priority - Fix missing io import in gemini.go When a task card is waiting for subtasks, it now shows: 1. The raw user prompt from elaboration (if stored) 2. The task description truncated at word boundary (~120 chars) 3. The task name as fallback 4. 'Waiting for subtasks…' only when all fields are empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server_test.go')
-rw-r--r--internal/api/server_test.go144
1 files changed, 144 insertions, 0 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index d090313..a670f33 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -132,6 +132,150 @@ func pollState(t *testing.T, store *storage.DB, taskID string, wantState task.St
return ""
}
+func testServerWithGeminiMockRunner(t *testing.T) (*Server, *storage.DB) {
+ t.Helper()
+ dbPath := filepath.Join(t.TempDir(), "test.db")
+ store, err := storage.Open(dbPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { store.Close() })
+
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
+
+ // Create the mock gemini binary script.
+ mockBinDir := t.TempDir()
+ mockGeminiPath := filepath.Join(mockBinDir, "mock-gemini-binary.sh")
+ mockScriptContent := `#!/bin/bash
+# Mock gemini binary that outputs stream-json wrapped in markdown to stdout.
+echo "```json"
+echo "{\"type\":\"content_block_start\",\"content_block\":{\"text\":\"Hello, Gemini!\",\"type\":\"text\"}}"
+echo "{\"type\":\"content_block_delta\",\"content_block\":{\"text\":\" How are you?\"}}"
+echo "{\"type\":\"content_block_end\"}"
+echo "{\"type\":\"message_delta\",\"message\":{\"role\":\"model\"}}"
+echo "{\"type\":\"message_end\"}"
+echo "```"
+exit 0
+`
+ if err := os.WriteFile(mockGeminiPath, []byte(mockScriptContent), 0755); err != nil {
+ t.Fatalf("writing mock gemini script: %v", err)
+ }
+
+ // Configure GeminiRunner to use the mock script.
+ geminiRunner := &executor.GeminiRunner{
+ BinaryPath: mockGeminiPath,
+ Logger: logger,
+ LogDir: t.TempDir(), // Ensure log directory is temporary for test
+ APIURL: "http://localhost:8080", // Placeholder, not used by this mock
+ }
+
+ runners := map[string]executor.Runner{
+ "claude": &mockRunner{}, // Keep mock for claude to not interfere
+ "gemini": geminiRunner,
+ }
+ pool := executor.NewPool(2, runners, store, logger)
+ srv := NewServer(store, pool, logger, "claude", "gemini") // Pass original binary paths
+ return srv, store
+}
+
+// TestGeminiLogs_ParsedCorrectly verifies that Gemini's markdown-wrapped stream-json
+// output is correctly unwrapped and parsed before being written to stdout.log
+// and exposed via the /api/tasks/{id}/executions/{exec-id}/log endpoint.
+func TestGeminiLogs_ParsedCorrectly(t *testing.T) {
+ srv, store := testServerWithGeminiMockRunner(t)
+
+ // Expected parsed JSON lines.
+ expectedParsedLogs := []string{
+ `{"type":"content_block_start","content_block":{"text":"Hello, Gemini!","type":"text"}}`,
+ `{"type":"content_block_delta","content_block":{"text":" How are you?"}}`,
+ `{"type":"content_block_end"}`,
+ `{"type":"message_delta","message":{"role":"model"}}`,
+ `{"type":"message_end"}`,
+ }
+
+ // 1. Create a task with Gemini agent.
+ tk := createTestTask(t, srv, `{
+ "name": "Gemini Log Test Task",
+ "description": "Test Gemini log parsing",
+ "agent": {
+ "type": "gemini",
+ "instructions": "generate some output",
+ "model": "gemini-2.5-flash-lite"
+ }
+ }`)
+
+ // 2. Run the task.
+ req := httptest.NewRequest("POST", "/api/tasks/"+tk.ID+"/run", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusAccepted {
+ t.Fatalf("run task status: want 202, got %d; body: %s", w.Code, w.Body.String())
+ }
+
+ // 3. Wait for the task to complete.
+ pollState(t, store, tk.ID, task.StateCompleted, 2*time.Second)
+
+ // Re-fetch the task to ensure we have the updated execution details.
+ updatedTask, err := store.GetTask(tk.ID)
+ if err != nil {
+ t.Fatalf("re-fetching task: %v", err)
+ }
+
+ // 4. Get the execution details to find the log path.
+ executions, err := store.ListExecutions(updatedTask.ID)
+ if err != nil {
+ t.Fatalf("listing executions: %v", err)
+ }
+ if len(executions) != 1 {
+ t.Fatalf("want 1 execution, got %d", len(executions))
+ }
+ exec := executions[0]
+ t.Logf("Re-fetched execution: %+v", exec) // Log the entire execution struct
+
+ // 5. Verify the content of stdout.log directly.
+ t.Logf("Attempting to read stdout.log from: %q", exec.StdoutPath)
+ stdoutContent, err := os.ReadFile(exec.StdoutPath)
+ if err != nil {
+ t.Fatalf("reading stdout.log: %v", err)
+ }
+ stdoutLines := strings.Split(strings.TrimSpace(string(stdoutContent)), "\n")
+ if len(stdoutLines) != len(expectedParsedLogs) {
+ t.Errorf("stdout.log line count: want %d, got %d\nContent:\n%s", len(expectedParsedLogs), len(stdoutLines), stdoutContent)
+ }
+ for i, line := range stdoutLines {
+ if i >= len(expectedParsedLogs) {
+ break
+ }
+ if line != expectedParsedLogs[i] {
+ t.Errorf("stdout.log line %d: want %q, got %q", i, expectedParsedLogs[i], line)
+ }
+ }
+
+ // 6. Verify the content retrieved via the API endpoint.
+ req = httptest.NewRequest("GET", "/api/tasks/"+tk.ID+"/executions/"+exec.ID+"/log", nil)
+ w = httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("GET /log status: want 200, got %d; body: %s", w.Code, w.Body.String())
+ }
+
+ apiLogContent := strings.TrimSpace(w.Body.String())
+ apiLogLines := strings.Split(apiLogContent, "\n")
+ if len(apiLogLines) != len(expectedParsedLogs) {
+ t.Errorf("API log line count: want %d, got %d\nContent:\n%s", len(expectedParsedLogs), len(apiLogLines), apiLogContent)
+ }
+ for i, line := range apiLogLines {
+ if i >= len(expectedParsedLogs) {
+ break
+ }
+ if line != expectedParsedLogs[i] {
+ t.Errorf("API log line %d: want %q, got %q", i, expectedParsedLogs[i], line)
+ }
+ }
+}
+
func TestListWorkspaces_UsesConfiguredRoot(t *testing.T) {
srv, _ := testServer(t)