返回 DeepSeek-Reasonix
catalog_test.go
根目录 / internal / usagecatalog / catalog_test.go
1 package usagecatalog
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9 )
10
11 func TestReconcileFileAndDuplicateReceiptAreIdempotent(t *testing.T) {
12 t.Parallel()
13 ctx := context.Background()
14 dir := t.TempDir()
15 path := filepath.Join(dir, "2026-08-10.jsonl")
16 line := []byte("{\"ts\":\"2026-08-10T10:00:00+08:00\",\"model\":\"deepseek/model\",\"source\":\"desktop\",\"total\":42}\n")
17 if err := os.WriteFile(path, line, 0o600); err != nil {
18 t.Fatal(err)
19 }
20 catalog, err := Open(ctx, filepath.Join(t.TempDir(), "usage.sqlite"))
21 if err != nil {
22 t.Fatal(err)
23 }
24 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
25 if err := catalog.ReconcileFile(ctx, path, "2026-08-10"); err != nil {
26 t.Fatal(err)
27 }
28 rows, err := catalog.Query(ctx, "2026-08-10", "2026-08-10", "desktop")
29 if err != nil || len(rows) != 1 || rows[0].Total != 42 || rows[0].Requests != 1 {
30 t.Fatalf("rows=%#v err=%v", rows, err)
31 }
32 var receipt AppendReceipt
33 if err := catalog.db.QueryRow(`SELECT file_path,day,byte_offset,byte_length,line_hash FROM usage_records WHERE file_path=?`, path).Scan(
34 &receipt.Path, &receipt.Day, &receipt.Offset, &receipt.Length, &receipt.LineHash); err != nil {
35 t.Fatal(err)
36 }
37 entry := Entry{Day: receipt.Day, Source: "desktop", ModelRef: "deepseek/model", Provider: "deepseek", Total: 42, Requests: 1}
38 if err := catalog.applyReceipt(ctx, receipt, entry); err != nil {
39 t.Fatal(err)
40 }
41 rows, err = catalog.Query(ctx, "2026-08-10", "2026-08-10", "desktop")
42 if err != nil || len(rows) != 1 || rows[0].Total != 42 {
43 t.Fatalf("duplicate changed aggregate: rows=%#v err=%v", rows, err)
44 }
45 }
46
47 func TestReadyRejectsExternalAppendUntilReconciled(t *testing.T) {
48 t.Parallel()
49 ctx := context.Background()
50 dir := t.TempDir()
51 path := filepath.Join(dir, "2026-08-10.jsonl")
52 if err := os.WriteFile(path, []byte("{\"ts\":\"2026-08-10T10:00:00Z\",\"total\":1}\n"), 0o600); err != nil {
53 t.Fatal(err)
54 }
55 catalog, err := Open(ctx, filepath.Join(t.TempDir(), "usage.sqlite"))
56 if err != nil {
57 t.Fatal(err)
58 }
59 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
60 if err := catalog.ReconcileFile(ctx, path, "2026-08-10"); err != nil {
61 t.Fatal(err)
62 }
63 if !catalog.Ready(ctx, dir, []string{"2026-08-10"}) {
64 t.Fatal("catalog should be ready")
65 }
66 f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o600)
67 if err != nil {
68 t.Fatal(err)
69 }
70 _, _ = f.WriteString("{\"ts\":\"2026-08-10T11:00:00Z\",\"total\":2}\n")
71 _ = f.Close()
72 if catalog.Ready(ctx, dir, []string{"2026-08-10"}) {
73 t.Fatal("external append was treated as indexed")
74 }
75 }
76
77 func TestReadyRejectsSameSizeRewriteWithDifferentMtime(t *testing.T) {
78 t.Parallel()
79 ctx := context.Background()
80 dir := t.TempDir()
81 path := filepath.Join(dir, "2026-08-10.jsonl")
82 original := []byte("{\"ts\":\"2026-08-10T10:00:00Z\",\"total\":1,\"source\":\"desktop\"}\n")
83 if err := os.WriteFile(path, original, 0o600); err != nil {
84 t.Fatal(err)
85 }
86 catalog, err := Open(ctx, filepath.Join(t.TempDir(), "usage.sqlite"))
87 if err != nil {
88 t.Fatal(err)
89 }
90 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
91 if err := catalog.ReconcileFile(ctx, path, "2026-08-10"); err != nil {
92 t.Fatal(err)
93 }
94 if !catalog.Ready(ctx, dir, []string{"2026-08-10"}) {
95 t.Fatal("catalog should be ready after reconcile")
96 }
97 // Same byte length, different content and mtime — Ready must fail so JSONL
98 // remains the authority until the projection is rescanned.
99 replacement := []byte("{\"ts\":\"2026-08-10T12:00:00Z\",\"total\":9,\"source\":\"desktop\"}\n")
100 if len(replacement) != len(original) {
101 t.Fatalf("test fixture length mismatch: %d vs %d", len(replacement), len(original))
102 }
103 if err := os.WriteFile(path, replacement, 0o600); err != nil {
104 t.Fatal(err)
105 }
106 // Force a distinct mtime on filesystems with coarse timestamps.
107 later := time.Now().Add(2 * time.Second)
108 if err := os.Chtimes(path, later, later); err != nil {
109 t.Fatal(err)
110 }
111 if catalog.Ready(ctx, dir, []string{"2026-08-10"}) {
112 t.Fatal("same-size rewrite was treated as still ready")
113 }
114 }
115
115 lines GO