Ctrl K
ComunidadGuías…

tests/presenceIpc.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// The Discord IPC client (server/presence/discordIpc.ts) against a FAKE
// Discord: a unix socket in a temp dir that speaks the framing from
// shared/presence/frame.ts. Everything the real Discord would punish — a
// second SET_ACTIVITY inside the rate window, an unchanged activity re-sent,
// a retry against an id it already refused — and everything the owner would
// notice — a card left behind on quit, a crash when Discord is not running —
// is asserted on the frames that actually cross the socket.

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { createServer, type Server, type Socket } from "node:net";
import os from "node:os";
import path from "node:path";
import { after, describe, it } from "node:test";
import { encodeFrame, FrameDecoder, OPCODE } from "../shared/presence/frame.ts";
import type { DiscordActivity } from "../shared/presence/types.ts";
import { DiscordIpcClient } from "../server/presence/discordIpc.ts";

const dir = mkdtempSync(path.join(os.tmpdir(), "astrolabe-ipc-"));
after(() => rmSync(dir, { recursive: true, force: true }));
let seq = 0;

interface Seen {
  op: number;
  json: Record<string, unknown>;
}

/** A Discord that answers READY, or closes with `refuse` when given. */
class FakeDiscord {
  readonly path = path.join(dir, `d${++seq}`, "discord-ipc-7");
  readonly frames: Seen[] = [];
  readonly sockets: Socket[] = [];
  ended = 0;
  connections = 0;
  refuse: number | null = null;
  private server: Server | null = null;

  async listen(): Promise<void> {
    const { mkdirSync } = await import("node:fs");
    mkdirSync(path.dirname(this.path), { recursive: true });
    this.server = createServer((socket) => {
      this.connections++;
      this.sockets.push(socket);
      const decoder = new FrameDecoder();
      socket.on("error", () => {});
      socket.on("end", () => {
        this.ended++;
        socket.end();
      });
      socket.on("data", (chunk) => {
        for (const f of decoder.push(chunk)) {
          this.frames.push({ op: f.op, json: f.json as Record<string, unknown> });
          if (f.op === OPCODE.HANDSHAKE) {
            if (this.refuse !== null) {
              socket.write(encodeFrame(OPCODE.CLOSE, { code: this.refuse, message: "Invalid Client ID" }));
              socket.end();
            } else socket.write(encodeFrame(OPCODE.FRAME, { cmd: "DISPATCH", evt: "READY", data: { v: 1 } }));
          }
        }
      });
    });
    await new Promise<void>((resolve) => this.server?.listen(this.path, resolve));
  }

  async close(): Promise<void> {
    for (const s of this.sockets) s.destroy();
    this.sockets.length = 0;
    await new Promise<void>((resolve) => (this.server ? this.server.close(() => resolve()) : resolve()));
    this.server = null;
  }

