Skip to content
81 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { MAX_CHANGES, WATCH_MS, byRepo, cleanChange, cleanRepoId, cleanRepoPath, named, stillWatching, watchUntil } from "./watch.ts";
5
6const AT = "2026-10-11T10:00:00Z";
7
8test("a change keeps only what a change carries", () => {
9 const change = cleanChange({
10 repo_id: "rep_1",
11 kind: "git.push",
12 ref: "refs/heads/main",
13 sha: "abc123",
14 commits: 3,
15 at: AT,
16 title: "a secret title",
17 body: "and its body",
18 });
19 assert.deepEqual(change, { repo_id: "rep_1", kind: "git.push", ref: "refs/heads/main", sha: "abc123", commits: 3, at: AT });
20});
21
22test("what is not a change is dropped", () => {
23 assert.equal(cleanChange(null), null);
24 assert.equal(cleanChange("git.push"), null);
25 assert.equal(cleanChange({ kind: "git.push", at: AT }), null);
26 assert.equal(cleanChange({ repo_id: "rep 1", kind: "git.push", at: AT }), null);
27 assert.equal(cleanChange({ repo_id: "rep_1", kind: "<script>", at: AT }), null);
28 // A field of the wrong shape goes; the change stays.
29 assert.deepEqual(cleanChange({ repo_id: "rep_1", kind: "pull.updated", number: -1, sha: "not a sha", run: "run 9", at: AT }), {
30 repo_id: "rep_1",
31 kind: "pull.updated",
32 at: AT,
33 });
34 assert.deepEqual(cleanChange({ repo_id: "rep_1", kind: "run.updated", run: "run_9", number: 4, at: AT }), {
35 repo_id: "rep_1",
36 kind: "run.updated",
37 run: "run_9",
38 number: 4,
39 at: AT,
40 });
41});
42
43test("changes go to their repositories in the order they came", () => {
44 const grouped = byRepo([
45 { repo_id: "rep_1", kind: "pull.opened", number: 1, at: AT },
46 { repo_id: "rep_2", kind: "issue.opened", number: 2, at: AT },
47 { repo_id: "rep_1", kind: "pull.updated", number: 1, at: AT },
48 { nonsense: true },
49 ]);
50 assert.deepEqual([...grouped.keys()], ["rep_1", "rep_2"]);
51 assert.deepEqual(
52 grouped.get("rep_1")!.map((change) => change.kind),
53 ["pull.opened", "pull.updated"],
54 );
55 assert.equal(byRepo("nope").size, 0);
56 const many = Array.from({ length: MAX_CHANGES + 10 }, (_, n) => ({ repo_id: "rep_1", kind: "pull.updated", number: n, at: AT }));
57 assert.equal(byRepo(many).get("rep_1")!.length, MAX_CHANGES);
58});
59
60test("each watcher hears the repository by the name it asked for", () => {
61 const changes = [{ repo_id: "rep_1", kind: "git.push", at: AT }];
62 assert.deepEqual(named(changes, "acme/rocket"), [{ repo_id: "rep_1", kind: "git.push", at: AT, repo: "acme/rocket" }]);
63 assert.equal(Object.hasOwn(changes[0]!, "repo"), false, "the topic's own list is left as it was");
64});
65
66test("repository ids and paths are checked", () => {
67 assert.equal(cleanRepoId("rep_01J9"), "rep_01J9");
68 assert.equal(cleanRepoId("../x"), null);
69 assert.equal(cleanRepoPath("acme/rocket.js"), "acme/rocket.js");
70 assert.equal(cleanRepoPath("acme"), null);
71 assert.equal(cleanRepoPath("acme/rocket/extra"), null);
72});
73
74test("a watch lasts its time and no longer", () => {
75 const now = Date.parse(AT);
76 const until = watchUntil(now);
77 assert.equal(until, now + WATCH_MS);
78 assert.equal(stillWatching(until, now + WATCH_MS - 1), true);
79 assert.equal(stillWatching(until, now + WATCH_MS), false);
80 assert.equal(stillWatching(null, now), false);
81});