Šķîþ ţö ḿàîñ çöñţéñţ

Ţéšţ Šţŕàţéĝý

Éṽéŕý ƒöŕḿàţ, ţööļ àñđ ƒļöŵ îñ ñéöķàþî îš ţéšţéđ àţ îţš îñţéŕƒàçé, àñđ à ƒöŕḿàţ'š ŕöüñđţŕîþ ţéšţ îš ţĥé ƃàŕ îţ ḿüšţ þàšš. Çöþý ţĥé þàţţéŕñš ƃéļöŵ ƒöŕ à ñéŵ ţéšţ, þîçķ ţĥé ƃüîļđ ţàĝ ţĥàţ ḿàţçĥéš îţš šçöþé, àñđ àđđ à ĝàţé çöñƒöŕḿàñçé çàšé ŵĥéñ ýöü çĥàñĝé ŵĥàţ kapi check ŕéþöŕţš.

Þŕîñçîþļéš​

  1. Éṽéŕý ƒöŕḿàţ àñđ ţööļ ĥàš ţéšţš. Ñö ƒöŕḿàţ ŕéàđéŕ/ŵŕîţéŕ öŕ ţööļ šĥîþš ŵîţĥöüţ ţéšţš.
  2. Ŕöüñđţŕîþ îš ţĥé ĝöļđ šţàñđàŕđ. Ƒöŕ ƒöŕḿàţš: ŕéàđ, ŵŕîţé, çöḿþàŕé ŵîţĥ öŕîĝîñàļ.
  3. Þöŕţ Öķàþî ţéšţ đàţà. Üšé Öķàþî'š ţéšţ ŕéšöüŕçé ƒîļéš àš ţĥé šöüŕçé öƒ ţŕüţĥ.
  4. Ţàƃļé-đŕîṽéñ ţéšţš. Ĝö'š ţàƃļé-đŕîṽéñ þàţţéŕñ ƒöŕ çöṽéŕîñĝ ḿüļţîþļé îñþüţš.
  5. Ţéšţ àţ ţĥé îñţéŕƒàçé ƃöüñđàŕý. Ţéšţ àĝàîñšţ DataFormatReader/Tool îñţéŕƒàçéš, ñöţ îñţéŕñàļš.
  6. Đéţéŕḿîñîšţîç ÀÎ ţéšţš. ÀÎ ţööļš üšé ḿöçķ þŕöṽîđéŕš îñ ÇÎ; ŕéàļ þŕöṽîđéŕš îñ ḿàñüàļ îñţéĝŕàţîöñ ţéšţš.

Ţéšţ Šţŕüçţüŕé​

neokapi/
├── core/model/
│ ├── model_test.go # Block creation, targets, overlays
│ └── run_test.go # Run sequence (canonical inline content)
├── core/flow/
│ ├── executor_test.go # Flow execution, error propagation
│ └── steps_test.go # StepsToGraph compilation
├── core/tool/
│ └── base_test.go # BaseTool dispatch, pass-through
│
├── core/formats/
│ ├── html/
│ │ ├── reader_test.go
│ │ ├── writer_test.go
│ │ └── testdata/ # Test fixtures
│ └── ... (each format follows the same pattern)
│
├── core/ai/tools/
│ └── tools_test.go # AI tool tests; use mock provider
├── providers/ai/
│ └── mock.go # Mock LLM provider
│
└── core/internal/testutil/
└── helpers.go # Common test helpers (RawDocFrom*, CollectParts/Blocks, PartsToChannel)

Ţéšţ Þàţţéŕñš​

Ŕöüñđţŕîþ Ţéšţ​

Ţĥé ḿöšţ îḿþöŕţàñţ ţéšţ ƒöŕ àñý ƒöŕḿàţ:

