| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AgentTeam, PresenceEntry } from "@g1t/contracts"; |
| 5 | |
| 6 | import { agentIdsOn, localTime, showing, teamBudgetBlock, teamsSection, whoToPage, type TeamsHere } from "./teammates.ts"; |
| 7 | |
| 8 | const NOW = new Date("2026-10-09T01:12:00Z"); |
| 9 | |
| 10 | const person = (username: string, extra: Partial<AgentTeam["people"][number]> = {}): AgentTeam["people"][number] => ({ |
| 11 | user_id: `usr_${username}`, |
| 12 | username, |
| 13 | name: null, |
| 14 | title: null, |
| 15 | timezone: null, |
| 16 | owns: [], |
| 17 | manager: null, |
| 18 | maintainer: false, |
| 19 | ...extra, |
| 20 | }); |
| 21 | |
| 22 | const entry = (username: string, presence: PresenceEntry["presence"], extra: Partial<PresenceEntry> = {}): PresenceEntry => ({ |
| 23 | user_id: `usr_${username}`, |
| 24 | username, |
| 25 | presence, |
| 26 | dnd_until: null, |
| 27 | status: null, |
| 28 | at: 1, |
| 29 | ...extra, |
| 30 | }); |
| 31 | |
| 32 | const sales: AgentTeam = { |
| 33 | slug: "sales", |
| 34 | name: "Sales", |
| 35 | description: "Two people and one agent", |
| 36 | lead: { kind: "user", username: "sofia", name: "Sofia Reyes", avatar: null }, |
| 37 | channel: { id: "chn_1", name: "sales" }, |
| 38 | budget_micros: 150_000_000, |
| 39 | people: [ |
| 40 | person("sofia", { name: "Sofia Reyes", title: "Head of Sales", owns: ["the forecast"], manager: "chase", maintainer: true, timezone: "America/Chicago" }), |
| 41 | person("jordan", { name: "Jordan Lee", title: "Account Executive", owns: ["Halcyon", "Bluebird"], manager: "sofia" }), |
| 42 | ], |
| 43 | agent_ids: [], |
| 44 | }; |
| 45 | |
| 46 | const here = (presence: PresenceEntry[]): TeamsHere => ({ |
| 47 | teams: [sales], |
| 48 | agents: [ |
| 49 | { id: "agt_david", handle: "david", display_name: "David", title: "CRM keeper", team: "sales" }, |
| 50 | { id: "agt_pax", handle: "pax", display_name: "Pax", title: "Billing", team: null }, |
| 51 | ], |
| 52 | presence, |
| 53 | }); |
| 54 | |
| 55 | test("an agent is on the teams it was added to and its home team", () => { |
| 56 | const agents = here([]).agents; |
| 57 | assert.deepEqual(agentIdsOn(sales, agents), ["agt_david"]); |
| 58 | assert.deepEqual(agentIdsOn({ ...sales, agent_ids: ["agt_pax"] }, agents), ["agt_pax", "agt_david"]); |
| 59 | }); |
| 60 | |
| 61 | test("an agent is told who leads, who owns what and who is around", () => { |
| 62 | const dnd = new Date(NOW.getTime() + 3 * 3600_000).toISOString(); |
| 63 | const text = teamsSection("agt_david", here([entry("sofia", "active", { dnd_until: dnd }), entry("jordan", "active")]), NOW)!; |
| 64 | assert.match(text, /^## Your teams/); |
| 65 | assert.match(text, /### Sales/); |
| 66 | assert.match(text, /Led by Sofia Reyes \(@sofia\)\./); |
| 67 | assert.match(text, /Its channel is #sales\./); |
| 68 | assert.match(text, /share a budget of \$150\.00 a month/); |
| 69 | assert.match(text, /Sofia Reyes \(@sofia\); Head of Sales, the lead, a maintainer; owns the forecast; reports to @chase \(focusing in Do Not Disturb until 23:12 their time; it's 20:12 for them\)/); |
| 70 | assert.match(text, /Jordan Lee \(@jordan\); Account Executive; owns Halcyon, Bluebird; reports to @sofia \(online\)/); |
| 71 | assert.match(text, /- @david: you/); |
| 72 | // Sofia is focusing, so Jordan is the one to page. |
| 73 | assert.match(text, /When a person is needed: Jordan Lee \(@jordan\), who is online now\./); |
| 74 | }); |
| 75 | |
| 76 | test("with no one reachable, the agent is told to post and wait", () => { |
| 77 | const text = teamsSection("agt_david", here([entry("sofia", "away"), entry("jordan", "offline")]), NOW)!; |
| 78 | assert.match(text, /No one on Sales can be reached now; post in #sales and say they'll see it when they're back\./); |
| 79 | }); |
| 80 | |
| 81 | test("without presence, nobody is said to be offline", () => { |
| 82 | const text = teamsSection("agt_david", here([]), NOW)!; |
| 83 | assert.doesNotMatch(text, /offline/); |
| 84 | assert.doesNotMatch(text, /When a person is needed/); |
| 85 | }); |
| 86 | |
| 87 | test("an agent on no team is told nothing", () => { |
| 88 | assert.equal(teamsSection("agt_david", { teams: [], agents: [], presence: [] }, NOW), null); |
| 89 | assert.equal(teamsSection("agt_david", null, NOW), null); |
| 90 | }); |
| 91 | |
| 92 | test("who to page: the lead, then a maintainer, then anyone reachable", () => { |
| 93 | const presence = new Map([entry("sofia", "active"), entry("jordan", "active")].map((e) => [e.user_id, e])); |
| 94 | assert.equal(whoToPage(sales, presence, NOW, true)?.username, "sofia"); |
| 95 | presence.set("usr_sofia", entry("sofia", "away")); |
| 96 | assert.equal(whoToPage(sales, presence, NOW, true)?.username, "jordan"); |
| 97 | assert.equal(whoToPage(sales, presence, NOW, false), null); |
| 98 | }); |
| 99 | |
| 100 | test("presence and local time read plainly", () => { |
| 101 | assert.equal(showing(undefined, null, NOW, true), "offline"); |
| 102 | assert.equal(showing(entry("a", "away", { status: { emoji: "🌴", text: "On leave", clear_at: null, source: "manual", set_at: "" } }), null, NOW, true), 'away; status "🌴 On leave"'); |
| 103 | assert.equal(showing(entry("a", "active"), null, NOW, false), null); |
| 104 | assert.equal(localTime("Europe/London", NOW), "02:12"); |
| 105 | assert.equal(localTime("Not/AZone", NOW), null); |
| 106 | }); |
| 107 | |
| 108 | test("a team's budget stops its agents once they have spent it together", () => { |
| 109 | assert.equal(teamBudgetBlock([{ name: "Sales", budget_micros: 150_000_000, spent_micros: 149_000_000 }]), null); |
| 110 | assert.match(teamBudgetBlock([{ name: "Sales", budget_micros: 150_000_000, spent_micros: 150_000_000 }])!, /^Sales's agents have used the team's budget of \$150\.00/); |
| 111 | }); |