add ai feature
This commit is contained in:
48
tests/contract/ai_command_preview_test.go
Normal file
48
tests/contract/ai_command_preview_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/renamer/cmd"
|
||||
)
|
||||
|
||||
func TestAICommandPreviewTable(t *testing.T) {
|
||||
t.Setenv("RENAMER_AI_KEY", "test-key")
|
||||
|
||||
tmp := t.TempDir()
|
||||
createFile(t, filepath.Join(tmp, "IMG_0001.jpg"))
|
||||
createFile(t, filepath.Join(tmp, "trip-notes.txt"))
|
||||
|
||||
root := cmd.NewRootCommand()
|
||||
root.SetArgs([]string{"ai", "--path", tmp, "--prompt", "Travel Memories", "--dry-run"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
root.SetIn(strings.NewReader("\n"))
|
||||
root.SetOut(&buf)
|
||||
root.SetErr(&buf)
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("ai command returned error: %v\noutput: %s", err, buf.String())
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "IMG_0001.jpg") {
|
||||
t.Fatalf("expected original filename in preview, got: %s", output)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "trip-notes.txt") {
|
||||
t.Fatalf("expected secondary filename in preview, got: %s", output)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "01.travel-memories-img-0001.jpg") {
|
||||
t.Fatalf("expected deterministic suggestion in preview, got: %s", output)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "02.travel-memories-trip-notes.txt") {
|
||||
t.Fatalf("expected sequential suggestion for second file, got: %s", output)
|
||||
}
|
||||
}
|
||||
68
tests/contract/ai_ledger_entry_test.go
Normal file
68
tests/contract/ai_ledger_entry_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/renamer/internal/ai"
|
||||
"github.com/rogeecn/renamer/internal/ai/flow"
|
||||
)
|
||||
|
||||
func TestAIMetadataPersistedInLedgerEntry(t *testing.T) {
|
||||
t.Setenv("RENAMER_AI_KEY", "test-key")
|
||||
|
||||
tmp := t.TempDir()
|
||||
createFile(t, filepath.Join(tmp, "clip.mov"))
|
||||
|
||||
suggestions := []flow.Suggestion{
|
||||
{Original: "clip.mov", Suggested: "highlight-01.mov"},
|
||||
}
|
||||
|
||||
validation := ai.ValidateSuggestions([]string{"clip.mov"}, suggestions)
|
||||
if len(validation.Conflicts) != 0 {
|
||||
t.Fatalf("expected no conflicts, got %#v", validation)
|
||||
}
|
||||
|
||||
entry, err := ai.Apply(context.Background(), tmp, suggestions, validation, ai.ApplyMetadata{
|
||||
Prompt: "Highlight Reel",
|
||||
PromptHistory: []string{"Highlight Reel", "Celebration Cut"},
|
||||
Notes: []string{"accepted preview"},
|
||||
Model: "googleai/gemini-1.5-flash",
|
||||
SequenceSeparator: "_",
|
||||
}, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("apply error: %v", err)
|
||||
}
|
||||
|
||||
if entry.Command != "ai" {
|
||||
t.Fatalf("expected command 'ai', got %q", entry.Command)
|
||||
}
|
||||
|
||||
if entry.Metadata == nil {
|
||||
t.Fatalf("expected metadata to be recorded")
|
||||
}
|
||||
|
||||
if got := entry.Metadata["prompt"]; got != "Highlight Reel" {
|
||||
t.Fatalf("unexpected prompt metadata: %#v", got)
|
||||
}
|
||||
|
||||
history, ok := entry.Metadata["promptHistory"].([]string)
|
||||
if !ok || len(history) != 2 {
|
||||
t.Fatalf("unexpected prompt history: %#v", entry.Metadata["promptHistory"])
|
||||
}
|
||||
|
||||
model, _ := entry.Metadata["model"].(string)
|
||||
if model == "" {
|
||||
t.Fatalf("expected model metadata to be present")
|
||||
}
|
||||
|
||||
if sep, ok := entry.Metadata["sequenceSeparator"].(string); !ok || sep != "_" {
|
||||
t.Fatalf("expected sequence separator metadata, got %#v", entry.Metadata["sequenceSeparator"])
|
||||
}
|
||||
|
||||
if _, err := ai.Apply(context.Background(), tmp, suggestions, validation, ai.ApplyMetadata{Prompt: "irrelevant"}, io.Discard); err == nil {
|
||||
t.Fatalf("expected error when renaming non-existent file")
|
||||
}
|
||||
}
|
||||
82
tests/integration/ai_flow_apply_test.go
Normal file
82
tests/integration/ai_flow_apply_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
renamercmd "github.com/rogeecn/renamer/cmd"
|
||||
"github.com/rogeecn/renamer/internal/history"
|
||||
)
|
||||
|
||||
func TestAIRenameApplyAndUndo(t *testing.T) {
|
||||
t.Setenv("RENAMER_AI_KEY", "test-key")
|
||||
|
||||
tmp := t.TempDir()
|
||||
createFile(t, filepath.Join(tmp, "IMG_2001.jpg"))
|
||||
createFile(t, filepath.Join(tmp, "session-notes.txt"))
|
||||
|
||||
root := renamercmd.NewRootCommand()
|
||||
root.SetArgs([]string{"ai", "--path", tmp, "--prompt", "Album Shots", "--yes"})
|
||||
root.SetIn(strings.NewReader(""))
|
||||
var output bytes.Buffer
|
||||
root.SetOut(&output)
|
||||
root.SetErr(&output)
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("ai command returned error: %v\noutput: %s", err, output.String())
|
||||
}
|
||||
|
||||
ledgerPath := filepath.Join(tmp, ".renamer")
|
||||
data, err := os.ReadFile(ledgerPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read ledger: %v", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if len(lines) == 0 {
|
||||
t.Fatalf("expected ledger entries")
|
||||
}
|
||||
var entry history.Entry
|
||||
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &entry); err != nil {
|
||||
t.Fatalf("decode entry: %v", err)
|
||||
}
|
||||
if entry.Command != "ai" {
|
||||
t.Fatalf("expected command 'ai', got %q", entry.Command)
|
||||
}
|
||||
if len(entry.Operations) != 2 {
|
||||
t.Fatalf("expected 2 operations recorded, got %d", len(entry.Operations))
|
||||
}
|
||||
if entry.Metadata == nil || entry.Metadata["prompt"] != "Album Shots" {
|
||||
t.Fatalf("expected prompt metadata recorded, got %#v", entry.Metadata)
|
||||
}
|
||||
|
||||
if sep, ok := entry.Metadata["sequenceSeparator"].(string); !ok || sep != "." {
|
||||
t.Fatalf("expected sequence separator metadata, got %#v", entry.Metadata["sequenceSeparator"])
|
||||
}
|
||||
|
||||
for _, op := range entry.Operations {
|
||||
dest := filepath.Join(tmp, filepath.FromSlash(op.To))
|
||||
if _, err := os.Stat(dest); err != nil {
|
||||
t.Fatalf("expected destination %q to exist: %v", dest, err)
|
||||
}
|
||||
}
|
||||
|
||||
undoCmd := renamercmd.NewRootCommand()
|
||||
undoCmd.SetArgs([]string{"undo", "--path", tmp})
|
||||
undoCmd.SetIn(strings.NewReader(""))
|
||||
undoCmd.SetOut(&output)
|
||||
undoCmd.SetErr(&output)
|
||||
if err := undoCmd.Execute(); err != nil {
|
||||
t.Fatalf("undo command error: %v\noutput: %s", err, output.String())
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(tmp, "IMG_2001.jpg")); err != nil {
|
||||
t.Fatalf("expected original root file restored: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tmp, "session-notes.txt")); err != nil {
|
||||
t.Fatalf("expected original secondary file restored: %v", err)
|
||||
}
|
||||
}
|
||||
46
tests/integration/ai_preview_regen_test.go
Normal file
46
tests/integration/ai_preview_regen_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/renamer/cmd"
|
||||
)
|
||||
|
||||
func TestAICommandSupportsPromptRefinement(t *testing.T) {
|
||||
t.Setenv("RENAMER_AI_KEY", "test-key")
|
||||
|
||||
tmp := t.TempDir()
|
||||
createFile(t, filepath.Join(tmp, "IMG_1024.jpg"))
|
||||
createFile(t, filepath.Join(tmp, "notes/day1.txt"))
|
||||
|
||||
root := cmd.NewRootCommand()
|
||||
root.SetArgs([]string{"ai", "--path", tmp})
|
||||
|
||||
// Simulate editing the prompt then quitting.
|
||||
var output bytes.Buffer
|
||||
input := strings.NewReader("e\nVacation Highlights\nq\n")
|
||||
root.SetIn(input)
|
||||
root.SetOut(&output)
|
||||
root.SetErr(&output)
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("ai command returned error: %v\noutput: %s", err, output.String())
|
||||
}
|
||||
|
||||
got := output.String()
|
||||
|
||||
if !strings.Contains(got, "Current prompt: \"Vacation Highlights\"") {
|
||||
t.Fatalf("expected updated prompt in output, got: %s", got)
|
||||
}
|
||||
|
||||
if !strings.Contains(got, "01.vacation-highlights-img-1024.jpg") {
|
||||
t.Fatalf("expected regenerated suggestion with new prefix, got: %s", got)
|
||||
}
|
||||
|
||||
if !strings.Contains(got, "Session ended without applying changes.") {
|
||||
t.Fatalf("expected session completion message, got: %s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user