func TestRoundTrip(t *testing.T) {
tests := []struct {
name string
file string
}{
{"simple", "testdata/simple.html"},
{"inline codes", "testdata/inline_codes.html"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original, err := os.ReadFile(tt.file)
require.NoError(t, err)

reader := NewReader()
err = reader.Open(ctx, testutil.RawDocFromReader(
bytes.NewReader(original), tt.file, model.LocaleEnglish))
require.NoError(t, err)
parts := testutil.CollectParts(t, reader.Read(ctx))
reader.Close()

var buf bytes.Buffer
writer := NewWriter()
writer.SetOutputWriter(&buf)
writer.Write(ctx, testutil.PartsToChannel(parts))
writer.Close()

assert.Equal(t, string(original), buf.String())
})
}
}

Éẋţŕàçţîöñ Ţéšţ​

Ṽéŕîƒý šþéçîƒîç Ƃļöçķš àŕé éẋţŕàçţéđ ŵîţĥ çöŕŕéçţ çöñţéñţ:

func TestExtraction(t *testing.T) {
data, err := os.ReadFile("testdata/sample.html")
require.NoError(t, err)

reader := NewReader()
err = reader.Open(ctx, testutil.RawDocFromReader(
bytes.NewReader(data), "testdata/sample.html", model.LocaleEnglish))
require.NoError(t, err)
defer reader.Close()

blocks := testutil.CollectBlocks(t, reader.Read(ctx))
require.Len(t, blocks, 3)
assert.Equal(t, "Welcome", blocks[0].SourceText())
}

Ƒļöŵ Éẋéçüţîöñ Ţéšţ​

Ƃüîļđ à ţööļ ƃý éḿƃéđđîñĝ tool.BaseTool àñđ šéţţîñĝ ĥàñđļéŕ ƒîéļđš, àššéḿƃļé à ƒļöŵ ŵîţĥ ţĥé flow.NewFlow(...).AddTool(...).Build() ƃüîļđéŕ (ŵĥîçĥ ŕéţüŕñš (*Flow, error)), àñđ đŕîṽé îţ ŵîţĥ ExecuteWithChannels ƒöŕ çĥàññéļ-ļéṽéļ çöñţŕöļ:

func TestFlowExecution(t *testing.T) {
uppercase := &tool.BaseTool{
ToolName: "uppercase",
Produce: func(v tool.VariantView) error {
if v.Translatable() {
v.SetTargetText(model.LocaleFrench, strings.ToUpper(v.SourceText()))
}
return nil
},
}

f, err := flow.NewFlow("test").AddTool(uppercase).Build()
require.NoError(t, err)

executor := flow.NewExecutor()
in, out, wait := executor.ExecuteWithChannels(t.Context(), f)

go func() {
in <- &model.Part{Type: model.PartBlock, Resource: model.NewBlock("tu1", "hello")}
close(in)
}()
for range out { /* drain */ }
require.NoError(t, wait())
}

Ŕüññîñĝ Ţéšţš​

make test # All tests
make test-unit # Unit tests only (-short)
make test-race # With race detector
make test-verbose # Verbose output
make cover # Coverage report

Šîñĝļé ţéšţ:

go test -tags fts5 ./core/flow/ -run TestFlowExecutorContextCancellation -v

Þàšš -tags fts5 ŵĥéñéṽéŕ ýöü ƃýþàšš make. Ţĥé ƃüîļđ šüççééđš ŵîţĥöüţ îţ, àñđ öñļý à ǫüéŕý ţĥàţ ŕéàçĥéš ƑŢŠ5 (çöñţéñţ ḿéḿöŕý, ţéŕḿš) ƒàîļš àţ ŕüñţîḿé ŵîţĥ no such function: fts5. Šþéļļ éṽéŕý ţàĝ îñ öñé çöḿḿà-šéþàŕàţéđ ƒļàĝ (-tags "fts5,integration"): go ķééþš öñļý ţĥé ļàšţ -tags öççüŕŕéñçé.

Ţéšţ Ţàĝš​

ŢàĝÞüŕþöšéÇöḿḿàñđ
(ñöñé)Üñîţ ţéšţš öñļýgo test -tags fts5 ./...
integration+ þļüĝîñ àñđ ƒöŕḿàţ îñţéĝŕàţîöñgo test -tags "fts5,integration" ./...
acceptance+ ñàţîṽé-ƒöŕḿàţ çöñšüḿéŕ-ţööļçĥàîñ ţéšţšgo test -tags "fts5,acceptance" ./...
e2e+ éñđ-ţö-éñđ ţéšţšgo test -tags "fts5,e2e" ./...

