| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { Signal } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | bySignalUrgency, |
| 8 | countByKind, |
| 9 | filterSignals, |
| 10 | followUpsDue, |
| 11 | isFollowUpDue, |
| 12 | isSignalKind, |
| 13 | parseWho, |
| 14 | reachOutHref, |
| 15 | signalMeta, |
| 16 | stageMeta, |
| 17 | today, |
| 18 | } from "./signals.ts"; |
| 19 | |
| 20 | function signal(workspace: string, kind: string, valueMicros = 0, owner: string | null = null): Signal { |
| 21 | return { workspace, kind: kind as Signal["kind"], detail: `${workspace} ${kind}`, valueMicros, stage: null, owner }; |
| 22 | } |
| 23 | |
| 24 | test("most urgent first: by kind, then the larger figure, else as billing sent them", () => { |
| 25 | const sorted = bySignalUrgency([ |
| 26 | signal("a", "growing", 10), |
| 27 | signal("b", "first_payment", 99), |
| 28 | signal("c", "near_ceiling", 5), |
| 29 | signal("d", "at_limit", 1), |
| 30 | signal("e", "near_ceiling", 50), |
| 31 | signal("f", "something_new", 1000), |
| 32 | signal("g", "declined", 0), |
| 33 | signal("h", "growing", 10), |
| 34 | ]); |
| 35 | assert.deepEqual( |
| 36 | sorted.map((s) => s.workspace), |
| 37 | ["d", "g", "e", "c", "a", "h", "b", "f"], |
| 38 | ); |
| 39 | }); |
| 40 | |
| 41 | test("sorting leaves the input alone", () => { |
| 42 | const input = [signal("a", "growing"), signal("b", "at_limit")]; |
| 43 | bySignalUrgency(input); |
| 44 | assert.equal(input[0].workspace, "a"); |
| 45 | }); |
| 46 | |
| 47 | test("filter by kind, and by whose they are", () => { |
| 48 | const all = [ |
| 49 | signal("a", "at_limit", 0, "Me@g1t.sh"), |
| 50 | signal("b", "at_limit", 0, null), |
| 51 | signal("c", "growing", 0, "other@g1t.sh"), |
| 52 | signal("d", "growing", 0, ""), |
| 53 | ]; |
| 54 | const me = "me@g1t.sh"; |
| 55 | assert.deepEqual( |
| 56 | filterSignals(all, { kind: null, who: "all", me }).map((s) => s.workspace), |
| 57 | ["a", "b", "c", "d"], |
| 58 | ); |
| 59 | assert.deepEqual( |
| 60 | filterSignals(all, { kind: "at_limit", who: "all", me }).map((s) => s.workspace), |
| 61 | ["a", "b"], |
| 62 | ); |
| 63 | assert.deepEqual( |
| 64 | filterSignals(all, { kind: null, who: "unassigned", me }).map((s) => s.workspace), |
| 65 | ["b", "d"], |
| 66 | ); |
| 67 | assert.deepEqual( |
| 68 | filterSignals(all, { kind: null, who: "mine", me }).map((s) => s.workspace), |
| 69 | ["a"], |
| 70 | ); |
| 71 | assert.deepEqual(filterSignals(all, { kind: "growing", who: "mine", me }), []); |
| 72 | }); |
| 73 | |
| 74 | test("counts by kind", () => { |
| 75 | assert.deepEqual(countByKind([signal("a", "at_limit"), signal("b", "at_limit"), signal("c", "growing")]), { at_limit: 2, growing: 1 }); |
| 76 | }); |
| 77 | |
| 78 | test("query values are checked", () => { |
| 79 | assert.equal(parseWho("mine"), "mine"); |
| 80 | assert.equal(parseWho("unassigned"), "unassigned"); |
| 81 | assert.equal(parseWho("everyone"), "all"); |
| 82 | assert.equal(parseWho(null), "all"); |
| 83 | assert.ok(isSignalKind("near_ceiling")); |
| 84 | assert.ok(!isSignalKind("nearly")); |
| 85 | assert.ok(!isSignalKind(null)); |
| 86 | }); |
| 87 | |
| 88 | test("links into the queue leave out the defaults", () => { |
| 89 | assert.equal(reachOutHref({}), "/reach-out"); |
| 90 | assert.equal(reachOutHref({ kind: "at_limit" }), "/reach-out?kind=at_limit"); |
| 91 | assert.equal(reachOutHref({ kind: "declined", who: "mine" }), "/reach-out?kind=declined&who=mine"); |
| 92 | assert.equal(reachOutHref({ kind: null, who: "all" }), "/reach-out"); |
| 93 | }); |
| 94 | |
| 95 | test("names for kinds and stages, and for ones sudo does not know", () => { |
| 96 | assert.equal(signalMeta("at_limit").label, "At limit"); |
| 97 | assert.equal(signalMeta("at_limit").tone, "danger"); |
| 98 | assert.equal(signalMeta("brand_new").label, "brand new"); |
| 99 | assert.equal(stageMeta(null).label, "No stage"); |
| 100 | assert.equal(stageMeta("churn_risk").label, "Churn risk"); |
| 101 | assert.equal(stageMeta("odd").label, "odd"); |
| 102 | }); |
| 103 | |
| 104 | test("follow-ups due: today or earlier, open deals only, one per workspace, oldest first", () => { |
| 105 | const s = (workspace: string, nextAt: string | null, stage: string | null = "contacted", kind = "growing"): Signal => ({ |
| 106 | ...signal(workspace, kind), |
| 107 | nextAt, |
| 108 | stage, |
| 109 | nextStep: nextAt ? "Call" : null, |
| 110 | }); |
| 111 | const on = "2026-10-04"; |
| 112 | assert.ok(isFollowUpDue(s("a", "2026-10-04"), on)); |
| 113 | assert.ok(isFollowUpDue(s("a", "2026-10-01"), on)); |
| 114 | assert.ok(!isFollowUpDue(s("a", "2026-10-05"), on)); |
| 115 | assert.ok(!isFollowUpDue(s("a", null), on)); |
| 116 | assert.ok(!isFollowUpDue(s("a", "2026-10-01", "won"), on)); |
| 117 | assert.ok(!isFollowUpDue(s("a", "2026-10-01", "lost"), on)); |
| 118 | assert.ok(isFollowUpDue(s("a", "2026-10-01", null), on)); |
| 119 | const due = followUpsDue( |
| 120 | [s("b", "2026-10-03"), s("a", "2026-09-30", "contacted", "at_limit"), s("a", "2026-09-30", "contacted", "high_spend"), s("c", "2026-10-09")], |
| 121 | on, |
| 122 | ); |
| 123 | assert.deepEqual( |
| 124 | due.map((x) => `${x.workspace}:${x.kind}`), |
| 125 | ["a:at_limit", "b:growing"], |
| 126 | ); |
| 127 | assert.equal(today(new Date("2026-10-04T23:59:00Z")), "2026-10-04"); |
| 128 | assert.equal(reachOutHref({ due: true }), "/reach-out?due=1"); |
| 129 | assert.equal(reachOutHref({ due: true, who: "mine" }), "/reach-out?due=1&who=mine"); |
| 130 | }); |