- Add NewWithLoader(loader, opts...) for custom storage backends - Add Option type with WithFallback, WithFormality, WithHandlers, WithDefaultHandlers, WithMode, WithDebug options - Update New() and NewWithFS() to accept options - Add loader field to Service struct - Remove NewSubject() alias (use S() instead) - Add tests for new options and NewWithLoader Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
800 lines
22 KiB
Go
800 lines
22 KiB
Go
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("S with different value types", func(t *testing.T) {
|
|
s := S("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)
|
|
})
|
|
}
|
|
}
|
|
|