  sets(): Seen[] {
    return this.frames.filter((f) => f.op === OPCODE.FRAME && f.json.cmd === "SET_ACTIVITY");
  }
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function until(what: string, pred: () => boolean, ms = 2_000): Promise<void> {
  const end = Date.now() + ms;
  while (!pred()) {
    if (Date.now() > end) throw new Error(`timed out waiting for ${what}`);
    await sleep(5);
  }
}

const A: DiscordActivity = { type: 0, details: "Writing", state: "Chapter 3" };
const B: DiscordActivity = { type: 0, details: "Writing", state: "Chapter 4" };
const C: DiscordActivity = { type: 0, details: "Drawing" };

function client(candidates: string[], logs: string[] = []): DiscordIpcClient {
  return new DiscordIpcClient({
    clientId: "123456789012345678",
    pid: 4242,
    candidates,
    rateMs: 50,
    backoff: { first: 20, cap: 100 },
    log: (line) => logs.push(line),
  });
}

describe("DiscordIpcClient against a fake Discord", { skip: process.platform === "win32" }, () => {
  it("handshakes, reaches connected, sends one SET_ACTIVITY with pid and nonce", async () => {
    const d = new FakeDiscord();
    await d.listen();
    const c = client([path.join(dir, "missing", "discord-ipc-0"), d.path]);
    try {
      const seen: string[] = [];
      c.onConnection((s) => seen.push(s));
      c.start();
      await until("connected", () => c.connection === "connected");
      assert.deepEqual(d.frames[0], { op: OPCODE.HANDSHAKE, json: { v: 1, client_id: "123456789012345678" } });
      assert.deepEqual(seen, ["searching", "connected"]);
      c.setActivity(A);
      await until("a SET_ACTIVITY", () => d.sets().length === 1);
      const args = d.sets()[0].json.args as Record<string, unknown>;
      assert.equal(args.pid, 4242);
      assert.deepEqual(args.activity, A);
      assert.match(String(d.sets()[0].json.nonce), /^[0-9a-f-]{36}$/);
    } finally {
      await c.stop();
      await d.close();
    }
  });

  it("coalesces: two quick changes are one frame now and ONE more after the window, carrying the latest", async () => {
    const d = new FakeDiscord();
    await d.listen();
    const c = client([d.path]);
    try {
      c.start();
      await until("connected", () => c.connection === "connected");
      c.setActivity(A);
      c.setActivity(B);
      c.setActivity(C);
      await until("the first frame", () => d.sets().length === 1);
      assert.deepEqual((d.sets()[0].json.args as Record<string, unknown>).activity, A);
      await sleep(120);
      assert.equal(d.sets().length, 2);
      assert.deepEqual((d.sets()[1].json.args as Record<string, unknown>).activity, C);
      const nonces = new Set(d.sets().map((f) => f.json.nonce));
      assert.equal(nonces.size, 2, "a fresh nonce per command");
      // The same activity again, after the window: nothing.
      c.setActivity({ details: "Drawing", type: 0 });
      await sleep(80);
      assert.equal(d.sets().length, 2);
    } finally {
      await c.stop();
      await d.close();
    }
  });

  it("answers PING with PONG and the same body", async () => {
    const d = new FakeDiscord();
    await d.listen();
    const c = client([d.path]);
    try {
      c.start();
      await until("connected", () => c.connection === "connected");
      d.sockets[0].write(encodeFrame(OPCODE.PING, { nonce: "p-1", n: 7 }));
      await until("a PONG", () => d.frames.some((f) => f.op === OPCODE.PONG));
      assert.deepEqual(d.frames.find((f) => f.op === OPCODE.PONG)?.json, { nonce: "p-1", n: 7 });
    } finally {
      await c.stop();
      await d.close();
    }
  });

  it("reconnects after Discord goes away and comes back, and re-sends what it wants shown", async () => {
    const d = new FakeDiscord();
    await d.listen();
    const c = client([d.path]);
    try {
      c.start();
      await until("connected", () => c.connection === "connected");
      c.setActivity(A);
      await until("first frame", () => d.sets().length === 1);
      await d.close();
      await until("searching", () => c.connection === "searching");
      await sleep(60);
      await d.listen();
      await until("connected again", () => c.connection === "connected", 3_000);
      await until("the activity again", () => d.sets().length === 2);
      assert.deepEqual((d.sets()[1].json.args as Record<string, unknown>).activity, A);
      assert.equal(d.frames.filter((f) => f.op === OPCODE.HANDSHAKE).length, 2);
    } finally {
      await c.stop();
      await d.close();
    }
  });

  it("stop() clears the activity, sends CLOSE and ends the socket", async () => {
    const d = new FakeDiscord();
    await d.listen();
    const c = client([d.path]);
    try {
      c.start();
      await until("connected", () => c.connection === "connected");
      c.setActivity(A);
      await until("first frame", () => d.sets().length === 1);
      const t = Date.now();
      await c.stop();
      assert.ok(Date.now() - t < 400);
      await until("the socket ended", () => d.ended === 1);
      const tail = d.frames.slice(-2);
      assert.equal(tail[0].op, OPCODE.FRAME);
      assert.deepEqual(tail[0].json.args, { pid: 4242 });
      assert.equal("activity" in (tail[0].json.args as object), false);
      assert.deepEqual(tail[1], { op: OPCODE.CLOSE, json: {} });
      assert.equal(c.connection, "off");
    } finally {
      await d.close();
    }
  });

  it("CLOSE 4000 is invalid-id, and nothing retries until the id changes", async () => {
    const d = new FakeDiscord();
    d.refuse = 4000;
    await d.listen();
    const logs: string[] = [];
    const c = client([d.path], logs);
    try {
      c.start();
      await until("invalid-id", () => c.connection === "invalid-id");
      await sleep(5 * 100);
      assert.equal(d.connections, 1, "no reconnect");
      assert.equal(logs.length, 1);
      d.refuse = null;
      c.setClientId("876543210987654321");
      await until("connected with the new id", () => c.connection === "connected");
      assert.deepEqual(d.frames.filter((f) => f.op === OPCODE.HANDSHAKE).at(-1)?.json, { v: 1, client_id: "876543210987654321" });
    } finally {
      await c.stop();
      await d.close();
    }
  });

  it("no Discord at all: searching, one log line, no throw, a clean start and stop", async () => {
    const logs: string[] = [];
    const c = client([path.join(dir, "nobody", "discord-ipc-0"), path.join(dir, "nobody", "discord-ipc-1")], logs);
    c.start();
    assert.equal(c.connection, "searching");
    c.setActivity(A);
    await sleep(250);
    assert.equal(c.connection, "searching");
    assert.equal(logs.length, 1, "silence after the first line");
    await c.stop();
    assert.equal(c.connection, "off");
  });
});