f56cbbf4c6
- Added new frustration and revised behavior metrics across stats types, parser mappings, and UI charts. - Reworked session sync to fan out parsing across a capped worker pool with SyncOptions progress callbacks. - Added worker-based parse messaging and throttled TTY progress rendering in the stats sync command. - Updated behavior scoring to strip structured content, ignore noisy prompts, and fix profanity boundary regressions. - Expanded user message schema and migrations, then repaired assistant model/provider links during sync backfill. - Updated worker-import guidance in AGENTS.md and recorded sync-progress/metrics updates in package changelogs.
177 lines
8.4 KiB
TypeScript
177 lines
8.4 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { computeUserMessageMetrics, EMPTY_USER_METRICS } from "../src/user-metrics";
|
|
|
|
describe("computeUserMessageMetrics", () => {
|
|
it("returns zeros for empty / whitespace-only text", () => {
|
|
expect(computeUserMessageMetrics("")).toEqual({ ...EMPTY_USER_METRICS });
|
|
expect(computeUserMessageMetrics(" \n\t ")).toEqual({ ...EMPTY_USER_METRICS });
|
|
});
|
|
|
|
it("counts a sentence as yelling when >50% of its letters are uppercase", () => {
|
|
const m = computeUserMessageMetrics("STOP DOING THAT NOW");
|
|
expect(m.yelling).toBe(1);
|
|
});
|
|
|
|
it("treats mostly-lowercase sentences as not yelling even with embedded CAPS", () => {
|
|
const m = computeUserMessageMetrics("Hi there, please STOP doing THAT immediately, it is really annoying.");
|
|
expect(m.yelling).toBe(0);
|
|
});
|
|
|
|
it("ignores very short uppercase fragments below the letter floor", () => {
|
|
expect(computeUserMessageMetrics("OK").yelling).toBe(0);
|
|
expect(computeUserMessageMetrics("WIP.").yelling).toBe(0);
|
|
});
|
|
|
|
it("counts multiple yelling sentences separated by terminators", () => {
|
|
const m = computeUserMessageMetrics("WHY IS THIS BROKEN? FIX IT NOW!! please.");
|
|
expect(m.yelling).toBe(2);
|
|
});
|
|
|
|
it("does not flag camelCase / acronyms inside otherwise-lowercase prose", () => {
|
|
const m = computeUserMessageMetrics("call getHTMLParser then exit");
|
|
expect(m.yelling).toBe(0);
|
|
});
|
|
|
|
it("matches profanity case-insensitively at word boundaries only", () => {
|
|
// Regression: prior version used a non-raw template literal so `\b` was
|
|
// compiled as backspace (U+0008) and the regex matched nothing in real
|
|
// prose. Lock the word-boundary contract in.
|
|
const m = computeUserMessageMetrics("oh FUCK this is bullshit, damn it");
|
|
expect(m.profanity).toBe(3);
|
|
expect(computeUserMessageMetrics("import classes from module").profanity).toBe(0);
|
|
});
|
|
|
|
it("counts quality-dismissal vocabulary as profanity", () => {
|
|
const m = computeUserMessageMetrics("this is garbage, useless and horrible work");
|
|
expect(m.profanity).toBe(3);
|
|
});
|
|
|
|
it("folds drama runs / elongated interjections / dot trails into `anguish`", () => {
|
|
const m = computeUserMessageMetrics("why!!! seriously??? omg!?!?!?");
|
|
expect(m.anguish).toBeGreaterThanOrEqual(3);
|
|
expect(computeUserMessageMetrics("ok!! sure??").anguish).toBe(0);
|
|
});
|
|
|
|
it("absorbs shift-key `1` mishits into a single drama burst", () => {
|
|
expect(computeUserMessageMetrics("what!!!111").anguish).toBeGreaterThanOrEqual(1);
|
|
expect(computeUserMessageMetrics("are you serious!?!?!??111").anguish).toBeGreaterThanOrEqual(1);
|
|
expect(computeUserMessageMetrics("port 8111 please").anguish).toBe(0);
|
|
});
|
|
|
|
describe("negation signal", () => {
|
|
it("fires on line-leading correction openers", () => {
|
|
expect(computeUserMessageMetrics("no this is the renderer").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("nope, still wrong").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("nah look at this").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("wrong file").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("nvm got it").negation).toBe(1);
|
|
});
|
|
|
|
it("does not fire on words that share a prefix with negation tokens", () => {
|
|
// `now`, `nobody`, `north`, `noble`, `normal` all start with `no` but
|
|
// are not corrective negation.
|
|
expect(computeUserMessageMetrics("now everything works").negation).toBe(0);
|
|
expect(computeUserMessageMetrics("nobody knows why").negation).toBe(0);
|
|
expect(computeUserMessageMetrics("normal operation resumed").negation).toBe(0);
|
|
});
|
|
|
|
it("only anchors at the start of the message - mid-message `no`/`No` lines do not fire", () => {
|
|
// Real corrective negation overwhelmingly opens the message. Pasted error
|
|
// text and bullet lists trip the old `^...$/m` anchor with FPs like
|
|
// "Wrong user name or password" or "No JSDoc warning on X".
|
|
expect(computeUserMessageMetrics("i instantly get Finalizing ->\nNo speech detected").negation).toBe(0);
|
|
expect(computeUserMessageMetrics("Authentication failed\n\nWrong user name or password").negation).toBe(0);
|
|
});
|
|
|
|
it("strips `[Image #N]` placeholders so message-leading negation still fires", () => {
|
|
// The TUI inserts `[Image #1]` markers ahead of real user prose; the
|
|
// strip pass removes them so anchored-at-start negation still works.
|
|
expect(computeUserMessageMetrics("[Image #1] nope still broken").negation).toBe(1);
|
|
});
|
|
|
|
it("fires on explicit rejection phrases", () => {
|
|
expect(computeUserMessageMetrics("thats not what i wanted").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("that's not right").negation).toBe(1);
|
|
expect(computeUserMessageMetrics("this is not what i meant at all").negation).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("repetition signal", () => {
|
|
it("counts explicit recall verbs", () => {
|
|
expect(computeUserMessageMetrics("i meant the other file").repetition).toBe(1);
|
|
expect(computeUserMessageMetrics("i told you to skip it").repetition).toBe(1);
|
|
expect(computeUserMessageMetrics("i asked you for json not yaml").repetition).toBe(1);
|
|
expect(computeUserMessageMetrics("like i said earlier").repetition).toBe(1);
|
|
});
|
|
|
|
it("requires `you` after `i asked` to suppress neutral third-party usage", () => {
|
|
// In the real corpus, bare `i asked` is overwhelmingly "i asked
|
|
// <some third party>" - committee, experts, weaker LLMs, etc. -
|
|
// which is not frustration with us. The `(like|as) i asked` form is
|
|
// still allowed because it always refers back to our own ask.
|
|
expect(computeUserMessageMetrics("i asked the committee to review").repetition).toBe(0);
|
|
expect(computeUserMessageMetrics("so i asked a bunch of experts").repetition).toBe(0);
|
|
expect(computeUserMessageMetrics("you're not doing AST rewriting like i asked").repetition).toBe(1);
|
|
});
|
|
|
|
it("counts `still` only when paired with a negative / sameness marker", () => {
|
|
// Bare `still` would over-fire on neutral usage.
|
|
expect(computeUserMessageMetrics("the agent still works fine").repetition).toBe(0);
|
|
expect(computeUserMessageMetrics("it still doesnt work").repetition).toBe(1);
|
|
expect(computeUserMessageMetrics("still the same issue").repetition).toBe(1);
|
|
expect(computeUserMessageMetrics("still failing on darwin").repetition).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("blame signal", () => {
|
|
it("fires on accusatory second-person verbs", () => {
|
|
expect(computeUserMessageMetrics("you broke the layout").blame).toBe(1);
|
|
expect(computeUserMessageMetrics("you didnt update AGENTS").blame).toBe(1);
|
|
expect(computeUserMessageMetrics("you missed a callsite").blame).toBe(1);
|
|
expect(computeUserMessageMetrics("you forgot to commit").blame).toBe(1);
|
|
expect(computeUserMessageMetrics("you keep doing that").blame).toBe(1);
|
|
});
|
|
|
|
it("does not fire on bare `you`", () => {
|
|
// `you` alone is too generic - dominated by neutral instructions.
|
|
expect(computeUserMessageMetrics("can you fix the bug?").blame).toBe(0);
|
|
expect(computeUserMessageMetrics("could you also add a test").blame).toBe(0);
|
|
});
|
|
|
|
it("only fires on `stop X-ing` at sentence start", () => {
|
|
expect(computeUserMessageMetrics("stop touching git").blame).toBe(1);
|
|
expect(computeUserMessageMetrics("please stop making yolo changes").blame).toBe(0); // mid-sentence
|
|
expect(computeUserMessageMetrics("ok. stop reverting things").blame).toBe(1);
|
|
// `nonstop`/`stopping` should not match the imperative pattern.
|
|
expect(computeUserMessageMetrics("the loop keeps stopping").blame).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("zeros out behavior signals on long structured prompts", () => {
|
|
// >= 3 non-empty prose lines after stripping = deliberate prompt, not a tantrum.
|
|
const long = [
|
|
"no this is wrong, you broke it, i meant the other one.",
|
|
"please undo and try again.",
|
|
"acceptance: green tests.",
|
|
"thanks!",
|
|
].join("\n");
|
|
const m = computeUserMessageMetrics(long);
|
|
expect(m.negation).toBe(0);
|
|
expect(m.repetition).toBe(0);
|
|
expect(m.blame).toBe(0);
|
|
expect(m.anguish).toBe(0);
|
|
expect(m.profanity).toBe(0);
|
|
expect(m.yelling).toBe(0);
|
|
// But char/word counts still reflect the raw text.
|
|
expect(m.chars).toBeGreaterThan(0);
|
|
expect(m.words).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("captures multiple frustration signals on a single short message", () => {
|
|
const m = computeUserMessageMetrics("no, you broke it AGAIN. i told you it still doesnt work");
|
|
expect(m.negation).toBe(1);
|
|
expect(m.blame).toBe(1);
|
|
expect(m.repetition).toBeGreaterThanOrEqual(2); // `i told you` + `still doesnt`
|
|
});
|
|
});
|