* 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>
814 lines
22 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|