cli/pkg/i18n/compose_test.go
Vi 27f8632867
feat: BugSETI app, WebSocket hub, browser automation, and MCP tools (#336)
* feat: add security logging and fix framework regressions

This commit implements comprehensive security event logging and resolves critical regressions in the core framework.

Security Logging:
- Enhanced `pkg/log` with a `Security` level and helper.
- Added `log.Username()` to consistently identify the executing user.
- Instrumented GitHub CLI auth, Agentic configuration, filesystem sandbox, MCP handlers, and MCP TCP transport with security logs.
- Added `SecurityStyle` to the CLI for consistent visual representation of security events.

UniFi Security (CodeQL):
- Refactored `pkg/unifi` to remove hardcoded `InsecureSkipVerify`, resolving a high-severity alert.
- Added a `--verify-tls` flag and configuration option to control TLS verification.
- Updated command handlers to support the new verification parameter.

Framework Fixes:
- Restored original signatures for `MustServiceFor`, `Config()`, and `Display()` in `pkg/framework/core`, which had been corrupted during a merge.
- Fixed `pkg/framework/framework.go` and `pkg/framework/core/runtime_pkg.go` to match the restored signatures.
- These fixes resolve project-wide compilation errors caused by the signature mismatches.

I encountered significant blockers due to a corrupted state of the `dev` branch after a merge, which introduced breaking changes in the core framework's DI system. I had to manually reconcile these signatures with the expected usage across the codebase to restore build stability.

* feat(mcp): add RAG tools (query, ingest, collections)

Add vector database tools to the MCP server for RAG operations:
- rag_query: Search for relevant documentation using semantic similarity
- rag_ingest: Ingest files or directories into the vector database
- rag_collections: List available collections

Uses existing internal/cmd/rag exports (QueryDocs, IngestDirectory, IngestFile)
and pkg/rag for Qdrant client access. Default collection is "hostuk-docs"
with topK=5 for queries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(mcp): add metrics tools (record, query)

Add MCP tools for recording and querying AI/security metrics events.
The metrics_record tool writes events to daily JSONL files, and the
metrics_query tool provides aggregated statistics by type, repo, and agent.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add 'core mcp serve' command

Add CLI command to start the MCP server for AI tool integration.

- Create internal/cmd/mcpcmd package with serve subcommand
- Support --workspace flag for directory restriction
- Handle SIGINT/SIGTERM for clean shutdown
- Register in full.go build variant

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(ws): add WebSocket hub package for real-time streaming

Add pkg/ws package implementing a hub pattern for WebSocket connections:
- Hub manages client connections, broadcasts, and channel subscriptions
- Client struct represents connected WebSocket clients
- Message types: process_output, process_status, event, error, ping/pong
- Channel-based subscription system (subscribe/unsubscribe)
- SendProcessOutput and SendProcessStatus for process streaming integration
- Full test coverage including concurrency tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(mcp): add process management and WebSocket MCP tools

Add MCP tools for process management:
- process_start: Start a new external process
- process_stop: Gracefully stop a running process
- process_kill: Force kill a process
- process_list: List all managed processes
- process_output: Get captured process output
- process_input: Send input to process stdin

Add MCP tools for WebSocket:
- ws_start: Start WebSocket server for real-time streaming
- ws_info: Get hub statistics (clients, channels)

Update Service struct with optional process.Service and ws.Hub fields,
new WithProcessService and WithWSHub options, getter methods, and
Shutdown method for cleanup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(webview): add browser automation package via Chrome DevTools Protocol

Add pkg/webview package for browser automation:
- webview.go: Main interface with Connect, Navigate, Click, Type, QuerySelector, Screenshot, Evaluate
- cdp.go: Chrome DevTools Protocol WebSocket client implementation
- actions.go: DOM action types (Click, Type, Hover, Scroll, etc.) and ActionSequence builder
- console.go: Console message capture and filtering with ConsoleWatcher and ExceptionWatcher
- angular.go: Angular-specific helpers for router navigation, component access, and Zone.js stability

Add MCP tools for webview:
- webview_connect/disconnect: Connection management
- webview_navigate: Page navigation
- webview_click/type/query/wait: DOM interaction
- webview_console: Console output capture
- webview_eval: JavaScript execution
- webview_screenshot: Screenshot capture

Add documentation:
- docs/mcp/angular-testing.md: Guide for Angular application testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: document new packages and BugSETI application

- Update CLAUDE.md with documentation for:
  - pkg/ws (WebSocket hub for real-time streaming)
  - pkg/webview (Browser automation via CDP)
  - pkg/mcp (MCP server tools: process, ws, webview)
  - BugSETI application overview
- Add comprehensive README for BugSETI with:
  - Installation and configuration guide
  - Usage workflow documentation
  - Architecture overview
  - Contributing guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bugseti): add BugSETI system tray app with auto-update

BugSETI - Distributed Bug Fixing like SETI@home but for code

Features:
- System tray app with Wails v3
- GitHub issue fetching with label filters
- Issue queue with priority management
- AI context seeding via seed-agent-developer skill
- Automated PR submission flow
- Stats tracking and leaderboard
- Cross-platform notifications
- Self-updating with stable/beta/nightly channels

Includes:
- cmd/bugseti: Main application with Angular frontend
- internal/bugseti: Core services (fetcher, queue, seeder, submit, config, stats, notify)
- internal/bugseti/updater: Auto-update system (checker, downloader, installer)
- .github/workflows/bugseti-release.yml: CI/CD for all platforms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve import cycle and code duplication

- Remove pkg/log import from pkg/io/local to break import cycle
  (pkg/log/rotation.go imports pkg/io, creating circular dependency)
- Use stderr logging for security events in sandbox escape detection
- Remove unused sync/atomic import from core.go
- Fix duplicate LogSecurity function declarations in cli/log.go
- Update workspace/service.go Crypt() call to match interface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update tests for new function signatures and format code

- Update core_test.go: Config(), Display() now panic instead of returning error
- Update runtime_pkg_test.go: sr.Config() now panics instead of returning error
- Update MustServiceFor tests to use assert.Panics
- Format BugSETI, MCP tools, and webview packages with gofmt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
Co-authored-by: Claude <developers@lethean.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 17:22:05 +00:00

814 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) {
// Clear locale env vars to ensure British English fallback (en-GB)
t.Setenv("LANG", "")
t.Setenv("LC_ALL", "")
t.Setenv("LC_MESSAGES", "")
// 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) {
// Clear locale env vars to ensure British English fallback (en-GB)
t.Setenv("LANG", "")
t.Setenv("LC_ALL", "")
t.Setenv("LC_MESSAGES", "")
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) {
// Clear locale env vars to ensure British English fallback (en-GB)
t.Setenv("LANG", "")
t.Setenv("LC_ALL", "")
t.Setenv("LC_MESSAGES", "")
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)
})
}
}