OXYGENHUB
# 5888949525ec oxygenhub-vscode / test / hub.test.mjs
8.3 KiB · last changed File links keep line ranges and readable state-address permalinks; API types brought up to date; 0.6.2 1 month ago source blame history raw
1
// Against a real hub when the binary exists: create a repository, an
2
// issue and a proposal through the client.
3
import { test } from "node:test";
4
import assert from "node:assert/strict";
5
import { spawn, execFileSync } from "node:child_process";
6
import { mkdtempSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
7
import { tmpdir } from "node:os";
8
import { join } from "node:path";
9
import { HubClient, HubError, encodeDiffQuery, decodeDiffQuery, hubTarget, releaseNotes, looksPrerelease } from "../out/hub.mjs";
10
11
const HUB = join(import.meta.dirname, "../../oxygenhub/target/release/oxygenhub");
12
const OXY = join(import.meta.dirname, "../../oxygen/target/release/oxy");
13
14
test("hub client end to end", { skip: !existsSync(HUB) || !existsSync(OXY) }, async () => {
15
  const data = mkdtempSync(join(tmpdir(), "hubvs-"));
16
  const port = 17300 + Math.floor(Math.random() * 1000);
17
  const base = `http://127.0.0.1:${port}`;
18
  writeFileSync(join(data, "hub.toml"), `[server]\nbind = "127.0.0.1:${port}"\nbase_url = "${base}"\n[auth]\npush_tokens = ["site"]\n[registration]\nmode = "open"\n`);
19
  execFileSync(HUB, ["--data", data, "init"], { stdio: "ignore" });
20
  execFileSync(HUB, ["--data", data, "admin", "user", "create", "andy", "--admin"], { stdio: "ignore" });
21
  const proc = spawn(HUB, ["--data", data, "serve"], { stdio: "ignore" });
22
  try {
23
    for (let i = 0; i < 50; i++) {
24
      try {
25
        await fetch(`${base}/health`);
26
        break;
27
      } catch {
28
        await new Promise((r) => setTimeout(r, 100));
29
      }
30
    }
31
    const client = new HubClient(base, "site");
32
    // The client has no createRepository method by design; use fetch directly.
33
    const res = await fetch(`${base}/api/v1/repos`, { method: "POST", headers: { authorization: "Bearer site", "content-type": "application/json" }, body: JSON.stringify({ owner: "andy", name: "demo" }) });
34
    assert.equal(res.status, 201);
35
    // Push a bookmark with oxy so a proposal has something to add.
36
    const work = mkdtempSync(join(tmpdir(), "hubvs-work-"));
37
    const env = { ...process.env, OXY_HOME: mkdtempSync(join(tmpdir(), "hubvs-home-")) };
38
    const oxy = (...args) => execFileSync(OXY, ["--plain", ...args], { cwd: work, env, stdio: ["ignore", "pipe", "pipe"] }).toString();
39
    oxy("init");
40
    oxy("remote", "add", "origin", `${base}/andy/demo`);
41
    oxy("config", "remote.origin.token", "site");
42
    mkdirSync(join(work, "src"), { recursive: true });
43
    writeFileSync(join(work, "src/a.txt"), "a\n");
44
    oxy("commit", "-m", "base");
45
    oxy("push", "origin");
46
    oxy("bookmark", "create", "feat");
47
    oxy("bookmark", "switch", "feat");
48
    writeFileSync(join(work, "src/b.txt"), "b\n");
49
    writeFileSync(join(work, "src/a.txt"), "a\nA\n");
50
    oxy("commit", "-m", "feat");
51
    oxy("push", "origin", "--bookmark", "feat");
52
    const p = await client.createProposal("andy", "demo", { title: "Feat", target: "main", source: "feat", body: "", draft: false });
53
    assert.equal(p.number, 1);
54
    assert.equal(p.changes.length, 1);
55
    // The proposal's files and both sides, as the diff editors fetch them.
56
    const files = await client.proposalFiles("andy", "demo", 1);
57
    assert.deepEqual(
58
      files.map((f) => [f.path, f.kind]),
59
      [
60
        ["src/a.txt", "modified"],
61
        ["src/b.txt", "added"],
62
      ],
63
    );
64
    assert.equal(await client.proposalContent("andy", "demo", 1, "base", "src/a.txt"), "a\n");
65
    assert.equal(await client.proposalContent("andy", "demo", 1, "head", "src/a.txt"), "a\nA\n");
66
    assert.equal(await client.proposalContent("andy", "demo", 1, "base", "src/b.txt"), undefined, "absent on the target side");
67
    // A review comment from the diff comes back on its line.
68
    const c = await client.commentProposal("andy", "demo", 1, "why?", "src/a.txt", 2);
69
    assert.equal(c.line, 2);
70
    const comments = await client.proposalComments("andy", "demo", 1);
71
    assert.equal(comments[0].line, 2);
72
    const list = await client.proposals("andy", "demo");
73
    assert.equal(list[0].title, "Feat");
74
    const issue = await client.createIssue("andy", "demo", "Bug", "text");
75
    assert.equal(issue.number, 2);
76
    await client.commentIssue("andy", "demo", 2, "more");
77
    const closed = await client.setIssueState("andy", "demo", 2, "closed");
78
    assert.equal(closed.state, "closed");
79
    // The inbox: the site token acts as the administrator, andy.
80
    const inbox = await client.notifications();
81
    assert.equal(typeof inbox.unread, "number");
82
    assert.ok(Array.isArray(inbox.items));
83
    await client.markAllNotificationsRead();
84
    assert.equal((await client.notifications()).unread, 0);
85
    // Releases: a tag pushed with oxy, then released with notes from history.
86
    oxy("bookmark", "switch", "main");
87
    oxy("tag", "create", "v1.0", "-m", "First");
88
    oxy("push", "origin");
89
    const tags = await client.tags("andy", "demo");
90
    assert.deepEqual(tags.map((t) => t.name), ["v1.0"]);
91
    assert.deepEqual(await client.releases("andy", "demo"), []);
92
    const hist = await client.history("andy", "demo", "v1.0", 500);
93
    const rel = await client.createRelease("andy", "demo", { tag: "v1.0", title: "One", notes: releaseNotes(hist.changes, []), prerelease: false });
94
    assert.equal(rel.tag, "v1.0");
95
    assert.match(rel.notes, /- base \(/);
96
    const rels = await client.releases("andy", "demo");
97
    assert.equal(rels.length, 1);
98
    assert.equal(rels[0].title, "One");
99
    const me = await client.me();
100
    assert.equal(me.username, "andy");
101
    await assert.rejects(() => new HubClient(base, "nope").me(), (e) => e instanceof HubError && e.status === 401);
102
  } finally {
103
    proc.kill();
104
  }
105
});
106
107
test("diff document queries round trip", () => {
108
  const ref = { base: "https://earth.example:7343", owner: "andy", name: "demo", number: 7, side: "head", path: "src/dir with space/a.txt" };
109
  const back = decodeDiffQuery(encodeDiffQuery(ref), "/" + ref.path);
110
  assert.deepEqual(back, ref);
111
  assert.equal(decodeDiffQuery("hub=x&owner=a&name=b&number=0&side=head", "/p"), undefined, "number must be positive");
112
  assert.equal(decodeDiffQuery("hub=x&owner=a&name=b&number=3&side=left", "/p"), undefined, "side is base or head");
113
  assert.equal(decodeDiffQuery("owner=a&name=b&number=3&side=head", "/p"), undefined, "the hub is required");
114
});
115
116
test("hub page urls open in the editor when they are proposals or issues", () => {
117
  assert.deepEqual(hubTarget("https://earth.example:7343/andy/oxygen/proposal/12"), { kind: "proposal", base: "https://earth.example:7343", owner: "andy", name: "oxygen", number: 12 });
118
  assert.deepEqual(hubTarget("https://h/andy/demo/issue/3#c7"), { kind: "issue", base: "https://h", owner: "andy", name: "demo", number: 3 });
119
  assert.equal(hubTarget("https://h/andy/demo/releases/tag/v1").kind, "other");
120
  assert.equal(hubTarget("https://h/andy/demo/proposal/x").kind, "other");
121
});
122
123
test("release notes list the changes since the previous release, newest first", () => {
124
  const c = (hash, message, when_ms) => ({ hash, message, when_ms, author: {}, deps: [], head: false, signature: {} });
125
  const old = [c("a".repeat(64), "base", 1)];
126
  const now = [...old, c("b".repeat(64), "Add feature\n\nbody", 2), c("c".repeat(64), "Fix bug", 3)];
127
  assert.equal(releaseNotes(now, old, "v1.0"), `Changes since v1.0:\n\n- Fix bug (${"c".repeat(12)})\n- Add feature (${"b".repeat(12)})\n`);
128
  assert.equal(releaseNotes(old, old, "v1.0"), "No changes since the last release.\n");
129
  assert.match(releaseNotes(old, []), /^Changes:\n\n- base/);
130
});
131
132
test("pre-release tags are recognised", () => {
133
  for (const t of ["v1.0-rc1", "2.0.0-beta.2", "v3-alpha", "1.0.0-pre", "v2.1b3".replace("b3", "-b3")]) {
134
    assert.ok(looksPrerelease(t), t);
135
  }
136
  for (const t of ["v1.0", "2.0.0", "release-2026", "v1.0-final", "debate"]) {
137
    assert.ok(!looksPrerelease(t), t);
138
  }
139
});
140
141
test("file links: line ranges and state-address permalinks", async () => {
142
  const { hubUrl } = await import("../out/hub.mjs");
143
  const a = "a".repeat(64), b = "b".repeat(64);
144
  assert.equal(
145
    hubUrl("https://h:7343", "andy", "oxy", { kind: "file", reference: `${a}+${b}`, path: "src/main.rs", line: 10, endLine: 20 }),
146
    `https://h:7343/andy/oxy/file/${a}+${b}/src/main.rs#L10-L20`,
147
  );
148
  assert.equal(hubUrl("https://h", "a", "r", { kind: "file", reference: "main", path: "x.rs", line: 3, endLine: 3 }), "https://h/a/r/file/main/x.rs#L3");
149
  assert.equal(hubUrl("https://h", "a", "r", { kind: "file", reference: "feature/x", path: "x.rs" }), "https://h/a/r/file/feature%2Fx/x.rs");
150
});