Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

1,073 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ActivityDigest, ActorCount, ChatActivity, Deployment, InboxItem, RepoDigest, WorkflowRun } from "@g1t/contracts";
5
6import {
7 type AttentionRow,
8 type CodeNeed,
9 type CodePull,
10 type DeployRow,
11 type LandedChange,
12 actorKind,
13 agentLabel,
14 attention,
15 awayOf,
16 chatRow,
17 codeRow,
18 dayIn,
19 deploysIn,
20 digestOf,
21 duration,
22 explainQuestion,
23 landed,
24 landedByDay,
25 landedDeploys,
26 landedFromActivity,
27 landedIn,
28 notificationRow,
29 parseWindow,
30 pushRows,
31 rankAttention,
32 running,
33 runningDeploys,
34 runningWorkflows,
35 sessionTrailers,
36 settledIn,
37 shippedAsks,
38 shippedLabel,
39 sinceWords,
40 spanFor,
41 spanMonths,
42 spanSentence,
43 spendIn,
44 startHere,
45 summarizeQuestion,
46 waitingSentence,
47 withSessions,
48 withShipped,
49} from "./home.ts";
50
51const HOUR = 3_600_000;
52const DAY = 24 * HOUR;
53// Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles.
54const NOW = Date.parse("2026-10-09T18:00:00Z");
55const LA = "America/Los_Angeles";
56const repo = { namespace: "acme", name: "web" };
57const iso = (ms: number) => new Date(ms).toISOString();
58
59function pull(over: Partial<CodePull> = {}): CodePull {
60 return {
61 repo,
62 number: 1,
63 title: "Fix the flaky test",
64 status: "merged",
65 mergedAt: iso(NOW - HOUR),
66 createdAt: iso(NOW - 3 * HOUR),
67 updatedAt: iso(NOW - HOUR),
68 author: { username: "g1t", kind: "agent" },
69 ...over,
70 };
71}
72
73const need = (over: Partial<CodeNeed>): CodeNeed => ({
74 key: "pull:p1",
75 reason: "checks_failing",
76 repo,
77 ref: "#12",
78 title: "Add retries",
79 ask: "It failed in the merge queue.",
80 by: { name: "bo", agent: false },
81 at: NOW - 2 * HOUR,
82 to: "/acme/web/pull/12",
83 facts: [{ label: "Files changed", value: "4", tone: null }],
84 ...over,
85});
86
87test("Code's needs become rows with what is at stake", () => {
88 const failing = codeRow(need({}));
89 assert.equal(failing.kind, "pull_blocked");
90 assert.equal(failing.stake?.text, "Checks failing");
91 assert.equal(failing.action.label, "Look");
92 const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] }));
93 assert.equal(deploy.kind, "deploy");
94 assert.equal(deploy.stake?.text, "Production on an older build");
95 assert.equal(codeRow(need({ key: "review:p2", reason: "needs_review", facts: [{ label: "Lines", value: "+10 −2", tone: null }] })).stake?.text, "+10 −2");
96 assert.equal(codeRow(need({ key: "runner:j1", reason: "stalled", facts: [{ label: "Waiting", value: "2 h", tone: "warn" }] })).stake?.text, "Waiting 2 h");
97 // Your pull request that meets everything: merge it, beside reviews asked of you.
98 const ready = codeRow(need({ key: "ready:p3", reason: "ready", ask: "Ready to land when you merge it." }));
99 assert.equal(ready.kind, "ready");
100 assert.equal(ready.stake?.text, "Ready to merge");
101 assert.equal(ready.action.label, "Merge");
102 // One that needs you (a conflict, a request for changes) blocks it.
103 const conflict = codeRow(need({ key: "pull:p4", reason: "blocking", ask: "It conflicts with the branch it will land on." }));
104 assert.equal(conflict.kind, "pull_blocked");
105 assert.equal(conflict.stake?.text, "Blocking");
106});
107
108const row = (over: Partial<AttentionRow>): AttentionRow => ({
109 key: "k",
110 kind: "chat",
111 title: "t",
112 detail: "d",
113 stake: null,
114 action: { label: "Open", to: "/x" },
115 owner: null,
116 at: NOW,
117 from: "chat",
118 ...over,
119});
120
121test("Start here: the most pressing tier first, then what has waited longest", () => {
122 const rows = [
123 row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }),
124 row({ key: "review:new", kind: "review", at: NOW - HOUR, action: { label: "Review", to: "/acme/web/pull/2" } }),
125 row({ key: "review:old", kind: "review", at: NOW - 3 * HOUR, action: { label: "Review", to: "/acme/web/pull/1" } }),
126 row({ key: "pull:p", kind: "pull_blocked", at: NOW - 10 * 60_000, action: { label: "Look", to: "/acme/web/pull/3" } }),
127 ];
128 const ranked = rankAttention(rows);
129 assert.deepEqual(
130 ranked.map((r) => r.key),
131 ["pull:p", "review:old", "review:new", "chat:old"],
132 );
133 const start = startHere(ranked);
134 assert.equal(start?.row.key, "pull:p");
135 assert.match(start?.why ?? "", /can't land/);
136 // A failed production deploy beats everything.
137 const withDeploy = rankAttention([
138 ...rows,
139 row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } }),
140 ]);
141 assert.equal(startHere(withDeploy)?.row.key, "deploy:site");
142 // Ties in tier say so.
143 const two = startHere(rankAttention(rows.filter((r) => r.kind === "review")));
144 assert.equal(two?.row.key, "review:old");
145 assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/);
146 assert.equal(startHere([]), null);
147});
148
149test("the same order whatever order the rows came in", () => {
150 const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })];
151 assert.deepEqual(
152 rankAttention(rows).map((r) => r.key),
153 ["a", "b"],
154 );
155 assert.deepEqual(
156 rankAttention([...rows].reverse()).map((r) => r.key),
157 ["a", "b"],
158 );
159});
160
161const item = (over: Partial<InboxItem>): InboxItem => ({
162 id: "n1",
163 reason: "ci_activity",
164 severity: "error",
165 title: "Checks failed on #12",
166 body: "Add retries",
167 event: "check.failed",
168 repo: "acme/web",
169 workspace: "acme",
170 subject: "pull",
171 number: 12,
172 url: "/acme/web/pull/12",
173 actor: "g1t",
174 count: 1,
175 createdAt: iso(NOW - HOUR),
176 updatedAt: iso(NOW - HOUR),
177 readAt: null,
178 doneAt: null,
179 saved: false,
180 snoozedUntil: null,
181 ...over,
182});
183
184test("notifications: warnings and failures in this workspace; none about repositories without Code", () => {
185 assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification");
186 assert.equal(notificationRow(item({}), "acme", false), null);
187 assert.equal(notificationRow(item({ repo: null, url: "/acme/-/chat/general" }), "acme", false)?.kind, "notification");
188 // A reason this page does not know, older ones included, reads as a plain notification.
189 assert.equal(notificationRow(item({ reason: "agent" as string as InboxItem["reason"] }), "acme", true)?.stake?.text, "Notification");
190 assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null);
191 assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null);
192 assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null);
193});
194
195test("a notification about something already listed is left out", () => {
196 const out = attention({
197 slug: "acme",
198 code: true,
199 codeNeeds: [need({})],
200 notifications: [item({})],
201 chat: [],
202 });
203 assert.deepEqual(
204 out.rows.map((r) => r.key),
205 ["pull:p1"],
206 );
207 assert.deepEqual(out.missing, []);
208});
209
210test("without Code access no Code row is asked for or shown, and a silent source is named", () => {
211 const out = attention({
212 slug: "acme",
213 code: false,
214 codeNeeds: [need({})],
215 notifications: [item({}), item({ id: "n2", repo: null, reason: "mention", severity: "warning", url: "/acme/-/chat/general" })],
216 chat: null,
217 });
218 assert.deepEqual(
219 out.rows.map((r) => r.key),
220 ["notification:n2"],
221 );
222 assert.equal(
223 out.rows.some((r) => r.from === "code"),
224 false,
225 );
226 assert.deepEqual(out.missing, ["Chat"]);
227});
228
229test("chat: mentions anywhere and unread direct messages, never muted", () => {
230 const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) };
231 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null);
232 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions");
233 assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null);
234 const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme");
235 assert.equal(dm?.action.to, "/acme/-/chat/dm/c1");
236});
237
238// The span is NOW back to 3 days ago unless a test says otherwise.
239const SPAN = { from: NOW - 3 * DAY, now: NOW };
240
241test("a day is the reader's day", () => {
242 assert.equal(dayIn(NOW, LA), "2026-10-09");
243 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09");
244 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10");
245 assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09");
246});
247
248test("since your last visit, in words, in the reader's time zone", () => {
249 // NOW is Friday 11:00 in Los Angeles.
250 assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago");
251 assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago");
252 assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago");
253 assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago");
254 // 07:00 Friday.
255 assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning");
256 // 02:00 Friday is Thursday night.
257 assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night");
258 // 19:00 Thursday.
259 assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening");
260 // 19:00 Tuesday.
261 assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening");
262 // 02:00 Wednesday is Tuesday night.
263 assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night");
264 assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30");
265});
266
267test("how long a span is", () => {
268 assert.equal(duration(30_000), "a moment");
269 assert.equal(duration(45 * 60_000), "45 minutes");
270 assert.equal(duration(HOUR), "1 hour");
271 assert.equal(duration(3 * DAY + HOUR), "3 days");
272 assert.equal(duration(15 * DAY), "2 weeks");
273});
274
275test("the span: since your last visit, capped, or a fixed window", () => {
276 assert.equal(parseWindow("24h"), "24h");
277 assert.equal(parseWindow("7d"), "7d");
278 assert.equal(parseWindow("nonsense"), "last");
279 assert.equal(parseWindow(null), "last");
280 const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA);
281 assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR);
282 assert.equal(away.words, "Since Tuesday evening");
283 assert.equal(away.length, "2 days");
284 assert.equal(away.note, null);
285 // Never visited: the last 24 hours, and it says why.
286 const first = spanFor("last", NOW, null, LA);
287 assert.equal(first.from, NOW - DAY);
288 assert.equal(first.note, "first");
289 assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown");
290 // Away a month: 14 days at most.
291 const capped = spanFor("last", NOW, NOW - 30 * DAY, LA);
292 assert.equal(capped.from, NOW - 14 * DAY);
293 assert.equal(capped.note, "capped");
294 assert.equal(capped.words, "In the last 14 days");
295 // The fixed windows ignore the last visit.
296 assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY);
297 assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]);
298});
299
300// --- People ------------------------------------------------------------
301
302const ANA = "user:usr_ana";
303const BO = "user:usr_bo";
304const CY = "user:usr_cy";
305const MARGO = "agent:agt_margo";
306const G1T = "user:usr_g1t_agent";
307const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" };
308const REPOS = {
309 rep_web: { namespace: "acme", name: "web", defaultBranch: "main" },
310 rep_api: { namespace: "acme", name: "api", defaultBranch: "main" },
311 rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" },
312 rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" },
313};
314
315const count = (actor: string, n: number): ActorCount => ({ actor, count: n });
316const quietRepo = (repo_id: string): RepoDigest => ({
317 repo_id,
318 pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
319 pulls: { opened: [], merged: [], closed: [] },
320 items: { opened: [], closed: [] },
321 reviews: [],
322 comments: [],
323 deployments: { succeeded: [], failed: [], production: 0 },
324 releases: [],
325 packages: [],
326});
327
328/**
329 * A day on a busy workspace: 3 people pushed 41 commits to 4 projects,
330 * merged 5 pull requests and opened 2; older agent rows (g1t and Margo)
331 * pushed 6, opened 3 pull requests and reviewed 2, and are not people;
332 * 2 production deploys went out; 120 messages were sent, 15 of them
333 * Margo's; 7 docs were edited.
334 */
335function busyDay(): ActivityDigest {
336 return {
337 from: iso(SPAN.from),
338 until: iso(SPAN.now),
339 complete: true,
340 repos: [
341 {
342 ...quietRepo("rep_web"),
343 pushes: {
344 count: 14,
345 commits: 33,
346 branches: ["fix-login", "main"],
347 by: [
348 { actor: ANA, count: 8, commits: 24 },
349 { actor: BO, count: 4, commits: 5 },
350 { actor: G1T, count: 2, commits: 4 },
351 ],
352 default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) },
353 },
354 pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] },
355 items: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] },
356 reviews: [count(BO, 2), count(G1T, 2)],
357 comments: [count(ANA, 4), count(MARGO, 3)],
358 deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 },
359 releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }],
360 packages: [],
361 },
362 {
363 ...quietRepo("rep_api"),
364 pushes: {
365 count: 5,
366 commits: 8,
367 branches: ["main"],
368 by: [{ actor: CY, count: 5, commits: 8 }],
369 default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) },
370 },
371 pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] },
372 packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }],
373 },
374 {
375 ...quietRepo("rep_docs"),
376 pushes: {
377 count: 2,
378 commits: 2,
379 branches: ["typo"],
380 by: [{ actor: BO, count: 2, commits: 2 }],
381 default_branch: { count: 0, commits: 0, by: [], last_at: null },
382 },
383 },
384 {
385 ...quietRepo("rep_cli"),
386 pushes: {
387 count: 3,
388 commits: 4,
389 branches: ["trunk"],
390 by: [
391 { actor: ANA, count: 1, commits: 2 },
392 { actor: MARGO, count: 2, commits: 2 },
393 ],
394 default_branch: {
395 count: 3,
396 commits: 4,
397 by: [
398 { actor: ANA, count: 1, commits: 2 },
399 { actor: MARGO, count: 2, commits: 2 },
400 ],
401 last_at: iso(NOW - 30 * 60_000),
402 },
403 },
404 },
405 ],
406 folios: {
407 created: [count(ANA, 1)],
408 edited_count: 7,
409 edited: [
410 { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] },
411 { folio_id: "fol_2", kind: "doc", authors: [ANA] },
412 { folio_id: "fol_3", kind: "doc", authors: [BO] },
413 { folio_id: "fol_4", kind: "doc", authors: [BO] },
414 { folio_id: "fol_5", kind: "doc", authors: [CY] },
415 { folio_id: "fol_6", kind: "doc", authors: [CY] },
416 { folio_id: "fol_7", kind: "doc", authors: [MARGO] },
417 ],
418 },
419 };
420}
421
422const busyChat: ChatActivity = {
423 from: iso(SPAN.from),
424 until: iso(SPAN.now),
425 messages: 120,
426 channels: 5,
427 authors: [
428 { key: ANA, messages: 60 },
429 { key: BO, messages: 30 },
430 { key: CY, messages: 15 },
431 { key: MARGO, messages: 15 },
432 ],
433};
434
435test("an actor is not a person when it is g1t or an agent of older rows; everyone else is", () => {
436 assert.equal(actorKind(MARGO), "agent");
437 assert.equal(actorKind(G1T), "agent");
438 assert.equal(actorKind("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot");
439 assert.equal(actorKind("user:wsp_01abc"), "agent", "a workspace, by its own token or g1t's upkeep, is never a person");
440 assert.equal(actorKind("user:g1t"), "agent");
441 assert.equal(actorKind(ANA), "person");
442 assert.equal(actorKind(""), "person");
443 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
444});
445
446test("the digest: what was done, by kind with counts, and who, agents marked", () => {
447 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
448 assert.deepEqual(
449 digest.people.lines.map((line) => [line.key, line.text, line.to]),
450 [
451 ["pushes", "47 commits to 4 projects", "/acme/-/repositories"],
452 ["pulls", "5 opened, 5 merged, 4 reviewed", "/acme/-/repositories"],
453 ["items", "2 opened, 2 closed, 7 comments", "/acme/-/work?repo=web"],
454 ["shipped", "2 deploys, 1 release, 1 package version", "/acme/web/deployments"],
455 ["docs", "1 doc created, 7 docs edited", "/acme/-/artifacts"],
456 ["chat", "120 messages in 5 conversations", "/acme/-/chat"],
457 ],
458 );
459 // Rows: most active first, people and agents, each marked; nobody is "someone".
460 assert.deepEqual(
461 digest.people.actors.map((actor) => [actor.name, actor.kind]),
462 digest.people.actors.map((actor) => [actor.name, actor.name === "margo" || actor.name === "g1t" ? "agent" : "person"]),
463 );
464 assert.deepEqual(new Set(digest.people.actors.map((actor) => actor.name)), new Set(["ana", "bo", "cy", "margo", "g1t"]));
465 assert.equal(digest.people.actors[0]!.name, "ana");
466 // Each row's parts add up to its count, and the kinds' counts to every row's.
467 for (const actor of digest.people.actors) {
468 assert.equal(
469 Object.values(actor.parts).reduce((a, b) => a + b, 0),
470 actor.count,
471 actor.name,
472 );
473 }
474 // The summary still counts people only.
475 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true });
476 assert.deepEqual(digest.missing, []);
477 assert.equal(digest.partial, false);
478});
479
480test("g1t is one row, named, whichever key it used: its account, a workspace, or chat's system:g1t", () => {
481 const chat: ChatActivity = {
482 ...busyChat,
483 messages: 5,
484 channels: 1,
485 authors: [
486 { key: "system:g1t", messages: 3 },
487 { key: ANA, messages: 2 },
488 ],
489 };
490 const activity = { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), comments: [count(G1T, 2), count("user:wsp_acme", 1)] }] };
491 const digest = digestOf({ slug: "acme", repos: REPOS, activity, chat, names: { [ANA]: "ana", "user:wsp_acme": "g1t", "system:g1t": "g1t" }, code: true });
492 assert.deepEqual(
493 digest.people.actors.map((actor) => [actor.name, actor.kind, actor.count, actor.parts]),
494 [
495 ["g1t", "agent", 6, { items: 3, chat: 3 }],
496 ["ana", "person", 2, { chat: 2 }],
497 ],
498 );
499 assert.equal(digest.summary.people, 1);
500 // A person whose name could not be read is an unknown account, never "someone".
501 const unread = digestOf({
502 slug: "acme",
503 repos: REPOS,
504 activity: null,
505 chat: { ...chat, authors: [{ key: "user:usr_gone", messages: 1 }] },
506 names: {},
507 code: true,
508 });
509 assert.deepEqual(
510 unread.people.actors.map((actor) => actor.name),
511 ["unknown account"],
512 );
513});
514
515test("the digest says which part didn't answer, and counts what could be read", () => {
516 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true });
517 assert.deepEqual(noChat.missing, ["Chat"]);
518 assert.equal(
519 noChat.people.lines.some((line) => line.key === "chat"),
520 false,
521 );
522 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, code: true });
523 assert.deepEqual(noCode.missing, ["Code"]);
524 assert.deepEqual(
525 noCode.people.lines.map((line) => line.text),
526 ["120 messages in 5 conversations"],
527 );
528 assert.equal(noCode.summary.peopleRead, true);
529 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
530 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, code: false });
531 assert.deepEqual(member.missing, []);
532 assert.deepEqual(
533 member.people.lines.map((line) => line.key),
534 ["docs", "chat"],
535 );
536 // A digest cut short says so.
537 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, code: true });
538 assert.equal(cut.partial, true);
539 // Nobody named: an import's pushes are still commits.
540 const imported = digestOf({
541 slug: "acme",
542 repos: REPOS,
543 activity: {
544 ...busyDay(),
545 folios: null,
546 repos: [
547 {
548 ...quietRepo("rep_web"),
549 pushes: {
550 count: 2,
551 commits: 7,
552 branches: ["main"],
553 by: [{ actor: "", count: 2, commits: 7 }],
554 default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) },
555 },
556 },
557 ],
558 },
559 chat: null,
560 names: {},
561 code: true,
562 });
563 assert.deepEqual(
564 imported.people.lines.map((line) => [line.text, line.to]),
565 [["7 commits to 1 project", "/acme/web/activity"]],
566 );
567 assert.equal(imported.summary.people, 0);
568});
569
570test("the sentence: people first, then what landed; honest when quiet or unread", () => {
571 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
572 assert.equal(
573 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
574 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes and 12 changes landed.",
575 );
576 const quiet = digestOf({
577 slug: "acme",
578 repos: REPOS,
579 activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } },
580 chat: { ...busyChat, messages: 0, channels: 0, authors: [] },
581 names: {},
582 code: true,
583 });
584 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
585 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
586 // Only merges.
587 const mergesOnly = digestOf({
588 slug: "acme",
589 repos: REPOS,
590 activity: { ...busyDay(), repos: [], folios: null },
591 chat: { ...busyChat, authors: [], messages: 0, channels: 0 },
592 names: {},
593 code: true,
594 });
595 assert.equal(spanSentence({ words: "Since this morning" }, mergesOnly, 5), "Since this morning, 5 changes landed.");
596 // People only talked.
597 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, code: true });
598 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
599 // Neither events nor chat answered.
600 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
601 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, 5 changes landed; what people did couldn't be read.");
602 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
603 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read.");
604 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
605 assert.equal(waitingSentence(0), "Nothing needs you right now.");
606 assert.equal(waitingSentence(1), "1 thing needs you.");
607 assert.equal(waitingSentence(8), "8 things need you.");
608});
609
610test("what landed: every merge in the span, newest first, g1t's older ones as @g1t", () => {
611 const merged = landedIn(
612 [
613 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
614 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
615 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
616 pull({ number: 4, status: "open", mergedAt: null }),
617 ],
618 SPAN,
619 );
620 assert.deepEqual(
621 merged.map((change) => [change.kind, change.title, change.detail]),
622 [
623 ["merge", "Fix the flaky test", "web#2 · by bo"],
624 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo"],
625 ],
626 );
627});
628
629test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
630 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
631 assert.deepEqual(
632 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to]),
633 [
634 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity"],
635 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0"],
636 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity"],
637 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages"],
638 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity"],
639 ],
640 );
641 const builds = landedDeploys([
642 {
643 key: "deploy:site:d1",
644 project: "site",
645 kind: "production",
646 outcome: "live",
647 commit: "abcdef1",
648 branch: null,
649 at: NOW - HOUR,
650 to: "/acme/site/deployments/d1",
651 by: "@g1t",
652 },
653 {
654 key: "deploy:site:d2",
655 project: "site",
656 kind: "preview",
657 outcome: "live",
658 commit: "abcdef2",
659 branch: "fix",
660 at: NOW - HOUR,
661 to: "/acme/site/deployments/d2",
662 by: "bo",
663 },
664 {
665 key: "deploy:site:d3",
666 project: "site",
667 kind: "production",
668 outcome: "failed",
669 commit: "abcdef3",
670 branch: null,
671 at: NOW - HOUR,
672 to: "/acme/site/deployments/d3",
673 by: "bo",
674 },
675 ]);
676 assert.deepEqual(
677 builds.map((change) => [change.kind, change.title, change.failed ?? false]),
678 [
679 ["deploy", "Production of site went live", false],
680 ["deploy", "Production of site failed to deploy", true],
681 ],
682 );
683 const all = landed([fromActivity, builds.slice(0, 1)]);
684 assert.deepEqual(
685 all.map((change) => change.key),
686 ["push:rep_cli", "landed:deploy:site:d1", "package:rep_api:container:api:1.4.0", "push:rep_web", "release:rep_web:v1.4.0", "push:rep_api"],
687 );
688});
689
690test("a production build is its own row only where no merge or push on the list says it", () => {
691 const row = (key: string, commit: string): DeployRow => ({
692 key,
693 project: "site",
694 kind: "production",
695 outcome: "live",
696 commit,
697 branch: null,
698 at: NOW - HOUR,
699 to: `/acme/site/deployments/${key}`,
700 by: "bo",
701 });
702 const merge: LandedChange = {
703 key: "merge:1",
704 kind: "merge",
705 title: "Fix",
706 detail: "",
707 to: "/acme/site/pull/1",
708 at: NOW - 2 * HOUR,
709 sha: "aaaaaaa111",
710 repoId: "r",
711 };
712 const push: LandedChange = {
713 key: "push:1",
714 kind: "push",
715 title: "Push",
716 detail: "",
717 to: "/acme/site/activity",
718 at: NOW - 2 * HOUR,
719 sha: "bbbbbbb222",
720 repoId: "r",
721 shipped: { state: "live", at: NOW - HOUR, to: "/acme/site/deployments/d2" },
722 };
723 const builds = landedDeploys([row("d1", "aaaaaaa"), row("d2", "ccccccc"), row("d3", "ddddddd")], [merge, push]);
724 assert.deepEqual(
725 builds.map((change) => change.key),
726 ["landed:d3"],
727 );
728});
729
730const deploy = (over: Partial<Deployment>): Deployment => ({
731 id: "d1",
732 kind: "production",
733 branch: null,
734 number: null,
735 commit: "abcdef1234",
736 status: "ready",
737 url: "https://site.g1t.page",
738 error: null,
739 warnings: [],
740 buildSeconds: 40,
741 createdBy: "bo",
742 createdAt: iso(NOW - 2 * HOUR),
743 finishedAt: iso(NOW - HOUR),
744 ...over,
745});
746
747test("builds that finished in the span: live or failed, never skipped", () => {
748 const rows = deploysIn(
749 [
750 {
751 slug: "site",
752 deployments: [
753 deploy({}),
754 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
755 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
756 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
757 deploy({ id: "d5", status: "building", finishedAt: null }),
758 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
759 ],
760 },
761 ],
762 "acme",
763 SPAN,
764 { "user:bo": "bo" },
765 );
766 assert.deepEqual(
767 rows.map((row) => [row.key, row.outcome]),
768 [
769 ["deploy:site:d1", "live"],
770 ["deploy:site:d2", "live"],
771 ["deploy:site:d3", "failed"],
772 ],
773 );
774 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
775 assert.equal(rows[0]!.commit, "abcdef1");
776 // The maker is named, never shown as an id; an older agent's by its handle; an id identity doesn't know as someone.
777 assert.equal(rows[0]!.by, "bo");
778 const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
779 assert.equal(unknown[0]!.by, "someone");
780 const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
781 assert.equal(robot[0]!.by, "@otto");
782});
783
784test("running now: workflow runs and builds going", () => {
785 const builds = runningDeploys(
786 [
787 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
788 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
789 ],
790 "acme",
791 );
792 assert.deepEqual(
793 builds.map((row) => row.status),
794 ["Building"],
795 );
796 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
797 id: "run_1",
798 workflowId: "wf_1",
799 path: ".g1t/workflows/ci.yml",
800 name: "CI",
801 title: "Fix the flaky test",
802 number: 12,
803 attempt: 1,
804 event: "push",
805 ref: "refs/heads/main",
806 sha: "abcdef1",
807 pull: null,
808 status: "in_progress",
809 conclusion: null,
810 error: null,
811 actor: "ana",
812 createdAt: iso(NOW - 10 * 60_000),
813 startedAt: iso(NOW - 9 * 60_000),
814 finishedAt: null,
815 ...over,
816 });
817 const workflows = runningWorkflows([
818 {
819 repo,
820 runs: [
821 run({}),
822 run({ id: "run_2", status: "queued", startedAt: null, title: "" }),
823 run({ id: "run_3", status: "completed", conclusion: "success" }),
824 run({ id: "run_4", status: "action_required" }),
825 ],
826 },
827 ]);
828 assert.deepEqual(
829 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
830 [
831 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
832 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
833 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
834 ],
835 );
836 // Longest-going first within a kind: workflow runs, then builds.
837 assert.deepEqual(
838 running([builds, workflows.slice(0, 1)]).map((row) => row.key),
839 ["workflow:acme/web:run_1", "deploy:site:d1"],
840 );
841});
842
843test("the span's months: one, or the ones it crosses", () => {
844 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
845 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
846 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
847});
848
849test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
850 const september = {
851 groups: [
852 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
853 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
854 ],
855 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
856 };
857 const october = {
858 groups: [
859 {
860 key: "2026-10-09",
861 label: "2026-10-09",
862 chargedMicros: 0,
863 lines: [
864 { kind: "Workflow minutes", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
865 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
866 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
867 ],
868 },
869 ],
870 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
871 };
872 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
873 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
874 assert.equal(spend.totalMicros, 5_600_000);
875 assert.deepEqual(
876 spend.lines.map((l) => [l.kind, l.micros, l.count]),
877 [
878 ["Workflow minutes", 5_000_000, 14],
879 ["Sandbox time", 600_000, 12],
880 ],
881 );
882 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
883 assert.equal(spend.monthMicros, 13_609_000);
884 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
885});
886
887// --- What landed: shipped, made in a session, day by day -----------------------
888
889const merge = (over: Partial<LandedChange> = {}): LandedChange => ({
890 key: "landed:acme/web#12",
891 kind: "merge",
892 title: "Add retries",
893 detail: "web#12 · by ana",
894 to: "/acme/web/pull/12",
895 at: NOW - 2 * HOUR,
896 sha: "m12",
897 repoId: "rep_web",
898 ...over,
899});
900
901test("each merge and push says whether it is live, from what deployments recorded", () => {
902 const push = merge({ key: "push:rep_web:evt_1", kind: "push", sha: "p1", at: NOW - HOUR });
903 const release = merge({ key: "release:rep_web:v1", kind: "release", sha: null });
904 assert.deepEqual(shippedAsks([merge(), push, release]), [
905 {
906 repo_id: "rep_web",
907 commits: [
908 { sha: "m12", landed_at: iso(NOW - 2 * HOUR) },
909 { sha: "p1", landed_at: iso(NOW - HOUR) },
910 ],
911 },
912 ]);
913 const answers = [
914 {
915 repo_id: "rep_web",
916 deploys: true,
917 commits: [
918 { sha: "m12", state: "live" as const, deployed_at: iso(NOW - 2 * HOUR + 9 * 60_000), url: "/acme/web/actions/runs/run_1", environment: "production" },
919 { sha: "p1", state: "waiting" as const, deployed_at: null, url: null, environment: null },
920 ],
921 },
922 ];
923 const [shippedMerge, shippedPush, untouched] = withShipped([merge(), push, release], answers);
924 assert.deepEqual(shippedMerge!.shipped, { state: "live", at: NOW - 2 * HOUR + 9 * 60_000, to: "/acme/web/actions/runs/run_1" });
925 assert.equal(shippedLabel(shippedMerge!.shipped!, shippedMerge!), "Live 9 min after merge");
926 assert.equal(shippedLabel(shippedPush!.shipped!, shippedPush!), "Waiting to deploy");
927 assert.equal(shippedLabel({ state: "failed", at: null, to: "/x" }, push), "Deploy failed");
928 assert.equal(shippedLabel({ state: "deploying", at: null, to: null }, push), "Deploying");
929 assert.equal(shippedLabel({ state: "live", at: push.at + 3 * HOUR, to: null }, push), "Live 3 h after push");
930 assert.equal(untouched!.shipped, undefined);
931 // A project that does not deploy says nothing; deployments not answering leaves rows as they were.
932 assert.equal(withShipped([merge()], [{ repo_id: "rep_web", deploys: false, commits: [] }])[0]!.shipped, undefined);
933 assert.deepEqual(withShipped([merge()], null), [merge()]);
934});
935
936test("a change made in a harness session carries the agent mark, linked to the session", () => {
937 const [marked, plain] = withSessions(
938 [merge(), merge({ key: "landed:acme/web#13", sha: "m13" })],
939 [
940 {
941 repo: { namespace: "acme", name: "web" },
942 pulls: [
943 {
944 number: 12,
945 sessions: [
946 { id: "ses_1", harness: "claude_code" },
947 { id: "ses_2", harness: "codex" },
948 ],
949 },
950 ],
951 },
952 ],
953 );
954 assert.deepEqual(marked!.agent, { harness: "claude_code", to: "/acme/-/work/sessions/ses_1" });
955 assert.equal(agentLabel(marked!.agent!), "Made in a Claude Code session");
956 assert.equal(plain!.agent, undefined);
957 assert.equal(agentLabel({ harness: null, to: null }), "Made in a session");
958 assert.deepEqual(sessionTrailers("Fix the bell\n\nG1t-Session: ses_01ABC\ng1t-session: ses_01abc\nG1t-Session: ses_02x"), ["ses_01abc", "ses_02x"]);
959 assert.deepEqual(sessionTrailers("Mentions G1t-Session: ses_1 in prose"), []);
960});
961
962test("each person's push to a default branch is a row: its commit, who pushed, and its session", () => {
963 const repo = { namespace: "acme", name: "web", defaultBranch: "main" };
964 const history = {
965 rep_web: [
966 { hash: "c3", parents: ["c2"], message: "Fix the bell's count\n\nG1t-Session: ses_9" },
967 { hash: "c2", parents: ["c1"], message: "Tidy" },
968 { hash: "c1", parents: ["c0"], message: "Start" },
969 { hash: "c0", parents: [], message: "Root" },
970 ],
971 };
972 const push = (id: string, after: string, before: string, at: number, actor = "usr_ana") => ({
973 repoId: "rep_web",
974 repo,
975 push: { id, time: iso(at), actor, data: { after, before } },
976 by: "ana",
977 });
978 const span = { from: NOW - DAY, now: NOW };
979 const rows = pushRows(
980 [push("evt_2", "c3", "c2", NOW - HOUR), push("evt_1", "c2", "c0", NOW - 2 * HOUR), push("evt_0", "c1", "c0", NOW - 2 * DAY)],
981 history,
982 span,
983 );
984 assert.deepEqual(
985 rows.map((row) => [row.title, row.detail, row.sha, row.to, row.agent?.to ?? null]),
986 [
987 ["Fix the bell's count", "web · pushed to main · by ana", "c3", "/acme/web/commit/c3", "/acme/-/work/sessions/ses_9"],
988 ["2 commits pushed to main", "web · Tidy · by ana", "c2", "/acme/web/activity", null],
989 ],
990 );
991 // g1t's own pushes (a pull request landing) are listed as the merge instead.
992 assert.deepEqual(pushRows([push("evt_3", "c3", "c2", NOW - HOUR, "usr_g1t_agent")], history, span), []);
993});
994
995test("what landed over the week reads day by day, newest first", () => {
996 const tz = "America/Los_Angeles";
997 const days = landedByDay([merge({ at: NOW - DAY }), merge({ key: "b", at: NOW - HOUR }), merge({ key: "c", at: NOW - 3 * DAY })], NOW, tz);
998 assert.deepEqual(
999 days.map((day) => [day.label, day.rows.length]),
1000 [
1001 ["Today", 1],
1002 ["Yesterday", 1],
1003 ["Tue, Oct 6", 1],
1004 ],
1005 );
1006});
1007
1008test("while you were away: what settled by itself, what shipped, and what still needs you", () => {
1009 const span = { from: NOW - DAY, now: NOW };
1010 const settled = settledIn(
1011 [
1012 item({ id: "n1", doneAt: iso(NOW - HOUR), settledReason: "CI passed in run 330" }),
1013 item({ id: "n2", doneAt: iso(NOW - 2 * HOUR), settledReason: "Merged", settledBy: "ana" }),
1014 // Marked done by hand: yours, not g1t's.
1015 item({ id: "n3", doneAt: iso(NOW - HOUR) }),
1016 // Before the span, or another workspace's.
1017 item({ id: "n4", doneAt: iso(NOW - 2 * DAY), settledReason: "Merged" }),
1018 item({ id: "n5", doneAt: iso(NOW - HOUR), settledReason: "Merged", workspace: "other", repo: "other/x" }),
1019 ],
1020 "acme",
1021 true,
1022 span,
1023 );
1024 assert.deepEqual(
1025 settled.map((row) => [row.key, row.why, row.where]),
1026 [
1027 ["settled:n1", "CI passed in run 330", "acme/web#12"],
1028 ["settled:n2", "Merged by @ana", "acme/web#12"],
1029 ],
1030 );
1031 assert.equal(
1032 settledIn([item({ doneAt: iso(NOW - HOUR), settledReason: "Merged" })], "acme", false, span).length,
1033 0,
1034 "nothing about repositories without Code",
1035 );
1036 const away = awayOf({
1037 settled,
1038 landed: [
1039 merge({ shipped: { state: "live", at: NOW, to: null } }),
1040 merge({ key: "x", shipped: { state: "waiting", at: null, to: null } }),
1041 merge({ key: "y" }),
1042 ],
1043 needs: 3,
1044 });
1045 assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 });
1046});
1047
1048test("Summarize asks about the span the page shows, as a person would", () => {
1049 const now = Date.UTC(2026, 9, 14, 22, 0);
1050 assert.equal(
1051 summarizeQuestion({ key: "last", note: null, words: "Since this afternoon", from: Date.UTC(2026, 9, 14, 18, 0), now }, "UTC"),
1052 "Summarize what changed since I was last here (6:00 PM) in plain English.",
1053 );
1054 assert.equal(
1055 summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from: now - DAY, now }, "UTC"),
1056 "Summarize what changed in the last 24 hours in plain English.",
1057 );
1058 // A first visit shows the last 24 hours, so it is not asked as "since I was last here".
1059 assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from: now - DAY, now }, "UTC"), /last here/);
1060});
1061
1062test("Explain asks about the one change, by what it is", () => {
1063 assert.equal(
1064 explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }),
1065 "Explain pull request #12 in acme/web in plain English: what changed, why, and is it live?",
1066 );
1067 assert.equal(
1068 explainQuestion({ kind: "push", title: "Fix login", to: "/acme/web/commit/abc1234def" }),
1069 "Explain commit abc1234 in acme/web in plain English: what changed, why, and is it live?",
1070 );
1071 assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/);
1072 assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/);
1073});