diff options
| -rw-r--r-- | internal/executor/gemini.go | 3 | ||||
| -rw-r--r-- | internal/executor/gemini_test.go | 57 | ||||
| -rw-r--r-- | internal/executor/preamble_test.go | 14 | ||||
| -rw-r--r-- | web/app.js | 7 | ||||
| -rw-r--r-- | web/test/new-task-button.test.mjs | 24 |
5 files changed, 85 insertions, 20 deletions
diff --git a/internal/executor/gemini.go b/internal/executor/gemini.go index 956d8b5..c30cd66 100644 --- a/internal/executor/gemini.go +++ b/internal/executor/gemini.go @@ -169,8 +169,9 @@ func (r *GeminiRunner) buildArgs(t *task.Task, e *storage.Execution, questionFil } args := []string{ - instructions, + "-p", instructions, "--output-format", "stream-json", + "--yolo", // auto-approve all tools (equivalent to Claude's bypassPermissions) } // Note: Gemini CLI flags might differ from Claude CLI. diff --git a/internal/executor/gemini_test.go b/internal/executor/gemini_test.go index 363f0e9..073525c 100644 --- a/internal/executor/gemini_test.go +++ b/internal/executor/gemini_test.go @@ -24,9 +24,9 @@ func TestGeminiRunner_BuildArgs_BasicTask(t *testing.T) { args := r.buildArgs(tk, &storage.Execution{ID: "test-exec"}, "/tmp/q.json") - // Gemini CLI: instructions is the first positional arg - if len(args) < 1 || args[0] != "fix the bug" { - t.Errorf("expected instructions as first arg, got: %v", args) + // Gemini CLI: instructions passed via -p for non-interactive mode + if len(args) < 2 || args[0] != "-p" || args[1] != "fix the bug" { + t.Errorf("expected -p <instructions> as first args, got: %v", args) } argMap := make(map[string]bool) @@ -52,17 +52,60 @@ func TestGeminiRunner_BuildArgs_PreamblePrepended(t *testing.T) { args := r.buildArgs(tk, &storage.Execution{ID: "test-exec"}, "/tmp/q.json") - if len(args) < 1 { - t.Fatalf("expected at least 1 arg, got: %v", args) + if len(args) < 2 || args[0] != "-p" { + t.Fatalf("expected -p <instructions> as first args, got: %v", args) } - if !strings.HasPrefix(args[0], planningPreamble) { + if !strings.HasPrefix(args[1], planningPreamble) { t.Errorf("instructions should start with planning preamble") } - if !strings.HasSuffix(args[0], "fix the bug") { + if !strings.HasSuffix(args[1], "fix the bug") { t.Errorf("instructions should end with original instructions") } } +func TestGeminiRunner_BuildArgs_IncludesYolo(t *testing.T) { + r := &GeminiRunner{} + tk := &task.Task{ + Agent: task.AgentConfig{ + Type: "gemini", + Instructions: "write a doc", + SkipPlanning: true, + }, + } + args := r.buildArgs(tk, &storage.Execution{ID: "test-exec"}, "/tmp/q.json") + argMap := make(map[string]bool) + for _, a := range args { + argMap[a] = true + } + if !argMap["--yolo"] { + t.Errorf("expected --yolo in gemini args (enables all tools); got: %v", args) + } +} + +func TestGeminiRunner_BuildArgs_IncludesPromptFlag(t *testing.T) { + r := &GeminiRunner{} + tk := &task.Task{ + Agent: task.AgentConfig{ + Type: "gemini", + Instructions: "do the thing", + SkipPlanning: true, + }, + } + args := r.buildArgs(tk, &storage.Execution{ID: "test-exec"}, "/tmp/q.json") + // Instructions must be passed via -p/--prompt for non-interactive headless mode, + // not as a bare positional (which starts interactive mode). + found := false + for i, a := range args { + if (a == "-p" || a == "--prompt") && i+1 < len(args) && args[i+1] == "do the thing" { + found = true + break + } + } + if !found { + t.Errorf("expected instructions passed via -p/--prompt flag; got: %v", args) + } +} + func TestGeminiRunner_Run_InaccessibleProjectDir_ReturnsError(t *testing.T) { r := &GeminiRunner{ BinaryPath: "true", // would succeed if it ran diff --git a/internal/executor/preamble_test.go b/internal/executor/preamble_test.go index 448ad3a..984f786 100644 --- a/internal/executor/preamble_test.go +++ b/internal/executor/preamble_test.go @@ -11,16 +11,14 @@ func TestPlanningPreamble_ContainsFinalSummarySection(t *testing.T) { } } -func TestPlanningPreamble_SummaryRequiresMarkdownHeader(t *testing.T) { - if !strings.Contains(planningPreamble, `Start it with "## Summary"`) { - t.Error("planningPreamble does not instruct agent to start summary with '## Summary'") +func TestPlanningPreamble_SummaryUsesFileEnvVar(t *testing.T) { + if !strings.Contains(planningPreamble, "CLAUDOMATOR_SUMMARY_FILE") { + t.Error("planningPreamble should instruct agent to write summary to $CLAUDOMATOR_SUMMARY_FILE") } } -func TestPlanningPreamble_SummaryDescribesRequiredContent(t *testing.T) { - for _, phrase := range []string{"What was accomplished", "Key decisions made", "Any issues or follow-ups"} { - if !strings.Contains(planningPreamble, phrase) { - t.Errorf("planningPreamble missing required summary content description: %q", phrase) - } +func TestPlanningPreamble_SummaryInstructsEchoToFile(t *testing.T) { + if !strings.Contains(planningPreamble, `"$CLAUDOMATOR_SUMMARY_FILE"`) { + t.Error("planningPreamble should show example of writing to $CLAUDOMATOR_SUMMARY_FILE via echo") } } @@ -278,6 +278,9 @@ export function filterActiveTasks(tasks) { return tasks.filter(t => _PANEL_ACTIVE_STATES.has(t.state)); } +// The New Task button is always visible regardless of active tab. +export function newTaskButtonShouldShowOnTab(_tab) { return true; } + export function filterTasksByTab(tasks, tab) { if (tab === 'active') return tasks.filter(t => ACTIVE_STATES.has(t.state)); if (tab === 'interrupted') return tasks.filter(t => INTERRUPTED_STATES.has(t.state)); @@ -2198,10 +2201,6 @@ function switchTab(name) { } }); - // Show/hide the header New Task button (only relevant on tasks tab) - document.getElementById('btn-new-task').style.display = - name === 'tasks' ? '' : 'none'; - if (name === 'running') { fetchTasks().then(renderRunningView).catch(() => { const currentEl = document.querySelector('.running-current'); diff --git a/web/test/new-task-button.test.mjs b/web/test/new-task-button.test.mjs new file mode 100644 index 0000000..45a3548 --- /dev/null +++ b/web/test/new-task-button.test.mjs @@ -0,0 +1,24 @@ +// new-task-button.test.mjs — visibility contract for the New Task button +// +// The New Task button lives in the global header and must be visible on all tabs. +// Run with: node --test web/test/new-task-button.test.mjs + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { newTaskButtonShouldShowOnTab } from '../app.js'; + +const ALL_TABS = ['tasks', 'active', 'running', 'stats']; + +describe('new task button visibility', () => { + for (const tab of ALL_TABS) { + it(`is visible on "${tab}" tab`, () => { + assert.equal(newTaskButtonShouldShowOnTab(tab), true, `expected button to be visible on tab "${tab}"`); + }); + } + + it('is visible on any unknown future tab', () => { + assert.equal(newTaskButtonShouldShowOnTab('help'), true); + assert.equal(newTaskButtonShouldShowOnTab('templates'), true); + assert.equal(newTaskButtonShouldShowOnTab(''), true); + }); +}); |