Ţĥé ĥéàđ-ţö-ĥéàđ çöḿþàŕîšöñ öƒ ţĥé Ĝö þöŕţš àĝàîñšţ ţĥé Öķàþî Ƒŕàḿéŵöŕķ îš à šéþàŕàţé šüîţé îñ ţĥé öķàþî-ƃŕîđĝé ŕéþöšîţöŕý (parity/). Îţ çöḿþîļéš àĝàîñšţ à ŕéļéàšéđ ñéöķàþî ḿöđüļé àñđ ŕüñš öñ à šçĥéđüļé ţĥéŕé; šéé À-02.

Ĝàţé Çöñƒöŕḿàñçé Šüîţé​

Ţĥé ĝàţé çöñƒöŕḿàñçé šüîţé ĥöļđš ţĥé ƃüîļţ kapi ƃîñàŕý ţö ŵĥàţ ţĥé ĝàţé çļàîḿš. Éàçĥ çàšé šţàţéš ŵĥîçĥ ƒîñđîñĝš kapi check ŕéþöŕţš àñđ ŵĥîçĥ îţ ļéàṽéš àļöñé, ŵĥàţ kapi fix ŵŕîţéš, ŵĥàţ kapi context <path> ñàḿéš àḿöñĝ à ƒîļé'š ŕüļéš, àñđ ŵĥàţ ţĥé Çļàüđé Çöđé þļüĝîñ'š éđîţ àñđ Šţöþ ĥööķš đéçîđé àƒţéŕ àñ éđîţ. Ŕüñ îţ ŵîţĥ:

make gate-conformance

Ţĥé ţàŕĝéţ ŕüñš TestGateConformance îñ kapi/e2e/, šö îţ üšéš ţĥé ƃîñàŕý àñđ ţĥé îšöļàţîöñ éñṽîŕöñḿéñţ ţĥàţ þàçķàĝé'š TestMain šéţš üþ, àñđ ĝîṽéš éàçĥ çàšé à ŵöŕķšþàçé öƒ îţš öŵñ. Îţ þŕîñţš à ḿàţŕîẋ ŵîţĥ öñé ļîñé þéŕ çàšé àñđ šüŕƒàçé (check, fix, context, hook): pass, fail, expected-fail ŵîţĥ ţĥé ŕéàšöñ, öŕ not covered.

Éàçĥ çàšé îš à đîŕéçţöŕý üñđéŕ kapi/e2e/gate/testdata/<case>/. Ţĥé đîŕéçţöŕý îš à šḿàļļ þŕöĵéçţ: à kapi.yaml, îţš çöñţéñţ ƒîļéš, àñđ, ƒöŕ à çàšé ţĥàţ ñééđš ŕüļéš, à voice.yaml üñđéŕ .kapi/. Ţĥàţ ƒîļé îš ţĥé àŕţîƒàçţ à kapi store import šţéþ ŕéàđš îñţö ţĥé þŕöĵéçţ'š šţöŕé: îţš terms: îñţö ţĥé ţéŕḿš šţöŕé àñđ îţš þàţţéŕñš îñţö ţĥé ṽöîçé þŕöƒîļé. Ƃéšîđé ţĥéḿ, expect.yaml ļîšţš šţàĝéš. Éàçĥ šţàĝé ŕüñš îţš steps àñđ ţĥéñ çĥéçķš îţš éẋþéçţàţîöñš:

description: A kept name rule fails a check on each rejected form.
stages:
- name: kept
steps:
- actor: agent
kapi: [context, note, --term, Sign in, --instead-of, Log in]
- keep: Write Sign in, not Log in, Sign-in, Signin or SignIn.
files:
- path: docs/guide.md
check:
failing: 1
report:
- {rule: terms.vocabulary, block: "welcome/p", quote: Log in, fails: true}
absent:
- {quote: Sign in}
fix:
failing: 0
after: |
# Welcome

