Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6, 5.1–5.4 (Details in AUSBAUSTUFEN.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.7 KiB
Go
150 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestUsageSummariesZaehltSchreibvorgaenge: Neuanlagen, Aktualisierungen und
|
|
// Löschungen werden getrennt nach Mensch (Web) und Agent (MCP) gezählt — und
|
|
// nur innerhalb des Zeitfensters.
|
|
func TestUsageSummariesZaehltSchreibvorgaenge(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
p := testProject(t, s)
|
|
|
|
protokoll := func(quelle, aktion, detail string) {
|
|
s.LogAudit(ctx, AuditEvent{ProjectID: p.ID, Actor: "x", Action: aktion, Source: quelle, Detail: detail})
|
|
}
|
|
protokoll(SourceMCP, ActionEntryWrite, DetailEntryCreated)
|
|
protokoll(SourceMCP, ActionEntryWrite, DetailEntryUpdated)
|
|
protokoll(SourceMCP, ActionEntryWrite, DetailEntryUpdated)
|
|
protokoll(SourceMCP, ActionEntryDelete, "in den Papierkorb")
|
|
protokoll(SourceWeb, ActionEntryWrite, DetailEntryCreated)
|
|
protokoll(SourceWeb, ActionEntryWrite, EntryRestoreDetail(3)) // zählt als Aktualisierung
|
|
protokoll(SourceWeb, ActionEntryWrite, "Sammelbearbeitung: 2 …") // ebenso
|
|
protokoll(SourceWeb, ActionEntryWrite, "") // aus der Zeit vor der Unterscheidung
|
|
protokoll(SourceWeb, ActionLoginSuccess, "") // zählt gar nicht
|
|
|
|
// Ein Schreibvorgang außerhalb des Fensters darf nicht mitzählen.
|
|
alt := time.Now().UTC().Add(-UsageWindow - time.Hour).Format(time.RFC3339Nano)
|
|
if _, err := s.schreiben.ExecContext(ctx,
|
|
`INSERT INTO audit_events (id, ts, project_id, api_key_id, actor, action, entry_slug, source, ip, detail)
|
|
VALUES (?, ?, ?, '', 'x', ?, '', ?, '', ?)`,
|
|
NewID(), alt, p.ID, ActionEntryWrite, SourceMCP, DetailEntryCreated); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
alle, err := s.UsageSummaries(ctx, "", time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u := alle[p.ID]
|
|
|
|
if (u.Agent != WriteCounts{Created: 1, Updated: 2, Deleted: 1}) {
|
|
t.Errorf("Agent = %+v", u.Agent)
|
|
}
|
|
if (u.Human != WriteCounts{Created: 1, Updated: 2, Unknown: 1}) {
|
|
t.Errorf("Mensch = %+v", u.Human)
|
|
}
|
|
// 4 Aktualisierungen von 6 bekannten Schreibvorgängen = 67 %.
|
|
if got := u.UpdatePercent(); got != 67 {
|
|
t.Errorf("UpdatePercent = %d, erwartet 67", got)
|
|
}
|
|
|
|
// Eingeschränkt auf das Projekt kommt dasselbe heraus.
|
|
einzeln, _ := s.UsageSummaries(ctx, p.ID, time.Now())
|
|
if einzeln[p.ID] != u {
|
|
t.Errorf("mit Projektfilter: %+v, ohne: %+v", einzeln[p.ID], u)
|
|
}
|
|
}
|
|
|
|
// TestUsageSummariesBestand: Einträge, kuratierte und solche ohne Schlagwort —
|
|
// ohne Papierkorb.
|
|
func TestUsageSummariesBestand(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
p := testProject(t, s)
|
|
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "a", Title: "A", Tags: []string{"x"}, Curated: true, Source: SourceHuman})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "b", Title: "B", Source: SourceAgent})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "c", Title: "C", Source: SourceAgent})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "weg", Title: "Weg", Curated: true, Source: SourceAgent})
|
|
s.MoveEntryToTrash(ctx, p.ID, "weg", "tom")
|
|
|
|
alle, _ := s.UsageSummaries(ctx, "", time.Now())
|
|
u := alle[p.ID]
|
|
|
|
if u.Entries != 3 || u.Curated != 1 || u.Untagged != 2 {
|
|
t.Errorf("Bestand = %d Einträge, %d kuratiert, %d ohne Schlagwort; erwartet 3, 1, 2",
|
|
u.Entries, u.Curated, u.Untagged)
|
|
}
|
|
if u.CuratedPercent() != 33 {
|
|
t.Errorf("CuratedPercent = %d, erwartet 33", u.CuratedPercent())
|
|
}
|
|
if u.LastUpdatedAt.IsZero() {
|
|
t.Error("LastUpdatedAt fehlt")
|
|
}
|
|
// Keine Schreibvorgänge im Protokoll → kein sinnvoller Anteil.
|
|
if u.UpdatePercent() != -1 {
|
|
t.Errorf("UpdatePercent ohne Schreibvorgänge = %d, erwartet -1", u.UpdatePercent())
|
|
}
|
|
|
|
ohne, _ := s.UntaggedEntries(ctx, p.ID, 10)
|
|
if len(ohne) != 2 {
|
|
t.Errorf("UntaggedEntries = %v, erwartet b und c", ohne)
|
|
}
|
|
}
|
|
|
|
// TestMissingLinks: Verweise ins Leere, meistgenannte zuerst. Verweise aus
|
|
// dem Papierkorb zählen nicht, Verweise auf gelöschte Einträge schon.
|
|
func TestMissingLinks(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
p := testProject(t, s)
|
|
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "a", Title: "A", Content: "[[fehlt]] [[vorhanden]] [[geloescht]]", Source: SourceHuman})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "b", Title: "B", Content: "[[fehlt]]", Source: SourceHuman})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "vorhanden", Title: "V", Source: SourceHuman})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "geloescht", Title: "G", Source: SourceHuman})
|
|
s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "im-papierkorb", Title: "P", Content: "[[nur-von-dort]]", Source: SourceHuman})
|
|
s.MoveEntryToTrash(ctx, p.ID, "geloescht", "tom")
|
|
s.MoveEntryToTrash(ctx, p.ID, "im-papierkorb", "tom")
|
|
|
|
liste, err := s.MissingLinks(ctx, p.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
erwartet := []MissingLink{{"fehlt", 2}, {"geloescht", 1}}
|
|
if len(liste) != len(erwartet) {
|
|
t.Fatalf("MissingLinks = %v, erwartet %v", liste, erwartet)
|
|
}
|
|
for i := range erwartet {
|
|
if liste[i] != erwartet[i] {
|
|
t.Errorf("[%d] = %v, erwartet %v", i, liste[i], erwartet[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecentlyUsedKeys(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
p := testProject(t, s)
|
|
|
|
benutzt, _ := s.CreateAPIKey(ctx, p.ID, "benutzt", "hash-1", "write")
|
|
s.CreateAPIKey(ctx, p.ID, "nie", "hash-2", "write")
|
|
weg, _ := s.CreateAPIKey(ctx, p.ID, "zurueckgezogen", "hash-3", "write")
|
|
|
|
jetzt := NowString()
|
|
s.schreiben.ExecContext(ctx, `UPDATE api_keys SET last_used_at = ? WHERE id IN (?, ?)`, jetzt, benutzt.ID, weg.ID)
|
|
s.RevokeAPIKey(ctx, weg.ID)
|
|
|
|
keys, err := s.RecentlyUsedKeys(ctx, p.ID, 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(keys) != 1 || keys[0].Name != "benutzt" {
|
|
t.Errorf("RecentlyUsedKeys = %v, erwartet nur „benutzt“", keys)
|
|
}
|
|
}
|