package i18n import ( "testing" "github.com/stretchr/testify/assert" ) // stringerValue is a test helper that implements fmt.Stringer type stringerValue struct { value string } func (s stringerValue) String() string { return s.value } func TestSubject_Good(t *testing.T) { t.Run("basic creation", func(t *testing.T) { s := S("file", "config.yaml") assert.Equal(t, "file", s.Noun) assert.Equal(t, "config.yaml", s.Value) assert.Equal(t, 1, s.count) assert.Equal(t, "", s.gender) assert.Equal(t, "", s.location) }) t.Run("NewSubject alias", func(t *testing.T) { s := NewSubject("repo", "core-php") assert.Equal(t, "repo", s.Noun) assert.Equal(t, "core-php", s.Value) }) t.Run("with count", func(t *testing.T) { s := S("file", "*.go").Count(5) assert.Equal(t, 5, s.CountInt()) assert.True(t, s.IsPlural()) }) t.Run("with gender", func(t *testing.T) { s := S("user", "alice").Gender("female") assert.Equal(t, "female", s.GenderString()) }) t.Run("with location", func(t *testing.T) { s := S("file", "config.yaml").In("workspace") assert.Equal(t, "workspace", s.LocationString()) }) t.Run("chained methods", func(t *testing.T) { s := S("repo", "core-php").Count(3).Gender("neuter").In("organisation") assert.Equal(t, "repo", s.NounString()) assert.Equal(t, 3, s.CountInt()) assert.Equal(t, "neuter", s.GenderString()) assert.Equal(t, "organisation", s.LocationString()) }) } func TestSubject_String(t *testing.T) { t.Run("string value", func(t *testing.T) { s := S("file", "config.yaml") assert.Equal(t, "config.yaml", s.String()) }) t.Run("stringer interface", func(t *testing.T) { // Using a struct that implements Stringer via embedded method s := S("item", stringerValue{"test"}) assert.Equal(t, "test", s.String()) }) t.Run("nil subject", func(t *testing.T) { var s *Subject assert.Equal(t, "", s.String()) }) t.Run("int value", func(t *testing.T) { s := S("count", 42) assert.Equal(t, "42", s.String()) }) } func TestSubject_IsPlural(t *testing.T) { t.Run("singular (count 1)", func(t *testing.T) { s := S("file", "test.go") assert.False(t, s.IsPlural()) }) t.Run("plural (count 0)", func(t *testing.T) { s := S("file", "*.go").Count(0) assert.True(t, s.IsPlural()) }) t.Run("plural (count > 1)", func(t *testing.T) { s := S("file", "*.go").Count(5) assert.True(t, s.IsPlural()) }) t.Run("nil subject", func(t *testing.T) { var s *Subject assert.False(t, s.IsPlural()) }) } func TestSubject_Getters(t *testing.T) { t.Run("nil safety", func(t *testing.T) { var s *Subject assert.Equal(t, "", s.NounString()) assert.Equal(t, 1, s.CountInt()) assert.Equal(t, "1", s.CountString()) assert.Equal(t, "", s.GenderString()) assert.Equal(t, "", s.LocationString()) }) t.Run("CountString", func(t *testing.T) { s := S("file", "test.go").Count(42) assert.Equal(t, "42", s.CountString()) }) } func TestIntentMeta(t *testing.T) { meta := IntentMeta{ Type: "action", Verb: "delete", Dangerous: true, Default: "no", Supports: []string{"force", "recursive"}, } assert.Equal(t, "action", meta.Type) assert.Equal(t, "delete", meta.Verb) assert.True(t, meta.Dangerous) assert.Equal(t, "no", meta.Default) assert.Contains(t, meta.Supports, "force") assert.Contains(t, meta.Supports, "recursive") } func TestComposed(t *testing.T) { composed := Composed{ Question: "Delete config.yaml?", Confirm: "Really delete config.yaml?", Success: "Config.yaml deleted", Failure: "Failed to delete config.yaml", Meta: IntentMeta{ Type: "action", Verb: "delete", Dangerous: true, Default: "no", }, } assert.Equal(t, "Delete config.yaml?", composed.Question) assert.Equal(t, "Really delete config.yaml?", composed.Confirm) assert.Equal(t, "Config.yaml deleted", composed.Success) assert.Equal(t, "Failed to delete config.yaml", composed.Failure) assert.True(t, composed.Meta.Dangerous) } func TestNewTemplateData(t *testing.T) { t.Run("from subject", func(t *testing.T) { s := S("file", "config.yaml").Count(3).Gender("neuter").In("workspace") data := newTemplateData(s) assert.Equal(t, "config.yaml", data.Subject) assert.Equal(t, "file", data.Noun) assert.Equal(t, 3, data.Count) assert.Equal(t, "neuter", data.Gender) assert.Equal(t, "workspace", data.Location) assert.Equal(t, "config.yaml", data.Value) }) t.Run("from nil subject", func(t *testing.T) { data := newTemplateData(nil) assert.Equal(t, "", data.Subject) assert.Equal(t, "", data.Noun) assert.Equal(t, 1, data.Count) assert.Equal(t, "", data.Gender) assert.Equal(t, "", data.Location) assert.Nil(t, data.Value) }) t.Run("with formality", func(t *testing.T) { s := S("user", "Hans").Formal() data := newTemplateData(s) assert.Equal(t, FormalityFormal, data.Formality) assert.True(t, data.IsFormal) }) t.Run("with plural", func(t *testing.T) { s := S("file", "*.go").Count(5) data := newTemplateData(s) assert.True(t, data.IsPlural) assert.Equal(t, 5, data.Count) }) } func TestSubject_Formality(t *testing.T) { t.Run("default is neutral", func(t *testing.T) { s := S("user", "name") assert.Equal(t, "neutral", s.FormalityString()) assert.False(t, s.IsFormal()) assert.False(t, s.IsInformal()) }) t.Run("Formal()", func(t *testing.T) { s := S("user", "name").Formal() assert.Equal(t, "formal", s.FormalityString()) assert.True(t, s.IsFormal()) }) t.Run("Informal()", func(t *testing.T) { s := S("user", "name").Informal() assert.Equal(t, "informal", s.FormalityString()) assert.True(t, s.IsInformal()) }) t.Run("Formality() explicit", func(t *testing.T) { s := S("user", "name").Formality(FormalityFormal) assert.Equal(t, "formal", s.FormalityString()) }) t.Run("nil safety", func(t *testing.T) { var s *Subject assert.Equal(t, "neutral", s.FormalityString()) assert.False(t, s.IsFormal()) assert.False(t, s.IsInformal()) }) } // --- Grammar composition tests using intent data --- // composeIntent executes intent templates with a subject for testing. // This is a test helper that replicates what C() used to do. func composeIntent(intent Intent, subject *Subject) *Composed { data := newTemplateData(subject) return &Composed{ Question: executeIntentTemplate(intent.Question, data), Confirm: executeIntentTemplate(intent.Confirm, data), Success: executeIntentTemplate(intent.Success, data), Failure: executeIntentTemplate(intent.Failure, data), Meta: intent.Meta, } } // TestGrammarComposition_MatchesIntents verifies that the grammar engine // can compose the same strings as the intent templates. // This turns the intents definitions into a comprehensive test suite. func TestGrammarComposition_MatchesIntents(t *testing.T) { // Test subjects for validation subjects := []struct { noun string value string }{ {"file", "config.yaml"}, {"directory", "src"}, {"repo", "core-php"}, {"branch", "feature/auth"}, {"commit", "abc1234"}, {"changes", "5 files"}, {"package", "laravel/framework"}, } // Test each core intent's composition for key, intent := range coreIntents { t.Run(key, func(t *testing.T) { for _, subj := range subjects { subject := S(subj.noun, subj.value) // Compose using intent templates directly composed := composeIntent(intent, subject) // Verify Success output matches ActionResult if intent.Success != "" && intent.Meta.Verb != "" { // Standard success pattern: "{{.Subject | title}} verbed" expectedSuccess := ActionResult(intent.Meta.Verb, subj.value) // Some intents have non-standard success messages switch key { case "core.run": // "completed" instead of "ran" expectedSuccess = Title(subj.value) + " completed" case "core.test": // "passed" instead of "tested" expectedSuccess = Title(subj.value) + " passed" case "core.validate": // "valid" instead of "validated" expectedSuccess = Title(subj.value) + " valid" case "core.check": // "OK" instead of "checked" expectedSuccess = Title(subj.value) + " OK" case "core.continue", "core.proceed": // No subject in success continue case "core.confirm": // No subject in success continue } assert.Equal(t, expectedSuccess, composed.Success, "%s: Success mismatch for subject %s", key, subj.value) } // Verify Failure output matches ActionFailed if intent.Failure != "" && intent.Meta.Verb != "" { // Standard failure pattern: "Failed to verb subject" expectedFailure := ActionFailed(intent.Meta.Verb, subj.value) // Some intents have non-standard failure messages switch key { case "core.test": // "failed" instead of "Failed to test" expectedFailure = Title(subj.value) + " failed" case "core.validate": // "invalid" instead of "Failed to validate" expectedFailure = Title(subj.value) + " invalid" case "core.check": // "failed" instead of "Failed to check" expectedFailure = Title(subj.value) + " failed" case "core.continue", "core.proceed", "core.confirm": // Non-standard failures continue } assert.Equal(t, expectedFailure, composed.Failure, "%s: Failure mismatch for subject %s", key, subj.value) } } }) } } // TestActionResult_AllIntentVerbs tests that ActionResult handles // all verbs used in the core intents. func TestActionResult_AllIntentVerbs(t *testing.T) { // Extract all unique verbs from intents verbs := make(map[string]bool) for _, intent := range coreIntents { if intent.Meta.Verb != "" { verbs[intent.Meta.Verb] = true } } subject := "test item" for verb := range verbs { t.Run(verb, func(t *testing.T) { result := ActionResult(verb, subject) // Should produce non-empty result assert.NotEmpty(t, result, "ActionResult(%q, %q) should not be empty", verb, subject) // Should start with title-cased subject assert.Contains(t, result, Title(subject), "ActionResult should contain title-cased subject") // Should contain past tense of verb past := PastTense(verb) assert.Contains(t, result, past, "ActionResult(%q) should contain past tense %q", verb, past) }) } } // TestActionFailed_AllIntentVerbs tests that ActionFailed handles // all verbs used in the core intents. func TestActionFailed_AllIntentVerbs(t *testing.T) { verbs := make(map[string]bool) for _, intent := range coreIntents { if intent.Meta.Verb != "" { verbs[intent.Meta.Verb] = true } } subject := "test item" for verb := range verbs { t.Run(verb, func(t *testing.T) { result := ActionFailed(verb, subject) // Should produce non-empty result assert.NotEmpty(t, result, "ActionFailed(%q, %q) should not be empty", verb, subject) // Should start with "Failed to" assert.Contains(t, result, "Failed to", "ActionFailed should contain 'Failed to'") // Should contain the verb assert.Contains(t, result, verb, "ActionFailed should contain the verb") // Should contain the subject assert.Contains(t, result, subject, "ActionFailed should contain the subject") }) } } // TestProgress_AllIntentVerbs tests that Progress handles // all verbs used in the core intents. func TestProgress_AllIntentVerbs(t *testing.T) { verbs := make(map[string]bool) for _, intent := range coreIntents { if intent.Meta.Verb != "" { verbs[intent.Meta.Verb] = true } } for verb := range verbs { t.Run(verb, func(t *testing.T) { result := Progress(verb) // Should produce non-empty result assert.NotEmpty(t, result, "Progress(%q) should not be empty", verb) // Should end with "..." assert.Contains(t, result, "...", "Progress should contain '...'") // Should contain gerund form gerund := Gerund(verb) assert.Contains(t, result, Title(gerund), "Progress(%q) should contain gerund %q", verb, gerund) }) } } // TestPastTense_AllIntentVerbs ensures PastTense works for all intent verbs. func TestPastTense_AllIntentVerbs(t *testing.T) { expected := map[string]string{ // Destructive "delete": "deleted", "remove": "removed", "discard": "discarded", "reset": "reset", "overwrite": "overwritten", // Creation "create": "created", "add": "added", "clone": "cloned", "copy": "copied", // Modification "save": "saved", "update": "updated", "rename": "renamed", "move": "moved", // Git "commit": "committed", "push": "pushed", "pull": "pulled", "merge": "merged", "rebase": "rebased", // Network "install": "installed", "download": "downloaded", "upload": "uploaded", "publish": "published", "deploy": "deployed", // Process "start": "started", "stop": "stopped", "restart": "restarted", "run": "ran", "build": "built", "test": "tested", // Info - these are regular verbs ending in consonant, -ed suffix "continue": "continued", "proceed": "proceeded", "confirm": "confirmed", // Additional "sync": "synced", "boot": "booted", "format": "formatted", "analyse": "analysed", "link": "linked", "unlink": "unlinked", "fetch": "fetched", "generate": "generated", "validate": "validated", "check": "checked", "scan": "scanned", } for verb, want := range expected { t.Run(verb, func(t *testing.T) { got := PastTense(verb) assert.Equal(t, want, got, "PastTense(%q)", verb) }) } } // TestGerund_AllIntentVerbs ensures Gerund works for all intent verbs. func TestGerund_AllIntentVerbs(t *testing.T) { expected := map[string]string{ // Destructive "delete": "deleting", "remove": "removing", "discard": "discarding", "reset": "resetting", "overwrite": "overwriting", // Creation "create": "creating", "add": "adding", "clone": "cloning", "copy": "copying", // Modification "save": "saving", "update": "updating", "rename": "renaming", "move": "moving", // Git "commit": "committing", "push": "pushing", "pull": "pulling", "merge": "merging", "rebase": "rebasing", // Network "install": "installing", "download": "downloading", "upload": "uploading", "publish": "publishing", "deploy": "deploying", // Process "start": "starting", "stop": "stopping", "restart": "restarting", "run": "running", "build": "building", "test": "testing", // Info "continue": "continuing", "proceed": "proceeding", "confirm": "confirming", // Additional "sync": "syncing", "boot": "booting", "format": "formatting", "analyse": "analysing", "link": "linking", "unlink": "unlinking", "fetch": "fetching", "generate": "generating", "validate": "validating", "check": "checking", "scan": "scanning", } for verb, want := range expected { t.Run(verb, func(t *testing.T) { got := Gerund(verb) assert.Equal(t, want, got, "Gerund(%q)", verb) }) } } // TestQuestionFormat verifies that standard question format // can be composed from verb and subject. func TestQuestionFormat(t *testing.T) { tests := []struct { verb string subject string expected string }{ {"delete", "config.yaml", "Delete config.yaml?"}, {"create", "src", "Create src?"}, {"commit", "changes", "Commit changes?"}, {"push", "5 commits", "Push 5 commits?"}, {"install", "package", "Install package?"}, } for _, tt := range tests { t.Run(tt.verb, func(t *testing.T) { // Standard question format: "Verb subject?" result := Title(tt.verb) + " " + tt.subject + "?" assert.Equal(t, tt.expected, result) }) } } // TestConfirmFormat verifies dangerous action confirm messages. func TestConfirmFormat(t *testing.T) { // Dangerous actions have "Really verb subject?" confirm dangerous := []string{"delete", "remove", "discard", "reset", "overwrite", "merge", "rebase", "publish", "deploy"} for _, verb := range dangerous { t.Run(verb, func(t *testing.T) { subject := "test item" // Basic confirm format result := "Really " + verb + " " + subject + "?" assert.Contains(t, result, "Really", "Dangerous action confirm should start with 'Really'") assert.Contains(t, result, verb) assert.Contains(t, result, subject) assert.Contains(t, result, "?") }) } } // TestIntentConsistency verifies patterns across all intents. func TestIntentConsistency(t *testing.T) { // These intents have non-standard question formats specialQuestions := map[string]bool{ "core.continue": true, // "Continue?" (no subject) "core.proceed": true, // "Proceed?" (no subject) "core.confirm": true, // "Are you sure?" (different format) } for key, intent := range coreIntents { t.Run(key, func(t *testing.T) { verb := intent.Meta.Verb // Verify verb is set assert.NotEmpty(t, verb, "intent should have a verb") // Verify Question contains the verb (unless special case) if !specialQuestions[key] { assert.Contains(t, intent.Question, Title(verb)+" ", "Question should contain '%s '", Title(verb)) } // Verify dangerous intents default to "no" if intent.Meta.Dangerous { assert.Equal(t, "no", intent.Meta.Default, "Dangerous intent should default to 'no'") } // Verify non-dangerous intents default to "yes" if !intent.Meta.Dangerous && intent.Meta.Type == "action" { assert.Equal(t, "yes", intent.Meta.Default, "Safe action intent should default to 'yes'") } }) } } // TestComposedVsManual compares template output with manual grammar composition. func TestComposedVsManual(t *testing.T) { tests := []struct { intentKey string noun string value string }{ {"core.delete", "file", "config.yaml"}, {"core.create", "directory", "src"}, {"core.save", "changes", "data"}, {"core.commit", "repo", "core-php"}, {"core.push", "branch", "feature/test"}, {"core.install", "package", "express"}, } for _, tt := range tests { t.Run(tt.intentKey, func(t *testing.T) { subject := S(tt.noun, tt.value) intent := coreIntents[tt.intentKey] // Compose using intent templates composed := composeIntent(intent, subject) // Manual composition using grammar functions manualSuccess := ActionResult(intent.Meta.Verb, tt.value) manualFailure := ActionFailed(intent.Meta.Verb, tt.value) assert.Equal(t, manualSuccess, composed.Success, "Template Success should match ActionResult()") assert.Equal(t, manualFailure, composed.Failure, "Template Failure should match ActionFailed()") }) } } // TestGrammarCanReplaceIntents demonstrates that the grammar engine // can compose all the standard output forms without hardcoded templates. // This proves the i18n system can work with just verb definitions. func TestGrammarCanReplaceIntents(t *testing.T) { tests := []struct { verb string subject string // Expected outputs that grammar should produce wantQuestion string wantSuccess string wantFailure string wantProgress string }{ { verb: "delete", subject: "config.yaml", wantQuestion: "Delete config.yaml?", wantSuccess: "Config.Yaml deleted", wantFailure: "Failed to delete config.yaml", wantProgress: "Deleting...", }, { verb: "create", subject: "project", wantQuestion: "Create project?", wantSuccess: "Project created", wantFailure: "Failed to create project", wantProgress: "Creating...", }, { verb: "build", subject: "app", wantQuestion: "Build app?", wantSuccess: "App built", wantFailure: "Failed to build app", wantProgress: "Building...", }, { verb: "run", subject: "tests", wantQuestion: "Run tests?", wantSuccess: "Tests ran", wantFailure: "Failed to run tests", wantProgress: "Running...", }, { verb: "commit", subject: "changes", wantQuestion: "Commit changes?", wantSuccess: "Changes committed", wantFailure: "Failed to commit changes", wantProgress: "Committing...", }, { verb: "overwrite", subject: "file", wantQuestion: "Overwrite file?", wantSuccess: "File overwritten", wantFailure: "Failed to overwrite file", wantProgress: "Overwriting...", }, { verb: "reset", subject: "state", wantQuestion: "Reset state?", wantSuccess: "State reset", wantFailure: "Failed to reset state", wantProgress: "Resetting...", }, } for _, tt := range tests { t.Run(tt.verb, func(t *testing.T) { // Compose using grammar functions only (no templates) question := Title(tt.verb) + " " + tt.subject + "?" success := ActionResult(tt.verb, tt.subject) failure := ActionFailed(tt.verb, tt.subject) progress := Progress(tt.verb) assert.Equal(t, tt.wantQuestion, question, "Question") assert.Equal(t, tt.wantSuccess, success, "Success") assert.Equal(t, tt.wantFailure, failure, "Failure") assert.Equal(t, tt.wantProgress, progress, "Progress") }) } } // TestProgressSubjectMatchesExpected tests ProgressSubject for all intent verbs. func TestProgressSubjectMatchesExpected(t *testing.T) { tests := []struct { verb string subject string want string }{ {"delete", "config.yaml", "Deleting config.yaml..."}, {"create", "project", "Creating project..."}, {"build", "app", "Building app..."}, {"install", "package", "Installing package..."}, {"commit", "changes", "Committing changes..."}, {"push", "commits", "Pushing commits..."}, {"pull", "updates", "Pulling updates..."}, {"sync", "files", "Syncing files..."}, {"fetch", "data", "Fetching data..."}, {"check", "status", "Checking status..."}, } for _, tt := range tests { t.Run(tt.verb, func(t *testing.T) { result := ProgressSubject(tt.verb, tt.subject) assert.Equal(t, tt.want, result) }) } }