Sign in with your account.
context:
names: ['Sign in, not "Log in"']
hook:
edit: {path: docs/guide.md, append: "\nLog in again.\n"}
blocks: true
names: [Forbidden term "Log in" found]
log: {failing: 1, caught: 0}

À šţéþ îš à kapi çöḿḿàñđ ļîñé (ŕüñ îñ ţĥé þŕöĵéçţ, àš ţĥé þéŕšöñ öŕ àš àñ agent), à keep öŕ narrow öƒ à ŕüļé ñàḿéđ ƃý ţĥé šéñţéñçé kapi rules þŕîñţš, öŕ à write ţö à ƒîļé. À fix ŵîţĥ ñö after àššéŕţš ţĥàţ ţĥé ƒîļé ķééþš îţš ƃýţéš; args: [] ŕüñš kapi fix öṽéŕ ţĥé ŵĥöļé þŕöĵéçţ, àñđ unchanged ñàḿéš öţĥéŕ ƒîļéš ŵĥöšé ƃýţéš ḿüšţ ñöţ ḿöṽé. À ĥööķ ŕüñš ţĥé Šţöþ ĥööķ üñļéšš îţ šéţš event: edit, ŵĥîçĥ ŕüñš ţĥé ÞöšţŢööļÜšé ĥööķ öñ ţĥé éđîţ'š ƒîļé; outside: true ŵŕîţéš ţĥàţ ƒîļé öüţšîđé ţĥé þŕöĵéçţ. À ĥööķ'š log ŕéàđš kapi context log --session àƒţéŕ ţĥé ŕüñ: failing îš ţĥé çöüñţ ţĥé ŕüñ ŕéçöŕđéđ, àñđ caught îš ĥöŵ ḿàñý ƒîñđîñĝš ţĥé šéššîöñ'š ŕüñš ĥàṽé çàüĝĥţ šö ƒàŕ.

Ţö àđđ à çàšé, çöþý ţĥé ñéàŕéšţ đîŕéçţöŕý, çĥàñĝé ţĥé þŕöĵéçţ àñđ ţĥé éẋþéçţàţîöñš, àñđ ŕüñ make gate-conformance. À ƒàîļîñĝ éẋþéçţàţîöñ þŕîñţš ţĥé ƒüļļ kapi check --json ŕéþöŕţ, öŕ ţĥé ƒîẋ öüţþüţ àñđ ƃöţĥ ṽéŕšîöñš öƒ ţĥé ƒîļé. Ƒîñđ ţĥé ƃļöçķ îđš àñđ ǫüöţéđ ŵöŕđš ţĥéŕé.

Àñ éẋþéçţàţîöñ ţĥé çüŕŕéñţ çöđé đöéš ñöţ ḿééţ çàŕŕîéš xfail: <reason>. Îţ îš ļîšţéđ àš expected-fail ŵîţĥ ţĥàţ ŕéàšöñ àñđ đöéš ñöţ ƒàîļ ţĥé ŕüñ. Ŵĥéñ ţĥé ƃéĥàṽîöüŕ ļàñđš, ţĥé éẋþéçţàţîöñ þàššéš, ţĥé ŕüñ ƒàîļš àñđ ñàḿéš ţĥé xfail ţö ŕéḿöṽé. Ñéṽéŕ ḿàŕķ àñ éẋþéçţàţîöñ àš šķîþþéđ îñšţéàđ.

Ţĥé ƒîẋţüŕéš çöñţàîñ ƃàññéđ ŵöŕđš öñ þüŕþöšé. Ţĥé ŕéþöšîţöŕý'š öŵñ kapi.yaml éẋçļüđéš **/testdata/**, šö kapi check öñ ţĥé ŕéþöšîţöŕý ñéṽéŕ ŕéàđš ţĥéḿ.

ÇÎ​

ĜîţĤüƃ Àçţîöñš ŕüñš ţĥé ţéšţš öñ éṽéŕý þüšĥ àñđ þüļļ ŕéǫüéšţ (.github/workflows/ci.yml).

Ŕéļàţéđ​