Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core.ref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ea4ea1d83f8c5f61da91bc7c72c8cde0018cd569
9e738199df77aca481afae83dbff4c3df420af80
46 changes: 46 additions & 0 deletions frontend/src/lib/features/items/activityAttributionData.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import {
agentOwnerName,
historyTelemetryRunIDs,
isAIChatAttributed,
loadAttributedComments,
loadAttributedItemHistory,
MAX_HISTORY_TELEMETRY_RUNS,
} from "./activityAttributionData.js";

describe("item activity attribution request graph", () => {
Expand Down Expand Up @@ -38,3 +41,46 @@ describe("item activity attribution request graph", () => {
expect(agentOwnerName(history[0])).toBe("History Owner");
});
});

// History is delivered newest-first, which is the order the telemetry cap
// consumes. A run id can recur because several field rows share one turn.
function chatGroup(runId) {
return { source: "ai_chat", agent_run_id: runId };
}

describe("history telemetry lazy-load scope", () => {
it("keeps only distinct ai-chat runs, newest first", () => {
const ids = historyTelemetryRunIDs([
chatGroup(101),
chatGroup(101),
{ source: "standard_agent", agent_run_id: 200 },
{ source: "ai_chat", agent_run_id: null },
{ source: "", agent_run_id: 300 },
chatGroup(102),
]);

expect(ids).toEqual([101, 102]);
});

it("caps the fetchable runs so a long history cannot fan out per row", () => {
const groups = Array.from({ length: 75 }, (_, index) => chatGroup(1000 + index));

const ids = historyTelemetryRunIDs(groups);

expect(MAX_HISTORY_TELEMETRY_RUNS).toBe(20);
expect(ids).toHaveLength(20);
expect(ids).toEqual(Array.from({ length: 20 }, (_, index) => 1000 + index));
});

it("tolerates an empty or missing group list", () => {
expect(historyTelemetryRunIDs([])).toEqual([]);
expect(historyTelemetryRunIDs(undefined)).toEqual([]);
});

it("matches only the ai_chat source", () => {
expect(isAIChatAttributed({ source: "ai_chat" })).toBe(true);
expect(isAIChatAttributed({ source: "standard_agent" })).toBe(false);
expect(isAIChatAttributed({})).toBe(false);
expect(isAIChatAttributed(null)).toBe(false);
});
});
57 changes: 57 additions & 0 deletions internal/aitools/env_provenance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build test

package aitools

import "testing"

func TestEnvEventMetadataDescribesAgentProvenance(t *testing.T) {
tests := []struct {
name string
env Env
wantSource string
wantRunID int
}{
{
name: "chat names its surface and run",
env: Env{UserID: 7, Source: SourceAIChat, RunID: 55},
wantSource: "ai_chat",
wantRunID: 55,
},
{
name: "unset source falls back to agent",
env: Env{UserID: 7},
wantSource: "agent",
wantRunID: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metadata := tt.env.eventMetadata()
if metadata.ActorKind != "agent" {
t.Fatalf("actor kind = %q, want agent", metadata.ActorKind)
}
if metadata.ActorRef != "user:7" {
t.Fatalf("actor ref = %q, want user:7", metadata.ActorRef)
}
if metadata.SourceKind != tt.wantSource {
t.Fatalf("source kind = %q, want %q", metadata.SourceKind, tt.wantSource)
}
if metadata.AgentRunID != tt.wantRunID {
t.Fatalf("agent run id = %d, want %d", metadata.AgentRunID, tt.wantRunID)
}
})
}
}

func TestEnvHistoryRunIDOnlyReportsRecordedRuns(t *testing.T) {
withRun := Env{UserID: 7, RunID: 55}
if got := withRun.historyRunID(); got == nil || *got != 55 {
t.Fatalf("historyRunID() = %v, want 55", got)
}

withoutRun := Env{UserID: 7}
if got := withoutRun.historyRunID(); got != nil {
t.Fatalf("historyRunID() = %d, want nil", *got)
}
}
8 changes: 8 additions & 0 deletions internal/handlers/agent_conversation_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ func (r fixedChatResolver) ResolveForFeatureWithOverride(string, int) (llm.Clien
return r.client, nil
}

func (r fixedChatResolver) ResolveWithIdentity(int) (llm.Client, llm.ConnectionDescriptor, error) {
return r.client, llm.ConnectionDescriptor{}, nil
}

func (r fixedChatResolver) ResolveForFeatureWithIdentity(string, int) (llm.Client, llm.ConnectionDescriptor, error) {
return r.client, llm.ConnectionDescriptor{}, nil
}

func newConversationHandler(t *testing.T) (*AIHandler, *models.User, database.Database, *capturedChatClient) {
t.Helper()
tdb := testutils.CreateTestDB(t, true)
Expand Down
45 changes: 45 additions & 0 deletions internal/itemevents/itemevents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,48 @@ func TestChangesReturnsStableTypedCanonicalFields(t *testing.T) {
t.Fatalf("Changes() = %#v, want %#v", got, want)
}
}

func TestNewEventSurfacesAgentRunAsSourceRef(t *testing.T) {
tests := []struct {
name string
metadata Metadata
wantRef string
}{
{
name: "agent run with no other source",
metadata: func() Metadata {
m := Agent("user:7", "ai_chat")
m.AgentRunID = 55
return m
}(),
wantRef: "agent_run:55",
},
{
name: "automation source ref wins over the run link",
metadata: func() Metadata {
m := Agent("user:7", "ai_chat")
m.AgentRunID = 55
m.SourceRef = "app:jira"
return m
}(),
wantRef: "app:jira",
},
{
name: "no run leaves the source ref empty",
metadata: Agent("user:7", "mcp"),
wantRef: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event, err := newEvent("item.updated", 3, 9, tt.metadata, map[string]any{"field": "title"})
if err != nil {
t.Fatalf("newEvent() error = %v", err)
}
if event.SourceRef != tt.wantRef {
t.Fatalf("source ref = %q, want %q", event.SourceRef, tt.wantRef)
}
})
}
}
34 changes: 34 additions & 0 deletions internal/repository/llm_usage_token_classes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,41 @@ func TestLLMUsageRepositoryPersistsAndAggregatesTokenClasses(t *testing.T) {
totals.CacheReadTokens != 48 || totals.CacheWriteTokens != 12 || totals.ReasoningTokens != 10 || totals.Calls != 2 {
t.Fatalf("totals = %+v", totals)
}
if totals.Model != "model-a" {
t.Fatalf("model = %q, want model-a", totals.Model)
}
if totals.CostUSD == nil || *totals.CostUSD != cost {
t.Fatalf("cost = %v, want %v", totals.CostUSD, cost)
}
}

func TestLLMUsageRepositorySumsProviderCallsPerRow(t *testing.T) {
tdb := testutils.CreateTestDB(t, true)
t.Cleanup(func() { _ = tdb.Close() })
repo := NewLLMUsageRepository(tdb.GetDatabase())

// A chat turn stores one aggregated row covering several provider
// round-trips; a broker row covers exactly one. TotalsForRun must add the
// per-row counts, not count rows.
if err := repo.Insert(context.Background(), LLMUsageRecord{
RunID: 77, Calls: 6, Model: "model-a", TotalTokens: 900,
}); err != nil {
t.Fatalf("Insert() error = %v", err)
}
if err := repo.Insert(context.Background(), LLMUsageRecord{
RunID: 77, Model: "model-a", TotalTokens: 100,
}); err != nil {
t.Fatalf("second Insert() error = %v", err)
}

totals, err := repo.TotalsForRun(context.Background(), 77)
if err != nil {
t.Fatalf("TotalsForRun() error = %v", err)
}
if totals.Calls != 7 {
t.Fatalf("calls = %d, want 7", totals.Calls)
}
if totals.TotalTokens != 1000 {
t.Fatalf("total tokens = %d, want 1000", totals.TotalTokens)
}
}
91 changes: 91 additions & 0 deletions internal/services/item_history_provenance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//go:build test

package services

import (
"testing"

"windshift/internal/itemevents"
"windshift/internal/repository"
)

func TestHistorySourceForAgentStampsOnlyAgentActors(t *testing.T) {
tests := []struct {
name string
metadata itemevents.Metadata
want string
}{
{name: "ai chat", metadata: itemevents.Agent("user:7", "ai_chat"), want: "ai_chat"},
{name: "mcp", metadata: itemevents.Agent("user:7", "mcp"), want: "mcp"},
{name: "standard agent", metadata: itemevents.Agent("user:7", "standard_agent"), want: "standard_agent"},
{name: "direct user write", metadata: itemevents.User(7, "application"), want: ""},
{name: "automation", metadata: itemevents.System("automation"), want: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := historySourceForAgent(tt.metadata); got != tt.want {
t.Fatalf("historySourceForAgent() = %q, want %q", got, tt.want)
}
})
}
}

func TestHistoryRunForAgentLinksOnlyAgentWritesWithARun(t *testing.T) {
withRun := itemevents.Agent("user:7", "ai_chat")
withRun.AgentRunID = 55
withoutRun := itemevents.Agent("user:7", "mcp")
direct := itemevents.User(7, "application")

tests := []struct {
name string
metadata itemevents.Metadata
want *int
}{
{name: "agent run", metadata: withRun, want: intPtr(55)},
{name: "agent with no run", metadata: withoutRun, want: nil},
{name: "direct write", metadata: direct, want: nil},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := historyRunForAgent(tt.metadata)
if tt.want == nil {
if got != nil {
t.Fatalf("historyRunForAgent() = %d, want nil", *got)
}
return
}
if got == nil || *got != *tt.want {
t.Fatalf("historyRunForAgent() = %v, want %d", got, *tt.want)
}
})
}
}

func TestStampHistorySourceStampsEveryRowInTheBatch(t *testing.T) {
metadata := itemevents.Agent("user:7", "ai_chat")
metadata.AgentRunID = 55
history := []repository.HistoryEntry{{ItemID: 1, FieldName: "title"}, {ItemID: 1, FieldName: "status_id"}}

stampHistorySource(history, metadata)

for i, entry := range history {
if entry.Source != "ai_chat" {
t.Fatalf("entry %d source = %q, want ai_chat", i, entry.Source)
}
if entry.AgentRunID == nil || *entry.AgentRunID != 55 {
t.Fatalf("entry %d agent_run_id = %v, want 55", i, entry.AgentRunID)
}
}
}

func TestStampHistorySourceLeavesDirectWritesUntouched(t *testing.T) {
history := []repository.HistoryEntry{{ItemID: 1, FieldName: "title"}}

stampHistorySource(history, itemevents.User(7, "application"))

if history[0].Source != "" || history[0].AgentRunID != nil {
t.Fatalf("direct write stamped: source=%q agent_run_id=%v", history[0].Source, history[0].AgentRunID)
}
}
Loading