mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-01 12:25:31 +00:00
Compact one-row strip: correction rate with count, median time to first edit, and the top reworked file, plus one coaching note derived locally with the same thresholds and copy as the CLI's workflow-insights buildCoachingNotes. Decodes two new optional payload blocks (current.workflow and current.topReworkedFiles), so payloads from older CLIs still parse. The section hides entirely when there is no signal and individual stats never render as zero placeholders. It reads the store's current payload, so it follows the selected agent tab automatically. Adds unit tests for the duration formatter, note selection, model derivation, and payload decoding. Co-authored-by: reviewer <review@local>
235 lines
10 KiB
Swift
235 lines
10 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import CodeBurnMenubar
|
||
|
||
@Suite("WorkflowStrip -- duration formatter")
|
||
struct WorkflowStripDurationTests {
|
||
@Test("seconds under a minute")
|
||
func seconds() {
|
||
#expect(WorkflowStripModel.formatDuration(ms: 8_000) == "8s")
|
||
#expect(WorkflowStripModel.formatDuration(ms: 45_000) == "45s")
|
||
// Rounds to nearest second (away from zero on .5).
|
||
#expect(WorkflowStripModel.formatDuration(ms: 1_500) == "2s")
|
||
}
|
||
|
||
@Test("whole minutes below an hour")
|
||
func minutes() {
|
||
#expect(WorkflowStripModel.formatDuration(ms: 60_000) == "1m")
|
||
#expect(WorkflowStripModel.formatDuration(ms: 360_000) == "6m")
|
||
// 1m30s rounds up to 2m.
|
||
#expect(WorkflowStripModel.formatDuration(ms: 90_000) == "2m")
|
||
}
|
||
|
||
@Test("hours and minutes at and above an hour")
|
||
func hours() {
|
||
#expect(WorkflowStripModel.formatDuration(ms: 3_600_000) == "1h 0m")
|
||
#expect(WorkflowStripModel.formatDuration(ms: 3_900_000) == "1h 5m")
|
||
#expect(WorkflowStripModel.formatDuration(ms: 7_500_000) == "2h 5m")
|
||
}
|
||
|
||
@Test("coaching-note short formatter matches the CLI (seconds/minutes only)")
|
||
func shortFormatter() {
|
||
#expect(WorkflowStripModel.formatDurationShort(ms: 45_000) == "45s")
|
||
#expect(WorkflowStripModel.formatDurationShort(ms: 300_000) == "5m")
|
||
// Above an hour the note still reads minutes, unlike the stat (which is "1h 5m").
|
||
#expect(WorkflowStripModel.formatDurationShort(ms: 3_900_000) == "65m")
|
||
}
|
||
}
|
||
|
||
@Suite("WorkflowStrip -- coaching note selection")
|
||
struct WorkflowStripNoteTests {
|
||
private func file(_ path: String, sessions: Int, edits: Int) -> ReworkedFileEntry {
|
||
ReworkedFileEntry(path: path, sessions: sessions, edits: edits)
|
||
}
|
||
|
||
@Test("corrections note fires at threshold and wins over the others")
|
||
func correctionsWins() {
|
||
let note = WorkflowStripModel.coachingNote(
|
||
corrections: 5,
|
||
correctionRate: 0.2,
|
||
topFile: file("sdk.py", sessions: 15, edits: 42),
|
||
medianTimeToFirstEditMs: 600_000
|
||
)
|
||
#expect(note == "You corrected the assistant on 20% of prompts (5 times). State the requirements in the first message to cut the back and forth.")
|
||
}
|
||
|
||
@Test("corrections note gated by rate and count")
|
||
func correctionsGating() {
|
||
// Rate below 0.15 -> falls through to churn.
|
||
#expect(WorkflowStripModel.coachingNote(corrections: 9, correctionRate: 0.1, topFile: file("a.py", sessions: 4, edits: 8), medianTimeToFirstEditMs: nil)
|
||
== "a.py was reworked across 4 sessions (8 edits). A focused pass on it may cost less than the repeated churn.")
|
||
// Count below 3 -> falls through to churn.
|
||
#expect(WorkflowStripModel.coachingNote(corrections: 2, correctionRate: 0.9, topFile: file("a.py", sessions: 4, edits: 8), medianTimeToFirstEditMs: nil)
|
||
== "a.py was reworked across 4 sessions (8 edits). A focused pass on it may cost less than the repeated churn.")
|
||
}
|
||
|
||
@Test("churn note fires and uses the basename")
|
||
func churn() {
|
||
let note = WorkflowStripModel.coachingNote(
|
||
corrections: 0,
|
||
correctionRate: nil,
|
||
topFile: file("src/lib/sdk.py", sessions: 15, edits: 42),
|
||
medianTimeToFirstEditMs: nil
|
||
)
|
||
#expect(note == "sdk.py was reworked across 15 sessions (42 edits). A focused pass on it may cost less than the repeated churn.")
|
||
}
|
||
|
||
@Test("ttfe note fires at 5 minutes when nothing stronger does")
|
||
func ttfe() {
|
||
let note = WorkflowStripModel.coachingNote(
|
||
corrections: 0,
|
||
correctionRate: nil,
|
||
topFile: file("a.py", sessions: 2, edits: 3),
|
||
medianTimeToFirstEditMs: 300_000
|
||
)
|
||
#expect(note == "Median time to first edit is 5m. Point the assistant at the target file to cut the exploration before it starts editing.")
|
||
}
|
||
|
||
@Test("no note when nothing crosses a threshold")
|
||
func none() {
|
||
#expect(WorkflowStripModel.coachingNote(corrections: 1, correctionRate: 0.05, topFile: file("a.py", sessions: 1, edits: 2), medianTimeToFirstEditMs: 120_000) == nil)
|
||
#expect(WorkflowStripModel.coachingNote(corrections: 0, correctionRate: nil, topFile: nil, medianTimeToFirstEditMs: nil) == nil)
|
||
}
|
||
}
|
||
|
||
@Suite("WorkflowStrip -- model derivation")
|
||
struct WorkflowStripModelTests {
|
||
@Test("full payload renders all three stats and a note")
|
||
func full() {
|
||
let model = WorkflowStripModel(
|
||
workflow: WorkflowBlock(corrections: 5, correctionRate: 0.03, medianTimeToFirstEditMs: 360_000),
|
||
topReworkedFiles: [ReworkedFileEntry(path: "sdk.py", sessions: 15, edits: 42)]
|
||
)
|
||
#expect(model.corrections == "3% (5)")
|
||
#expect(model.firstEdit == "6m")
|
||
#expect(model.reworked == "sdk.py ×15")
|
||
#expect(model.isEmpty == false)
|
||
// rate 0.03 < 0.15 so corrections note doesn't fire; churn does.
|
||
#expect(model.note == "sdk.py was reworked across 15 sessions (42 edits). A focused pass on it may cost less than the repeated churn.")
|
||
}
|
||
|
||
@Test("basename is applied to the reworked stat")
|
||
func reworkedBasename() {
|
||
let model = WorkflowStripModel(
|
||
workflow: nil,
|
||
topReworkedFiles: [ReworkedFileEntry(path: "app/src/main.ts", sessions: 4, edits: 9)]
|
||
)
|
||
#expect(model.reworkedName == "main.ts")
|
||
#expect(model.reworked == "main.ts ×4")
|
||
}
|
||
|
||
@Test("nil workflow and empty files -> hidden")
|
||
func emptyHidden() {
|
||
let model = WorkflowStripModel(workflow: nil, topReworkedFiles: [])
|
||
#expect(model.isEmpty)
|
||
#expect(model.corrections == nil)
|
||
#expect(model.firstEdit == nil)
|
||
#expect(model.reworkedName == nil)
|
||
#expect(model.note == nil)
|
||
}
|
||
|
||
@Test("all-empty workflow (zeros/nulls) -> hidden, no zero placeholders")
|
||
func allEmptyHidden() {
|
||
let model = WorkflowStripModel(
|
||
workflow: WorkflowBlock(corrections: 0, correctionRate: 0.0, medianTimeToFirstEditMs: nil),
|
||
topReworkedFiles: []
|
||
)
|
||
#expect(model.isEmpty)
|
||
#expect(model.corrections == nil) // 0 corrections is never "0% (0)"
|
||
#expect(model.firstEdit == nil)
|
||
}
|
||
|
||
@Test("zero median and zero-session file are treated as absent")
|
||
func degenerateValues() {
|
||
let model = WorkflowStripModel(
|
||
workflow: WorkflowBlock(corrections: 0, correctionRate: nil, medianTimeToFirstEditMs: 0),
|
||
topReworkedFiles: [ReworkedFileEntry(path: "a.py", sessions: 0, edits: 0)]
|
||
)
|
||
#expect(model.firstEdit == nil)
|
||
#expect(model.reworkedName == nil)
|
||
#expect(model.isEmpty)
|
||
}
|
||
|
||
@Test("a correction rate that rounds to 0% hides the corrections stat")
|
||
func subOnePercentCorrections() {
|
||
// Mirrors real month data: 6 corrections over thousands of turns -> 0.17%.
|
||
let model = WorkflowStripModel(
|
||
workflow: WorkflowBlock(corrections: 6, correctionRate: 0.001692524682651622, medianTimeToFirstEditMs: 384_425),
|
||
topReworkedFiles: [ReworkedFileEntry(path: "sdk.py", sessions: 9, edits: 54)]
|
||
)
|
||
#expect(model.corrections == nil)
|
||
#expect(model.firstEdit == "6m")
|
||
#expect(model.reworked == "sdk.py ×9")
|
||
#expect(model.note == "sdk.py was reworked across 9 sessions (54 edits). A focused pass on it may cost less than the repeated churn.")
|
||
#expect(model.isEmpty == false)
|
||
}
|
||
|
||
@Test("only a reworked file present renders one stat and the churn note")
|
||
func onlyReworked() {
|
||
let model = WorkflowStripModel(
|
||
workflow: nil,
|
||
topReworkedFiles: [ReworkedFileEntry(path: "a.py", sessions: 4, edits: 10)]
|
||
)
|
||
#expect(model.corrections == nil)
|
||
#expect(model.firstEdit == nil)
|
||
#expect(model.reworked == "a.py ×4")
|
||
#expect(model.isEmpty == false)
|
||
#expect(model.note == "a.py was reworked across 4 sessions (10 edits). A focused pass on it may cost less than the repeated churn.")
|
||
}
|
||
}
|
||
|
||
@Suite("WorkflowStrip -- payload decoding")
|
||
struct WorkflowStripDecodeTests {
|
||
private func decodeCurrent(_ json: String) throws -> CurrentBlock {
|
||
try JSONDecoder().decode(CurrentBlock.self, from: Data(json.utf8))
|
||
}
|
||
|
||
@Test("decodes workflow and topReworkedFiles when present")
|
||
func present() throws {
|
||
let current = try decodeCurrent("""
|
||
{
|
||
"label": "Month", "cost": 1, "calls": 2, "sessions": 3,
|
||
"inputTokens": 4, "outputTokens": 5,
|
||
"workflow": { "corrections": 5, "correctionRate": 0.03, "medianTimeToFirstEditMs": 360000 },
|
||
"topReworkedFiles": [ { "path": "sdk.py", "sessions": 15, "edits": 42 } ]
|
||
}
|
||
""")
|
||
#expect(current.workflow?.corrections == 5)
|
||
#expect(current.workflow?.correctionRate == 0.03)
|
||
#expect(current.workflow?.medianTimeToFirstEditMs == 360_000)
|
||
#expect(current.topReworkedFiles.first?.path == "sdk.py")
|
||
#expect(current.topReworkedFiles.first?.sessions == 15)
|
||
#expect(current.topReworkedFiles.first?.edits == 42)
|
||
}
|
||
|
||
@Test("decodes null workflow rates to nil")
|
||
func nulls() throws {
|
||
let current = try decodeCurrent("""
|
||
{
|
||
"label": "Month", "cost": 1, "calls": 2, "sessions": 3,
|
||
"inputTokens": 4, "outputTokens": 5,
|
||
"workflow": { "corrections": 0, "correctionRate": null, "medianTimeToFirstEditMs": null },
|
||
"topReworkedFiles": []
|
||
}
|
||
""")
|
||
#expect(current.workflow?.corrections == 0)
|
||
#expect(current.workflow?.correctionRate == nil)
|
||
#expect(current.workflow?.medianTimeToFirstEditMs == nil)
|
||
#expect(current.topReworkedFiles.isEmpty)
|
||
}
|
||
|
||
@Test("older payloads without the keys decode fine")
|
||
func backwardCompatible() throws {
|
||
let current = try decodeCurrent("""
|
||
{
|
||
"label": "Month", "cost": 1, "calls": 2, "sessions": 3,
|
||
"inputTokens": 4, "outputTokens": 5
|
||
}
|
||
""")
|
||
#expect(current.workflow == nil)
|
||
#expect(current.topReworkedFiles.isEmpty)
|
||
// And an older payload yields a hidden strip.
|
||
let model = WorkflowStripModel(workflow: current.workflow, topReworkedFiles: current.topReworkedFiles)
|
||
#expect(model.isEmpty)
|
||
}
|
||
}
|