-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_test.go
More file actions
364 lines (320 loc) · 12.9 KB
/
Copy pathapp_test.go
File metadata and controls
364 lines (320 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
package cmd
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/nao1215/omokage/internal/storage"
)
func TestAppLifecycle(t *testing.T) {
t.Parallel()
workDir := t.TempDir()
corpusDir := filepath.Join(workDir, "posts")
if err := os.MkdirAll(corpusDir, 0o750); err != nil {
t.Fatal(err)
}
writeTestFile(t, filepath.Join(corpusDir, "one.md"), "# Title\n\nI write short notes. However, I still use markdown.\n- bullet\n- bullet\n")
writeTestFile(t, filepath.Join(corpusDir, "two.txt"), "そして今日は静かです。だから文章は短めです。")
writeTestFile(t, filepath.Join(workDir, "target.md"), "# Draft\n\nI write short notes. But this draft uses different pacing.\n")
code, stdout, stderr := runApp(t, workDir, "init", "--name", "sample-style")
if code != 0 {
t.Fatalf("init failed: stdout=%q stderr=%q", stdout, stderr)
}
if !strings.Contains(stdout, "Initialized omokage project.") {
t.Fatalf("unexpected init stdout: %q", stdout)
}
code, stdout, stderr = runApp(t, workDir, "train", "--author", "nao", "posts")
if code != 0 {
t.Fatalf("train failed: stdout=%q stderr=%q", stdout, stderr)
}
if !strings.Contains(stdout, `Trained author "nao"`) {
t.Fatalf("unexpected train stdout: %q", stdout)
}
code, stdout, stderr = runApp(t, workDir, "list")
if code != 0 {
t.Fatalf("list failed: stdout=%q stderr=%q", stdout, stderr)
}
if strings.TrimSpace(stdout) != "nao" {
t.Fatalf("unexpected list stdout: %q", stdout)
}
code, stdout, stderr = runApp(t, workDir, "check", "--author", "nao", "target.md")
if code != 0 {
t.Fatalf("check failed: stdout=%q stderr=%q", stdout, stderr)
}
if !strings.Contains(stdout, "Author: nao") || !strings.Contains(stdout, "Similarity:") {
t.Fatalf("unexpected check stdout: %q", stdout)
}
code, stdout, stderr = runApp(t, workDir, "diff", "posts/one.md", "target.md")
if code != 0 {
t.Fatalf("diff failed: stdout=%q stderr=%q", stdout, stderr)
}
if !strings.Contains(stdout, "Reference:") || !strings.Contains(stdout, "Target:") {
t.Fatalf("unexpected diff stdout: %q", stdout)
}
}
func TestInitRejectsExistingProject(t *testing.T) {
t.Parallel()
workDir := t.TempDir()
if code, _, stderr := runApp(t, workDir, "init"); code != 0 {
t.Fatalf("first init failed: %s", stderr)
}
code, _, stderr := runApp(t, workDir, "init")
if code == 0 {
t.Fatal("expected second init to fail")
}
if !strings.Contains(stderr, "already exists") {
t.Fatalf("unexpected stderr: %q", stderr)
}
}
// trainedProject sets up a project with one author trained on a polite-register
// Japanese corpus and returns the working directory. It backs the explain/json
// output tests.
func trainedProject(t *testing.T) string {
t.Helper()
workDir := t.TempDir()
corpusDir := filepath.Join(workDir, "posts")
writeTestFile(t, filepath.Join(corpusDir, "one.txt"), "今日は朝から雨が降っています。傘を持って出かけました。電車はとても混んでいました。")
writeTestFile(t, filepath.Join(corpusDir, "two.txt"), "昨日は友人と食事に行きました。料理はどれも美味しかったです。また行きたいと思います。")
writeTestFile(t, filepath.Join(corpusDir, "three.txt"), "週末は近くの公園を散歩しました。空気が澄んでいて気持ちが良かったです。")
if code, _, stderr := runApp(t, workDir, "init"); code != 0 {
t.Fatalf("init failed: %s", stderr)
}
if code, _, stderr := runApp(t, workDir, "train", "--author", "me", "posts"); code != 0 {
t.Fatalf("train failed: %s", stderr)
}
return workDir
}
func TestCheckExplainTextOutput(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
// A register-flipped draft: the high-level register feature should lead.
writeTestFile(t, filepath.Join(workDir, "draft.txt"),
"本日は降雨である。外出を実施した。混雑は著しいものであった。対応を継続するものとする。")
code, stdout, stderr := runApp(t, workDir, "check", "--author", "me", "--explain", "draft.txt")
if code != 0 {
t.Fatalf("check --explain failed: stderr=%q", stderr)
}
for _, want := range []string{"Author: me", "Similarity:", "self-similarity median", "Score driver:", "Scoring note:", "High-level style", "σ)"} {
if !strings.Contains(stdout, want) {
t.Fatalf("explain output missing %q:\n%s", want, stdout)
}
}
}
func TestCheckJSONOutput(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
writeTestFile(t, filepath.Join(workDir, "draft.txt"),
"本日は降雨である。外出を実施した。混雑は著しいものであった。対応を継続するものとする。")
code, stdout, stderr := runApp(t, workDir, "check", "--author", "me", "--format", "json", "draft.txt")
if code != 0 {
t.Fatalf("check --format json failed: stderr=%q", stderr)
}
var payload struct {
Author string `json:"author"`
Similarity int `json:"similarity"`
Anchor *struct {
Median int `json:"median"`
Low int `json:"low"`
High int `json:"high"`
Samples int `json:"samples"`
} `json:"self_similarity_anchor"`
Driver string `json:"score_driver"`
Note string `json:"score_note"`
HighLevel []struct {
Feature string `json:"feature"`
Category string `json:"category"`
Target float64 `json:"target"`
ReferenceMean float64 `json:"reference_mean"`
Priority int `json:"priority"`
Actionable bool `json:"actionable"`
} `json:"high_level_drift"`
LowLevel []struct {
Feature string `json:"feature"`
} `json:"low_level_drift"`
Segments []struct {
Index int `json:"index"`
Feature string `json:"feature"`
Category string `json:"category"`
Z float64 `json:"z"`
Direction string `json:"direction"`
} `json:"segments"`
SegmentStyleDrift float64 `json:"segment_style_drift"`
}
if err := json.Unmarshal([]byte(stdout), &payload); err != nil {
t.Fatalf("check --format json did not emit valid JSON: %v\n%s", err, stdout)
}
if payload.Author != "me" {
t.Fatalf("unexpected author: %q", payload.Author)
}
if payload.Similarity < 0 || payload.Similarity > 100 {
t.Fatalf("similarity out of range: %d", payload.Similarity)
}
if payload.Anchor == nil {
t.Fatal("expected self_similarity_anchor in JSON output")
}
if payload.Anchor.Median <= 0 || payload.Anchor.Samples != 3 {
t.Fatalf("unexpected self_similarity_anchor: %+v", payload.Anchor)
}
if payload.Driver == "" {
t.Fatal("expected score_driver in JSON output")
}
if payload.Note == "" {
t.Fatal("expected score_note in JSON output")
}
if len(payload.HighLevel) == 0 {
t.Fatal("expected high-level drift entries in JSON output")
}
if payload.HighLevel[0].Priority != 1 {
t.Fatalf("expected the first high-level drift to have priority 1, got %d", payload.HighLevel[0].Priority)
}
// The register-flipped draft must localize to the register feature, and every
// reported segment must carry a corresponding z above the reporting bar.
if len(payload.Segments) == 0 {
t.Fatal("expected at least one localized segment for the register-flipped draft")
}
for _, segment := range payload.Segments {
if segment.Feature == "" || segment.Z < 1.0 {
t.Fatalf("segment #%d has no actionable feature: %+v", segment.Index, segment)
}
}
if payload.Segments[0].Feature != "polite sentence-ending ratio" {
t.Fatalf("expected register drift to lead the localization, got %q", payload.Segments[0].Feature)
}
// The register-flipped draft strays in every paragraph, so the median
// paragraph drift aggregate must be present and clearly positive.
if payload.SegmentStyleDrift <= 0 {
t.Fatalf("expected a positive segment_style_drift for the register-flipped draft, got %.3f", payload.SegmentStyleDrift)
}
}
func TestTrainStoresSelfSimilarityStats(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
record, err := storage.LoadProfile(filepath.Join(workDir, "profiles", "me.db"))
if err != nil {
t.Fatalf("load profile: %v", err)
}
if record.SelfSimilarity == nil {
t.Fatal("expected self-similarity stats to be stored at train time")
}
if len(record.SelfSimilarity.MeanZ) != 3 {
t.Fatalf("expected 3 leave-one-out samples, got %d", len(record.SelfSimilarity.MeanZ))
}
if record.SelfSimilarity.MeanZMedian <= 0 {
t.Fatalf("expected a positive leave-one-out median, got %f", record.SelfSimilarity.MeanZMedian)
}
}
func TestCheckWarnsWhenCalibrationMissing(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
profilePath := filepath.Join(workDir, "profiles", "me.db")
record, err := storage.LoadProfile(profilePath)
if err != nil {
t.Fatalf("load profile: %v", err)
}
record.SelfSimilarity = nil
if err := storage.SaveProfile(profilePath, record); err != nil {
t.Fatalf("save profile without calibration: %v", err)
}
writeTestFile(t, filepath.Join(workDir, "draft.txt"),
"今日はとても良い天気です。散歩に出かけました。気持ちが良かったです。")
code, _, stderr := runApp(t, workDir, "check", "--author", "me", "draft.txt")
if code != 0 {
t.Fatalf("check failed: %s", stderr)
}
if !strings.Contains(stderr, "no self-similarity calibration") {
t.Fatalf("expected missing-calibration warning, got stderr=%q", stderr)
}
}
func TestCheckPlainOutputStaysClean(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
writeTestFile(t, filepath.Join(workDir, "draft.txt"),
"今日はとても良い天気です。散歩に出かけました。気持ちが良かったです。")
code, stdout, stderr := runApp(t, workDir, "check", "--author", "me", "draft.txt")
if code != 0 {
t.Fatalf("plain check failed: stderr=%q", stderr)
}
if !strings.Contains(stdout, "Similarity:") {
t.Fatalf("plain check stdout missing the score: %q", stdout)
}
if !strings.Contains(stdout, "self-similarity median") {
t.Fatalf("plain check stdout should include the self-similarity anchor: %q", stdout)
}
// The output is captured (not a terminal), so the discoverability tip must be
// suppressed on both streams: a pipe, a redirect, a $(...) capture, or an LLM
// harness sees only the result. The tip is shown only at an interactive
// console; the flags stay discoverable through help.
if strings.Contains(stdout, "Tip:") {
t.Fatalf("captured stdout must not carry the tip: %q", stdout)
}
if strings.Contains(stderr, "Tip:") {
t.Fatalf("captured stderr must not carry the tip: %q", stderr)
}
}
func TestCheckExplainOmitsTip(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
writeTestFile(t, filepath.Join(workDir, "draft.txt"),
"今日はとても良い天気です。散歩に出かけました。気持ちが良かったです。")
_, stdout, stderr := runApp(t, workDir, "check", "--author", "me", "--explain", "draft.txt")
if strings.Contains(stdout, "Tip:") || strings.Contains(stderr, "Tip:") {
t.Fatalf("the detailed report must not repeat the tip: stdout=%q stderr=%q", stdout, stderr)
}
}
func TestRootHelpSurfacesExplain(t *testing.T) {
t.Parallel()
// Discoverability lives in the always-available help, not in every check run.
code, stdout, _ := runApp(t, t.TempDir(), "help")
if code != 0 {
t.Fatalf("help failed with code %d", code)
}
if !strings.Contains(stdout, "--explain") {
t.Fatalf("root help should mention --explain, got %q", stdout)
}
// The `help` command and the `help <command>` entry point must be discoverable
// from the root help, not just from `<command> --help`.
if !strings.Contains(stdout, "help") || !strings.Contains(stdout, "omokage help <command>") {
t.Fatalf("root help should advertise the help command, got %q", stdout)
}
}
func TestCheckRejectsUnknownFormat(t *testing.T) {
t.Parallel()
workDir := trainedProject(t)
writeTestFile(t, filepath.Join(workDir, "draft.txt"), "本日は晴天なり。")
code, _, stderr := runApp(t, workDir, "check", "--author", "me", "--format", "yaml", "draft.txt")
if code == 0 {
t.Fatal("expected an unknown format to fail")
}
if !strings.Contains(stderr, "unknown --format") {
t.Fatalf("unexpected stderr: %q", stderr)
}
}
func runApp(t *testing.T, workDir string, args ...string) (int, string, string) {
t.Helper()
// Point the global store at a path that does not exist, so tests never read or
// write a real per-user store and the global fallback stays inert unless a test
// opts in via runAppHome.
return runAppHome(t, workDir, filepath.Join(workDir, "__omokage_no_global__"), args...)
}
// runAppHome runs the app with an explicit global store directory, for the
// global-mode and local/global-precedence tests.
func runAppHome(t *testing.T, workDir, home string, args ...string) (int, string, string) {
t.Helper()
var stdout bytes.Buffer
var stderr bytes.Buffer
app := NewApp(&stdout, &stderr, workDir)
app.home = home
code := app.Run(args)
return code, stdout.String(), stderr.String()
}
func writeTestFile(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}