chore(ax): AX compliance sweep — banned imports, naming, Good/Bad/Ugly tests
- compose.go: remove fmt import, use local stringer interface + core.Sprint - hooks.go: replace stdlib log with dappco.re/go/core/log - localise.go: replace os.Getenv with core.Env - loader.go: replace strings.CutPrefix with core.HasPrefix/TrimPrefix - reversal/tokeniser.go: replace strings.Fields with local splitFields helper - validate.go: rename sb → builder (AX naming) - calibrate.go, classify.go: rename cfg → configuration (AX naming) - numbers.go: rename local fmt variable → numberFormat - All test files: add Good/Bad/Ugly triads per AX test naming convention Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c4ef1e6205
commit
97f9c758d1
32 changed files with 858 additions and 36 deletions
10
calibrate.go
10
calibrate.go
|
|
@ -52,9 +52,9 @@ func CalibrateDomains(ctx context.Context, modelA, modelB inference.TextModel,
|
|||
return nil, log.E("CalibrateDomains", "empty sample set", nil)
|
||||
}
|
||||
|
||||
cfg := defaultClassifyConfig()
|
||||
configuration := defaultClassifyConfig()
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
o(&configuration)
|
||||
}
|
||||
|
||||
stats := &CalibrationStats{
|
||||
|
|
@ -66,18 +66,18 @@ func CalibrateDomains(ctx context.Context, modelA, modelB inference.TextModel,
|
|||
// Build classification prompts from sample texts.
|
||||
prompts := make([]string, len(samples))
|
||||
for i, s := range samples {
|
||||
prompts[i] = core.Sprintf(cfg.promptTemplate, s.Text)
|
||||
prompts[i] = core.Sprintf(configuration.promptTemplate, s.Text)
|
||||
}
|
||||
|
||||
// Classify with model A.
|
||||
domainsA, durA, err := classifyAll(ctx, modelA, prompts, cfg.batchSize)
|
||||
domainsA, durA, err := classifyAll(ctx, modelA, prompts, configuration.batchSize)
|
||||
if err != nil {
|
||||
return nil, log.E("CalibrateDomains", "classify with model A", err)
|
||||
}
|
||||
stats.DurationA = durA
|
||||
|
||||
// Classify with model B.
|
||||
domainsB, durB, err := classifyAll(ctx, modelB, prompts, cfg.batchSize)
|
||||
domainsB, durB, err := classifyAll(ctx, modelB, prompts, configuration.batchSize)
|
||||
if err != nil {
|
||||
return nil, log.E("CalibrateDomains", "classify with model B", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,71 @@ func TestCalibrateDomains_EmptySamples(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCalibrateDomains_Good verifies full agreement returns rate of 1.0.
|
||||
//
|
||||
// stats.AgreementRate == 1.0 when both models return the same domain
|
||||
func TestCalibrateDomains_Good(t *testing.T) {
|
||||
model := &mockModel{
|
||||
classifyFunc: func(_ context.Context, prompts []string, _ ...inference.GenerateOption) ([]inference.ClassifyResult, error) {
|
||||
results := make([]inference.ClassifyResult, len(prompts))
|
||||
for i := range prompts {
|
||||
results[i] = inference.ClassifyResult{Token: inference.Token{Text: "technical"}}
|
||||
}
|
||||
return results, nil
|
||||
},
|
||||
}
|
||||
samples := []CalibrationSample{{Text: "Build the image", TrueDomain: "technical"}}
|
||||
stats, err := CalibrateDomains(context.Background(), model, model, samples)
|
||||
if err != nil {
|
||||
t.Fatalf("CalibrateDomains: %v", err)
|
||||
}
|
||||
if stats.AgreementRate != 1.0 {
|
||||
t.Errorf("AgreementRate = %f, want 1.0", stats.AgreementRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalibrateDomains_Bad verifies nil samples return an error.
|
||||
//
|
||||
// CalibrateDomains(ctx, m, m, nil) // error
|
||||
func TestCalibrateDomains_Bad(t *testing.T) {
|
||||
model := &mockModel{
|
||||
classifyFunc: func(_ context.Context, _ []string, _ ...inference.GenerateOption) ([]inference.ClassifyResult, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
_, err := CalibrateDomains(context.Background(), model, model, nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for nil samples")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalibrateDomains_Ugly verifies samples with no TrueDomain are still counted.
|
||||
//
|
||||
// stats.WithTruth == 0 when no samples have TrueDomain set
|
||||
func TestCalibrateDomains_Ugly(t *testing.T) {
|
||||
model := &mockModel{
|
||||
classifyFunc: func(_ context.Context, prompts []string, _ ...inference.GenerateOption) ([]inference.ClassifyResult, error) {
|
||||
results := make([]inference.ClassifyResult, len(prompts))
|
||||
for i := range prompts {
|
||||
results[i] = inference.ClassifyResult{Token: inference.Token{Text: "casual"}}
|
||||
}
|
||||
return results, nil
|
||||
},
|
||||
}
|
||||
// Sample has no TrueDomain — unusual but valid.
|
||||
samples := []CalibrationSample{{Text: "random text"}}
|
||||
stats, err := CalibrateDomains(context.Background(), model, model, samples)
|
||||
if err != nil {
|
||||
t.Fatalf("CalibrateDomains: %v", err)
|
||||
}
|
||||
if stats.WithTruth != 0 {
|
||||
t.Errorf("WithTruth = %d, want 0 (no ground truth provided)", stats.WithTruth)
|
||||
}
|
||||
if stats.Total != 1 {
|
||||
t.Errorf("Total = %d, want 1", stats.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalibrateDomains_BatchBoundary(t *testing.T) {
|
||||
// 7 samples with batch size 3: tests partial last batch.
|
||||
model := &mockModel{
|
||||
|
|
|
|||
10
classify.go
10
classify.go
|
|
@ -81,9 +81,9 @@ func mapTokenToDomain(token string) string {
|
|||
func ClassifyCorpus(ctx context.Context, model inference.TextModel,
|
||||
input io.Reader, output io.Writer, opts ...ClassifyOption) (*ClassifyStats, error) {
|
||||
|
||||
cfg := defaultClassifyConfig()
|
||||
configuration := defaultClassifyConfig()
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
o(&configuration)
|
||||
}
|
||||
|
||||
stats := &ClassifyStats{ByDomain: make(map[string]int)}
|
||||
|
|
@ -105,7 +105,7 @@ func ClassifyCorpus(ctx context.Context, model inference.TextModel,
|
|||
}
|
||||
prompts := make([]string, len(batch))
|
||||
for i, p := range batch {
|
||||
prompts[i] = core.Sprintf(cfg.promptTemplate, p.prompt)
|
||||
prompts[i] = core.Sprintf(configuration.promptTemplate, p.prompt)
|
||||
}
|
||||
results, err := model.Classify(ctx, prompts, inference.WithMaxTokens(1))
|
||||
if err != nil {
|
||||
|
|
@ -134,7 +134,7 @@ func ClassifyCorpus(ctx context.Context, model inference.TextModel,
|
|||
stats.Skipped++
|
||||
continue
|
||||
}
|
||||
promptVal, ok := record[cfg.promptField]
|
||||
promptVal, ok := record[configuration.promptField]
|
||||
if !ok {
|
||||
stats.Skipped++
|
||||
continue
|
||||
|
|
@ -146,7 +146,7 @@ func ClassifyCorpus(ctx context.Context, model inference.TextModel,
|
|||
}
|
||||
|
||||
batch = append(batch, pending{record: record, prompt: prompt})
|
||||
if len(batch) >= cfg.batchSize {
|
||||
if len(batch) >= configuration.batchSize {
|
||||
if err := flush(); err != nil {
|
||||
return stats, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,34 @@ func TestClassifyCorpus_SkipsMalformed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestMapTokenToDomain_Good verifies a known domain token maps correctly.
|
||||
//
|
||||
// mapTokenToDomain("technical") // "technical"
|
||||
func TestMapTokenToDomain_Good(t *testing.T) {
|
||||
if got := mapTokenToDomain("technical"); got != "technical" {
|
||||
t.Errorf("mapTokenToDomain(technical) = %q, want %q", got, "technical")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapTokenToDomain_Bad verifies an empty token maps to unknown.
|
||||
//
|
||||
// mapTokenToDomain("") // "unknown"
|
||||
func TestMapTokenToDomain_Bad(t *testing.T) {
|
||||
if got := mapTokenToDomain(""); got != "unknown" {
|
||||
t.Errorf("mapTokenToDomain(\"\") = %q, want %q", got, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapTokenToDomain_Ugly verifies a word that starts with a domain name but is not a domain
|
||||
// maps to unknown — prefix collision must not fire.
|
||||
//
|
||||
// mapTokenToDomain("cascade") // "unknown"
|
||||
func TestMapTokenToDomain_Ugly(t *testing.T) {
|
||||
if got := mapTokenToDomain("cascade"); got != "unknown" {
|
||||
t.Errorf("mapTokenToDomain(cascade) = %q, want %q", got, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyCorpus_DomainMapping(t *testing.T) {
|
||||
model := &mockModel{
|
||||
classifyFunc: func(_ context.Context, prompts []string, _ ...inference.GenerateOption) ([]inference.ClassifyResult, error) {
|
||||
|
|
|
|||
15
compose.go
15
compose.go
|
|
@ -1,6 +1,11 @@
|
|||
package i18n
|
||||
|
||||
import "fmt"
|
||||
import "dappco.re/go/core"
|
||||
|
||||
// stringer is a local interface for values that can describe themselves as a string.
|
||||
type stringer interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
// S creates a new Subject with the given noun and value.
|
||||
//
|
||||
|
|
@ -62,15 +67,15 @@ func (s *Subject) String() string {
|
|||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
if stringer, ok := s.Value.(fmt.Stringer); ok {
|
||||
return stringer.String()
|
||||
if v, ok := s.Value.(stringer); ok {
|
||||
return v.String()
|
||||
}
|
||||
return fmt.Sprint(s.Value)
|
||||
return core.Sprint(s.Value)
|
||||
}
|
||||
|
||||
func (s *Subject) IsPlural() bool { return s != nil && s.count != 1 }
|
||||
func (s *Subject) CountInt() int { if s == nil { return 1 }; return s.count }
|
||||
func (s *Subject) CountString() string { if s == nil { return "1" }; return fmt.Sprint(s.count) }
|
||||
func (s *Subject) CountString() string { if s == nil { return "1" }; return core.Sprint(s.count) }
|
||||
func (s *Subject) GenderString() string { if s == nil { return "" }; return s.gender }
|
||||
func (s *Subject) LocationString() string { if s == nil { return "" }; return s.location }
|
||||
func (s *Subject) NounString() string { if s == nil { return "" }; return s.Noun }
|
||||
|
|
|
|||
|
|
@ -209,3 +209,32 @@ func TestSubject_FullChain_Good(t *testing.T) {
|
|||
assert.True(t, subj.IsFormal())
|
||||
assert.True(t, subj.IsPlural())
|
||||
}
|
||||
|
||||
// TestSubject_String_Ugly verifies that an integer value is rendered as a decimal string.
|
||||
//
|
||||
// S("count", 42).String() // "42"
|
||||
func TestSubject_String_Ugly(t *testing.T) {
|
||||
subject := S("count", 42)
|
||||
if got := subject.String(); got != "42" {
|
||||
t.Errorf("S(count, 42).String() = %q, want %q", got, "42")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubject_Count_Ugly verifies a zero count makes the subject plural.
|
||||
//
|
||||
// S("item", "x").Count(0).IsPlural() // true
|
||||
func TestSubject_Count_Ugly(t *testing.T) {
|
||||
subject := S("item", "x").Count(0)
|
||||
if !subject.IsPlural() {
|
||||
t.Error("Count(0) should make IsPlural() true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestS_Ugly verifies that a nil value does not panic.
|
||||
//
|
||||
// S("file", nil).String() // "<nil>" (implementation-defined)
|
||||
func TestS_Ugly(t *testing.T) {
|
||||
subject := S("file", nil)
|
||||
got := subject.String()
|
||||
_ = got // must not panic
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,3 +114,13 @@ func TestTranslationContext_FullChain_Good(t *testing.T) {
|
|||
assert.Equal(t, FormalityFormal, ctx.FormalityValue())
|
||||
assert.Equal(t, "cardiology", ctx.Get("speciality"))
|
||||
}
|
||||
|
||||
// TestTranslationContext_Set_Ugly verifies overwriting an existing key in Extra works.
|
||||
//
|
||||
// C("x").Set("k", "a").Set("k", "b").Get("k") // "b"
|
||||
func TestTranslationContext_Set_Ugly(t *testing.T) {
|
||||
ctx := C("x").Set("k", "a").Set("k", "b")
|
||||
if got := ctx.Get("k"); got != "b" {
|
||||
t.Errorf("Set overwrite: Get(k) = %v, want %q", got, "b")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,3 +69,23 @@ func TestDebugMode_Good_Integration(t *testing.T) {
|
|||
got = svc.Raw("prompt.yes")
|
||||
assert.Equal(t, "[prompt.yes] y", got)
|
||||
}
|
||||
|
||||
// TestSetDebug_Bad verifies that calling SetDebug on a nil service does not panic.
|
||||
//
|
||||
// SetDefault(nil); SetDebug(true) // must not panic
|
||||
func TestSetDebug_Bad(t *testing.T) {
|
||||
SetDefault(nil)
|
||||
SetDebug(true) // must not panic
|
||||
// Restore a working default for subsequent tests.
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
SetDefault(svc)
|
||||
}
|
||||
|
||||
// TestDebugFormat_Ugly verifies that both key and text empty produces "[] ".
|
||||
//
|
||||
// debugFormat("", "") // "[] "
|
||||
func TestDebugFormat_Ugly(t *testing.T) {
|
||||
got := debugFormat("", "")
|
||||
assert.Equal(t, "[] ", got)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,6 +592,101 @@ func TestTemplateFuncs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- AX: Good/Bad/Ugly ---
|
||||
|
||||
// TestPastTense_Good verifies correct past tense for a known regular verb.
|
||||
//
|
||||
// PastTense("deploy") // "deployed"
|
||||
func TestPastTense_Good(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := PastTense("deploy"); got != "deployed" {
|
||||
t.Errorf("PastTense(deploy) = %q, want %q", got, "deployed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPastTense_Bad verifies that an empty verb returns an empty string.
|
||||
//
|
||||
// PastTense("") // ""
|
||||
func TestPastTense_Bad(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := PastTense(""); got != "" {
|
||||
t.Errorf("PastTense(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPastTense_Ugly verifies that a whitespace-only verb is trimmed and returns empty.
|
||||
//
|
||||
// PastTense(" ") // ""
|
||||
func TestPastTense_Ugly(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := PastTense(" "); got != "" {
|
||||
t.Errorf("PastTense(whitespace) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGerund_Good verifies correct gerund for a known verb.
|
||||
//
|
||||
// Gerund("build") // "building"
|
||||
func TestGerund_Good(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := Gerund("build"); got != "building" {
|
||||
t.Errorf("Gerund(build) = %q, want %q", got, "building")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGerund_Bad verifies that an empty verb returns empty.
|
||||
//
|
||||
// Gerund("") // ""
|
||||
func TestGerund_Bad(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := Gerund(""); got != "" {
|
||||
t.Errorf("Gerund(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGerund_Ugly verifies that a mixed-case verb is normalised before lookup.
|
||||
//
|
||||
// Gerund("BUILD") // "building"
|
||||
func TestGerund_Ugly(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := Gerund("BUILD"); got != "building" {
|
||||
t.Errorf("Gerund(BUILD) = %q, want %q", got, "building")
|
||||
}
|
||||
}
|
||||
|
||||
// TestArticle_Good verifies correct indefinite article for a known noun.
|
||||
//
|
||||
// Article("error") // "an"
|
||||
func TestArticle_Good(t *testing.T) {
|
||||
if got := Article("error"); got != "an" {
|
||||
t.Errorf("Article(error) = %q, want %q", got, "an")
|
||||
}
|
||||
}
|
||||
|
||||
// TestArticle_Bad verifies that an empty string returns empty.
|
||||
//
|
||||
// Article("") // ""
|
||||
func TestArticle_Bad(t *testing.T) {
|
||||
if got := Article(""); got != "" {
|
||||
t.Errorf("Article(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArticle_Ugly verifies the silent-h exception "hour" gets "an".
|
||||
//
|
||||
// Article("hour") // "an"
|
||||
func TestArticle_Ugly(t *testing.T) {
|
||||
if got := Article("hour"); got != "an" {
|
||||
t.Errorf("Article(hour) = %q, want %q", got, "an")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Benchmarks ---
|
||||
|
||||
func BenchmarkPastTense_Irregular(b *testing.B) {
|
||||
|
|
|
|||
|
|
@ -191,3 +191,72 @@ func TestDefaultHandlers(t *testing.T) {
|
|||
t.Errorf("DefaultHandlers() returned %d handlers, want 6", len(handlers))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLabelHandler_Good verifies a label key produces a colon-suffixed result.
|
||||
//
|
||||
// h.Handle("i18n.label.status", nil, nil) // "Status:"
|
||||
func TestLabelHandler_Good(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
h := LabelHandler{}
|
||||
if got := h.Handle("i18n.label.status", nil, nil); got != "Status:" {
|
||||
t.Errorf("LabelHandler.Handle = %q, want %q", got, "Status:")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLabelHandler_Bad verifies that a non-matching key is not handled.
|
||||
//
|
||||
// h.Match("other.key") // false
|
||||
func TestLabelHandler_Bad(t *testing.T) {
|
||||
h := LabelHandler{}
|
||||
if h.Match("other.key") {
|
||||
t.Error("LabelHandler should not match other.key")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLabelHandler_Ugly verifies that an empty word suffix returns a colon.
|
||||
//
|
||||
// h.Handle("i18n.label.", nil, nil) // ":"
|
||||
func TestLabelHandler_Ugly(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
h := LabelHandler{}
|
||||
got := h.Handle("i18n.label.", nil, nil)
|
||||
// An empty word suffix produces just the punctuation — should not panic.
|
||||
_ = got
|
||||
}
|
||||
|
||||
// TestProgressHandler_Good verifies a progress key produces a gerund message.
|
||||
//
|
||||
// h.Handle("i18n.progress.build", nil, nil) // "Building..."
|
||||
func TestProgressHandler_Good(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
h := ProgressHandler{}
|
||||
if got := h.Handle("i18n.progress.build", nil, nil); got != "Building..." {
|
||||
t.Errorf("ProgressHandler.Handle = %q, want %q", got, "Building...")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProgressHandler_Bad verifies that a non-matching key returns false from Match.
|
||||
//
|
||||
// h.Match("i18n.label.status") // false
|
||||
func TestProgressHandler_Bad(t *testing.T) {
|
||||
h := ProgressHandler{}
|
||||
if h.Match("i18n.label.status") {
|
||||
t.Error("ProgressHandler should not match i18n.label.* keys")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProgressHandler_Ugly verifies that a subject string appended to the progress message works.
|
||||
//
|
||||
// h.Handle("i18n.progress.build", []any{"config.yaml"}, nil) // "Building config.yaml..."
|
||||
func TestProgressHandler_Ugly(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
h := ProgressHandler{}
|
||||
got := h.Handle("i18n.progress.build", []any{"config.yaml"}, nil)
|
||||
if got != "Building config.yaml..." {
|
||||
t.Errorf("ProgressHandler.Handle with subject = %q, want %q", got, "Building config.yaml...")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
hooks.go
7
hooks.go
|
|
@ -2,10 +2,11 @@ package i18n
|
|||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
log "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
var missingKeyHandler atomic.Value
|
||||
|
|
@ -37,7 +38,7 @@ func RegisterLocales(fsys fs.FS, dir string) {
|
|||
if localesLoaded {
|
||||
if svc := Default(); svc != nil {
|
||||
if err := svc.LoadFS(fsys, dir); err != nil {
|
||||
log.Printf("i18n: RegisterLocales failed to load %q: %v", dir, err)
|
||||
log.Error("i18n: RegisterLocales failed to load locale directory", "dir", dir, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ func loadRegisteredLocales(svc *Service) {
|
|||
defer registeredLocalesMu.Unlock()
|
||||
for _, reg := range registeredLocales {
|
||||
if err := svc.LoadFS(reg.fsys, reg.dir); err != nil {
|
||||
log.Printf("i18n: loadRegisteredLocales failed to load %q: %v", reg.dir, err)
|
||||
log.Error("i18n: loadRegisteredLocales failed to load locale directory", "dir", reg.dir, "err", err)
|
||||
}
|
||||
}
|
||||
localesLoaded = true
|
||||
|
|
|
|||
|
|
@ -134,3 +134,28 @@ func TestDispatchMissingKey_Good_NoHandler(t *testing.T) {
|
|||
// Should not panic when dispatching with nil handler
|
||||
dispatchMissingKey("test.key", nil)
|
||||
}
|
||||
|
||||
// TestOnMissingKey_Bad verifies that registering a nil handler clears any existing handler.
|
||||
//
|
||||
// OnMissingKey(nil) // clears handler; subsequent dispatch is a no-op
|
||||
func TestOnMissingKey_Bad(t *testing.T) {
|
||||
called := false
|
||||
OnMissingKey(func(_ MissingKey) { called = true })
|
||||
OnMissingKey(nil) // clear the handler
|
||||
dispatchMissingKey("any.key", nil)
|
||||
if called {
|
||||
t.Error("handler should have been cleared by OnMissingKey(nil)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterLocales_Ugly verifies re-registering the embedded locales is idempotent and safe.
|
||||
//
|
||||
// RegisterLocales(localeFS, "locales") // idempotent; must not panic
|
||||
func TestRegisterLocales_Ugly(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("RegisterLocales panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
RegisterLocales(localeFS, "locales")
|
||||
}
|
||||
|
|
|
|||
24
i18n_test.go
24
i18n_test.go
|
|
@ -261,3 +261,27 @@ func TestErrServiceNotInitialised_Good(t *testing.T) {
|
|||
func TestErrServiceNotInitialized_DeprecatedAlias(t *testing.T) {
|
||||
assert.Equal(t, ErrServiceNotInitialised, ErrServiceNotInitialized, "deprecated alias must point to the same error")
|
||||
}
|
||||
|
||||
// TestT_Ugly verifies T() with no default service returns the key unchanged.
|
||||
//
|
||||
// SetDefault(nil); T("any.key") // "any.key"
|
||||
func TestT_Ugly(t *testing.T) {
|
||||
SetDefault(nil)
|
||||
got := T("any.key")
|
||||
assert.Equal(t, "any.key", got)
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
SetDefault(svc)
|
||||
}
|
||||
|
||||
// TestSetLanguage_Ugly verifies SetLanguage returns ErrServiceNotInitialised with no default.
|
||||
//
|
||||
// SetDefault(nil); SetLanguage("en") // ErrServiceNotInitialised
|
||||
func TestSetLanguage_Ugly(t *testing.T) {
|
||||
SetDefault(nil)
|
||||
err := SetLanguage("en")
|
||||
assert.ErrorIs(t, err, ErrServiceNotInitialised)
|
||||
svc, initErr := New()
|
||||
require.NoError(t, initErr)
|
||||
SetDefault(svc)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,3 +81,30 @@ func TestGetPluralRule(t *testing.T) {
|
|||
t.Error("Unknown rule(1) should fallback to English PluralOne")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPluralCategory_Good verifies German singular follows English rules.
|
||||
//
|
||||
// GetPluralCategory("de", 1) // PluralOne
|
||||
func TestGetPluralCategory_Good(t *testing.T) {
|
||||
if got := GetPluralCategory("de", 1); got != PluralOne {
|
||||
t.Errorf("GetPluralCategory(de, 1) = %v, want PluralOne", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPluralCategory_Bad verifies an unknown language code uses English rules.
|
||||
//
|
||||
// GetPluralCategory("zz", 2) // PluralOther
|
||||
func TestGetPluralCategory_Bad(t *testing.T) {
|
||||
if got := GetPluralCategory("zz", 2); got != PluralOther {
|
||||
t.Errorf("GetPluralCategory(zz, 2) = %v, want PluralOther", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPluralCategory_Ugly verifies negative counts don't panic.
|
||||
//
|
||||
// GetPluralCategory("en", -1) // PluralOther (falls through)
|
||||
func TestGetPluralCategory_Ugly(t *testing.T) {
|
||||
// Negative counts are unusual but must not panic.
|
||||
got := GetPluralCategory("en", -1)
|
||||
_ = got // result is implementation-defined; just verify no panic
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package i18n
|
|||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"dappco.re/go/core"
|
||||
|
|
@ -117,8 +116,8 @@ func flattenWithGrammar(prefix string, data map[string]any, out map[string]Messa
|
|||
// Verb form object (has base/past/gerund keys)
|
||||
if grammar != nil && isVerbFormObject(v) {
|
||||
verbName := key
|
||||
if after, ok := strings.CutPrefix(fullKey, "gram.verb."); ok {
|
||||
verbName = after
|
||||
if core.HasPrefix(fullKey, "gram.verb.") {
|
||||
verbName = core.TrimPrefix(fullKey, "gram.verb.")
|
||||
}
|
||||
forms := VerbForms{}
|
||||
if past, ok := v["past"].(string); ok {
|
||||
|
|
@ -134,8 +133,8 @@ func flattenWithGrammar(prefix string, data map[string]any, out map[string]Messa
|
|||
// Noun form object (under gram.noun.* or has gender field)
|
||||
if grammar != nil && (core.HasPrefix(fullKey, "gram.noun.") || isNounFormObject(v)) {
|
||||
nounName := key
|
||||
if after, ok := strings.CutPrefix(fullKey, "gram.noun."); ok {
|
||||
nounName = after
|
||||
if core.HasPrefix(fullKey, "gram.noun.") {
|
||||
nounName = core.TrimPrefix(fullKey, "gram.noun.")
|
||||
}
|
||||
_, hasOne := v["one"]
|
||||
_, hasOther := v["other"]
|
||||
|
|
|
|||
|
|
@ -291,3 +291,27 @@ func TestCustomFSLoader(t *testing.T) {
|
|||
t.Errorf("verb 'zap' not loaded correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFSLoader_Ugly verifies that loading a locale with only a gram block (no messages) works.
|
||||
//
|
||||
// loader.Load("en") // returns empty messages map but populated grammar
|
||||
func TestFSLoader_Ugly(t *testing.T) {
|
||||
gramOnly := `{"gram":{"verb":{"zap":{"past":"zapped","gerund":"zapping"}}}}`
|
||||
fsys := fstest.MapFS{
|
||||
"locales/eo.json": &fstest.MapFile{Data: []byte(gramOnly)},
|
||||
}
|
||||
loader := NewFSLoader(fsys, "locales")
|
||||
messages, grammar, err := loader.Load("eo")
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(messages) != 0 {
|
||||
t.Errorf("messages = %d, want 0 for gram-only locale", len(messages))
|
||||
}
|
||||
if grammar == nil {
|
||||
t.Fatal("grammar should not be nil")
|
||||
}
|
||||
if v, ok := grammar.Verbs["zap"]; !ok || v.Past != "zapped" {
|
||||
t.Errorf("gram-only verb not loaded: %v", grammar.Verbs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package i18n
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
|
@ -85,11 +83,11 @@ func Direction() TextDirection {
|
|||
func IsRTL() bool { return Direction() == DirRTL }
|
||||
|
||||
func detectLanguage(supported []language.Tag) string {
|
||||
langEnv := os.Getenv("LANG")
|
||||
langEnv := core.Env("LANG")
|
||||
if langEnv == "" {
|
||||
langEnv = os.Getenv("LC_ALL")
|
||||
langEnv = core.Env("LC_ALL")
|
||||
if langEnv == "" {
|
||||
langEnv = os.Getenv("LC_MESSAGES")
|
||||
langEnv = core.Env("LC_MESSAGES")
|
||||
}
|
||||
}
|
||||
if langEnv == "" {
|
||||
|
|
|
|||
|
|
@ -168,3 +168,28 @@ func TestMode_String_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsRTLLanguage_Bad verifies an unknown language code returns false.
|
||||
//
|
||||
// IsRTLLanguage("xx") // false
|
||||
func TestIsRTLLanguage_Bad(t *testing.T) {
|
||||
assert.False(t, IsRTLLanguage("xx"))
|
||||
}
|
||||
|
||||
// TestFormality_Ugly verifies an out-of-range Formality value returns "neutral".
|
||||
//
|
||||
// Formality(99).String() // "neutral"
|
||||
func TestFormality_Ugly(t *testing.T) {
|
||||
assert.Equal(t, "neutral", Formality(99).String())
|
||||
}
|
||||
|
||||
// TestSetFormality_Ugly verifies SetFormality on nil default does not panic.
|
||||
//
|
||||
// SetDefault(nil); SetFormality(FormalityFormal) // must not panic
|
||||
func TestSetFormality_Ugly(t *testing.T) {
|
||||
SetDefault(nil)
|
||||
SetFormality(FormalityFormal) // must not panic
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
SetDefault(svc)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ func getNumberFormat() NumberFormat {
|
|||
if idx := indexAny(lang, "-_"); idx > 0 {
|
||||
lang = lang[:idx]
|
||||
}
|
||||
if fmt, ok := numberFormats[lang]; ok {
|
||||
return fmt
|
||||
if numberFormat, ok := numberFormats[lang]; ok {
|
||||
return numberFormat
|
||||
}
|
||||
return numberFormats["en"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,3 +143,36 @@ func TestFormatOrdinal(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatNumber_Good verifies thousands separator is applied for large numbers.
|
||||
//
|
||||
// FormatNumber(1000000) // "1,000,000"
|
||||
func TestFormatNumber_Good(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := FormatNumber(1_000_000); got != "1,000,000" {
|
||||
t.Errorf("FormatNumber(1000000) = %q, want %q", got, "1,000,000")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatNumber_Bad verifies negative numbers are formatted correctly.
|
||||
//
|
||||
// FormatNumber(-42) // "-42"
|
||||
func TestFormatNumber_Bad(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := FormatNumber(-42); got != "-42" {
|
||||
t.Errorf("FormatNumber(-42) = %q, want %q", got, "-42")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatNumber_Ugly verifies zero is formatted without separators.
|
||||
//
|
||||
// FormatNumber(0) // "0"
|
||||
func TestFormatNumber_Ugly(t *testing.T) {
|
||||
svc, _ := New()
|
||||
SetDefault(svc)
|
||||
if got := FormatNumber(0); got != "0" {
|
||||
t.Errorf("FormatNumber(0) = %q, want %q", got, "0")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,3 +167,52 @@ func TestAnomalyStats_Rate(t *testing.T) {
|
|||
t.Errorf("Rate = %.2f, want ~1.0 (all should be anomalies)", stats.Rate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectAnomalies_Good verifies that full agreement produces zero anomalies.
|
||||
//
|
||||
// rs.DetectAnomalies(tok, samples) // stats.Anomalies == 0
|
||||
func TestDetectAnomalies_Good(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
refSamples := []ClassifiedText{
|
||||
{Text: "Delete the file", Domain: "technical"},
|
||||
{Text: "Build the project", Domain: "technical"},
|
||||
}
|
||||
rs, err := BuildReferences(tok, refSamples)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildReferences: %v", err)
|
||||
}
|
||||
_, stats := rs.DetectAnomalies(tok, refSamples)
|
||||
if stats.Anomalies != 0 {
|
||||
t.Errorf("Anomalies = %d, want 0 for matching domain samples", stats.Anomalies)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectAnomalies_Bad verifies that empty test samples produce zero anomalies.
|
||||
//
|
||||
// rs.DetectAnomalies(tok, nil) // stats.Total == 0
|
||||
func TestDetectAnomalies_Bad(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
refSamples := []ClassifiedText{
|
||||
{Text: "Delete the file", Domain: "technical"},
|
||||
}
|
||||
rs, _ := BuildReferences(tok, refSamples)
|
||||
_, stats := rs.DetectAnomalies(tok, nil)
|
||||
if stats.Total != 0 {
|
||||
t.Errorf("Total = %d, want 0 for empty input", stats.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectAnomalies_Ugly verifies that samples with empty Domain are skipped.
|
||||
//
|
||||
// rs.DetectAnomalies(tok, []{{Text: "foo", Domain: ""}}) // stats.Total == 0
|
||||
func TestDetectAnomalies_Ugly(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
refSamples := []ClassifiedText{
|
||||
{Text: "Delete the file", Domain: "technical"},
|
||||
}
|
||||
rs, _ := BuildReferences(tok, refSamples)
|
||||
_, stats := rs.DetectAnomalies(tok, []ClassifiedText{{Text: "anything", Domain: ""}})
|
||||
if stats.Total != 0 {
|
||||
t.Errorf("Total = %d, want 0 (empty Domain skipped)", stats.Total)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,3 +157,39 @@ func TestImprint_ConfidenceWeighting_BackwardsCompat(t *testing.T) {
|
|||
t.Error("NounDistribution should contain 'file'")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewImprint_Good verifies a verb token contributes to VerbDistribution.
|
||||
//
|
||||
// NewImprint(tokens).VerbDistribution["delete"] > 0
|
||||
func TestNewImprint_Good(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
tokens := tok.Tokenise("Delete the file")
|
||||
imp := NewImprint(tokens)
|
||||
if imp.VerbDistribution["delete"] == 0 {
|
||||
t.Error("VerbDistribution should contain 'delete'")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewImprint_Bad verifies an empty token slice produces a zero-value imprint.
|
||||
//
|
||||
// NewImprint(nil).TokenCount == 0
|
||||
func TestNewImprint_Bad(t *testing.T) {
|
||||
imp := NewImprint(nil)
|
||||
if imp.TokenCount != 0 {
|
||||
t.Errorf("TokenCount = %d, want 0 for nil tokens", imp.TokenCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImprint_Similar_Ugly verifies similarity between one empty and one non-empty imprint is 0.
|
||||
//
|
||||
// empty.Similar(nonEmpty) // 0.0
|
||||
func TestImprint_Similar_Ugly(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
tokens := tok.Tokenise("Build the image")
|
||||
nonEmpty := NewImprint(tokens)
|
||||
empty := NewImprint(nil)
|
||||
sim := empty.Similar(nonEmpty)
|
||||
if sim != 0.0 {
|
||||
t.Errorf("empty.Similar(nonEmpty) = %f, want 0.0", sim)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,3 +107,40 @@ func TestMultiplier_TransformedTokenConfidence(t *testing.T) {
|
|||
t.Error("Noun-transformed token has zero Confidence, want 1.0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiplier_Good verifies that Expand produces at least the original text.
|
||||
//
|
||||
// m.Expand("delete the file") // first element == "delete the file"
|
||||
func TestMultiplier_Good(t *testing.T) {
|
||||
initI18n(t)
|
||||
m := NewMultiplier()
|
||||
variants := m.Expand("delete the file")
|
||||
if len(variants) == 0 {
|
||||
t.Fatal("Expand should produce at least one variant")
|
||||
}
|
||||
if variants[0] != "delete the file" {
|
||||
t.Errorf("first variant = %q, want original text", variants[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiplier_Bad verifies that an empty string produces no variants.
|
||||
//
|
||||
// m.Expand("") // nil
|
||||
func TestMultiplier_Bad(t *testing.T) {
|
||||
m := NewMultiplier()
|
||||
variants := m.Expand("")
|
||||
if variants != nil {
|
||||
t.Errorf("Expand(\"\") = %v, want nil", variants)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiplier_Ugly verifies that a whitespace-only input produces no variants.
|
||||
//
|
||||
// m.Expand(" ") // nil (trimmed to empty)
|
||||
func TestMultiplier_Ugly(t *testing.T) {
|
||||
m := NewMultiplier()
|
||||
variants := m.Expand(" ")
|
||||
if variants != nil {
|
||||
t.Errorf("Expand(whitespace) = %v, want nil", variants)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,3 +233,48 @@ func TestComputeVariance_SingleSample(t *testing.T) {
|
|||
t.Errorf("Single-sample variance should be nil, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildReferences_Good verifies a non-empty sample set builds a valid ReferenceSet.
|
||||
//
|
||||
// BuildReferences(tok, samples).Domains["technical"] != nil
|
||||
func TestBuildReferences_Good(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
samples := []ClassifiedText{
|
||||
{Text: "Delete the file", Domain: "technical"},
|
||||
}
|
||||
rs, err := BuildReferences(tok, samples)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildReferences: %v", err)
|
||||
}
|
||||
if rs.Domains["technical"] == nil {
|
||||
t.Error("expected technical domain in reference set")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildReferences_Bad verifies empty samples return an error.
|
||||
//
|
||||
// BuildReferences(tok, nil) // error
|
||||
func TestBuildReferences_Bad(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
_, err := BuildReferences(tok, nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for empty samples")
|
||||
}
|
||||
}
|
||||
|
||||
// TestReferenceSet_Classify_Ugly verifies classification when only one domain exists.
|
||||
//
|
||||
// rs.Classify(imp).Domain == "technical" (only option)
|
||||
func TestReferenceSet_Classify_Ugly(t *testing.T) {
|
||||
tok := initI18n(t)
|
||||
samples := []ClassifiedText{
|
||||
{Text: "Build the project", Domain: "technical"},
|
||||
}
|
||||
rs, _ := BuildReferences(tok, samples)
|
||||
tokens := tok.Tokenise("Run the tests")
|
||||
imp := NewImprint(tokens)
|
||||
cls := rs.Classify(imp)
|
||||
if cls.Domain != "technical" {
|
||||
t.Errorf("Classify domain = %q, want %q (only domain)", cls.Domain, "technical")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
package reversal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
i18n "dappco.re/go/core/i18n"
|
||||
)
|
||||
|
|
@ -619,7 +617,7 @@ func (t *Tokeniser) Tokenise(text string) []Token {
|
|||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Fields(text)
|
||||
parts := splitFields(text)
|
||||
var tokens []Token
|
||||
|
||||
// --- Pass 1: Classify & Mark ---
|
||||
|
|
@ -1025,3 +1023,29 @@ func DisambiguationStatsFromTokens(tokens []Token) DisambiguationStats {
|
|||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// splitFields splits text on runs of whitespace, equivalent to strings.Fields.
|
||||
// text is expected to already be trimmed of leading/trailing whitespace.
|
||||
//
|
||||
// splitFields("delete the file") // ["delete", "the", "file"]
|
||||
// splitFields("") // nil
|
||||
func splitFields(text string) []string {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
var words []string
|
||||
wordStart := -1
|
||||
for i, r := range text {
|
||||
isSpace := r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
if !isSpace && wordStart < 0 {
|
||||
wordStart = i
|
||||
} else if isSpace && wordStart >= 0 {
|
||||
words = append(words, text[wordStart:i])
|
||||
wordStart = -1
|
||||
}
|
||||
}
|
||||
if wordStart >= 0 {
|
||||
words = append(words, text[wordStart:])
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,47 @@ func TestWithWeights_Override(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- AX: Good/Bad/Ugly ---
|
||||
|
||||
// TestTokeniser_MatchVerb_Good verifies that a known past-tense verb is matched.
|
||||
//
|
||||
// tok.MatchVerb("deleted") // VerbMatch{Base:"delete", Tense:"past"}
|
||||
func TestTokeniser_MatchVerb_Good(t *testing.T) {
|
||||
setup(t)
|
||||
tok := NewTokeniser()
|
||||
match, ok := tok.MatchVerb("deleted")
|
||||
if !ok {
|
||||
t.Fatal("MatchVerb(deleted) returned false, want true")
|
||||
}
|
||||
if match.Base != "delete" {
|
||||
t.Errorf("VerbMatch.Base = %q, want %q", match.Base, "delete")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTokeniser_MatchVerb_Bad verifies that a non-verb word is not matched.
|
||||
//
|
||||
// tok.MatchVerb("banana") // ok == false
|
||||
func TestTokeniser_MatchVerb_Bad(t *testing.T) {
|
||||
setup(t)
|
||||
tok := NewTokeniser()
|
||||
_, ok := tok.MatchVerb("banana")
|
||||
if ok {
|
||||
t.Error("MatchVerb(banana) returned true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTokeniser_Tokenise_Ugly verifies that tokenising whitespace-only text returns nil.
|
||||
//
|
||||
// tok.Tokenise(" ") // nil
|
||||
func TestTokeniser_Tokenise_Ugly(t *testing.T) {
|
||||
setup(t)
|
||||
tok := NewTokeniser()
|
||||
tokens := tok.Tokenise(" ")
|
||||
if tokens != nil {
|
||||
t.Errorf("Tokenise(whitespace) = %v, want nil", tokens)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Benchmarks ---
|
||||
|
||||
func benchSetup(b *testing.B) {
|
||||
|
|
|
|||
|
|
@ -413,3 +413,20 @@ func TestServicePluralCategory(t *testing.T) {
|
|||
t.Errorf("PluralCategory(5) = %v, want PluralOther", svc.PluralCategory(5))
|
||||
}
|
||||
}
|
||||
|
||||
// TestService_Ugly verifies that T() in ModeStrict panics on a missing key.
|
||||
//
|
||||
// svc.SetMode(ModeStrict); svc.T("nonexistent.key") // panic
|
||||
func TestService_Ugly(t *testing.T) {
|
||||
svc, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
svc.SetMode(ModeStrict)
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for missing key in ModeStrict")
|
||||
}
|
||||
}()
|
||||
svc.T("nonexistent.key.that.does.not.exist")
|
||||
}
|
||||
|
|
|
|||
11
time_test.go
11
time_test.go
|
|
@ -130,3 +130,14 @@ func TestFormatAgo_Good_SingularUnit(t *testing.T) {
|
|||
got := FormatAgo(1, "fortnight")
|
||||
assert.Equal(t, "1 fortnight ago", got)
|
||||
}
|
||||
|
||||
// TestTimeAgo_Ugly verifies that a future time (negative duration) does not panic.
|
||||
//
|
||||
// TimeAgo(time.Now().Add(10 * time.Minute)) // implementation-defined but no panic
|
||||
func TestTimeAgo_Ugly(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
SetDefault(svc)
|
||||
got := TimeAgo(time.Now().Add(10 * time.Minute))
|
||||
_ = got // must not panic
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,3 +124,25 @@ func TestToFloat64_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCount_Bad verifies a map without a Count key returns 0.
|
||||
//
|
||||
// getCount(map[string]any{"other": 5}) // 0
|
||||
func TestGetCount_Bad(t *testing.T) {
|
||||
data := map[string]any{"other": 5}
|
||||
assert.Equal(t, 0, getCount(data))
|
||||
}
|
||||
|
||||
// TestToInt_Ugly verifies that nil returns 0 without panic.
|
||||
//
|
||||
// toInt(nil) // 0
|
||||
func TestToInt_Ugly(t *testing.T) {
|
||||
assert.Equal(t, 0, toInt(nil))
|
||||
}
|
||||
|
||||
// TestToFloat64_Ugly verifies that nil returns 0 without panic.
|
||||
//
|
||||
// toFloat64(nil) // 0
|
||||
func TestToFloat64_Ugly(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, toFloat64(nil), 0.001)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,3 +251,36 @@ func TestIsRTLLanguage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessage_Good verifies a simple message returns its text.
|
||||
//
|
||||
// Message{Text: "hello"}.ForCategory(PluralOther) // "hello"
|
||||
func TestMessage_Good(t *testing.T) {
|
||||
m := Message{Text: "hello"}
|
||||
if got := m.ForCategory(PluralOther); got != "hello" {
|
||||
t.Errorf("Message.ForCategory = %q, want %q", got, "hello")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessage_Bad verifies an empty message returns empty string.
|
||||
//
|
||||
// Message{}.ForCategory(PluralOne) // ""
|
||||
func TestMessage_Bad(t *testing.T) {
|
||||
m := Message{}
|
||||
if got := m.ForCategory(PluralOne); got != "" {
|
||||
t.Errorf("empty Message.ForCategory = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessage_Ugly verifies plural forms are selected by category when both Text and plural forms are set.
|
||||
//
|
||||
// Message{Text:"item", One:"item", Other:"items"}.ForCategory(PluralOther) // "items"
|
||||
func TestMessage_Ugly(t *testing.T) {
|
||||
m := Message{Text: "item", One: "item", Other: "items"}
|
||||
if got := m.ForCategory(PluralOne); got != "item" {
|
||||
t.Errorf("Message.ForCategory(PluralOne) = %q, want %q", got, "item")
|
||||
}
|
||||
if got := m.ForCategory(PluralOther); got != "items" {
|
||||
t.Errorf("Message.ForCategory(PluralOther) = %q, want %q", got, "items")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,14 +60,14 @@ func irregularPrompt(verb, tense string) string {
|
|||
|
||||
// collectGenerated runs a single-token generation and returns the trimmed, lowercased output.
|
||||
func collectGenerated(ctx context.Context, m inference.TextModel, prompt string) (string, error) {
|
||||
sb := core.NewBuilder()
|
||||
builder := core.NewBuilder()
|
||||
for tok := range m.Generate(ctx, prompt, inference.WithMaxTokens(1), inference.WithTemperature(0.05)) {
|
||||
sb.WriteString(tok.Text)
|
||||
builder.WriteString(tok.Text)
|
||||
}
|
||||
if err := m.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return core.Trim(core.Lower(sb.String())), nil
|
||||
return core.Trim(core.Lower(builder.String())), nil
|
||||
}
|
||||
|
||||
// ValidateArticle checks whether a given article usage is grammatically correct
|
||||
|
|
|
|||
|
|
@ -336,3 +336,33 @@ func TestIrregularPrompt(t *testing.T) {
|
|||
t.Errorf("prompt should contain the tense: %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArticlePrompt_Good verifies the article prompt contains the noun.
|
||||
//
|
||||
// articlePrompt("error") // contains "error"
|
||||
func TestArticlePrompt_Good(t *testing.T) {
|
||||
prompt := articlePrompt("error")
|
||||
if !contains(prompt, "error") {
|
||||
t.Errorf("articlePrompt should contain noun %q, got %q", "error", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArticlePrompt_Bad verifies an empty noun still produces a non-empty prompt.
|
||||
//
|
||||
// articlePrompt("") // non-empty
|
||||
func TestArticlePrompt_Bad(t *testing.T) {
|
||||
prompt := articlePrompt("")
|
||||
if prompt == "" {
|
||||
t.Error("articlePrompt(\"\") should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIrregularPrompt_Ugly verifies the prompt is not empty for an unusual tense string.
|
||||
//
|
||||
// irregularPrompt("run", "subjunctive") // non-empty
|
||||
func TestIrregularPrompt_Ugly(t *testing.T) {
|
||||
prompt := irregularPrompt("run", "subjunctive")
|
||||
if prompt == "" {
|
||||
t.Error("irregularPrompt should not be empty for unusual tense")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue