aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit/session-cleanup.test.mjs
blob: 604e691b4db367ce41fdc45ec43d77bac3bff5a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Exercise the shipped cleanup helper and shutdown listener, without opening a bridge or Chrome.
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
import { stripTypeScriptTypes } from "node:module";

const source = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8");
function section(start, end) {
  const from = source.indexOf(start);
  const to = source.indexOf(end, from);
  assert.ok(from >= 0 && to > from, `Missing source section: ${start}`);
  return source.slice(from, to);
}
const helper = stripTypeScriptTypes(section("const cleanupAutomationTargetBestEffort =", "const lockChromeControl ="));
const listener = section('pi.on("session_shutdown",', 'pi.on("before_agent_start",');

function harness(send, sessionKey = "session:test") {
  const timers = new Map();
  const events = [];
  let shutdown;
  const token = Symbol();
  const globalState = { loaded: { token }, auth: { until: "indefinite" } };
  const context = {
    AbortController, Promise,
    setTimeout(fn, ms) { const key = Symbol(); timers.set(key, { fn, ms }); return key; },
    clearTimeout(key) { timers.delete(key); },
    bridge: { send(...args) { events.push("send"); return send(...args); }, stop() { events.push("stop"); } },
    sessionCtx: {}, sessionKeyFor: () => sessionKey ?? undefined,
    clearAuthExpiryTimer() {}, clearCountdownInterval() {},
    globalState, PI_CHROME_GLOBAL_KEY: "loaded", PI_CHROME_AUTH_KEY: "auth", instanceToken: token,
    pi: { on(name, handler) { assert.equal(name, "session_shutdown"); shutdown = handler; } },
  };
  vm.runInNewContext(`${helper}\n${listener}`, context);
  return { shutdown, timers, events, globalState };
}

// Delivery must settle before bridge.stop; reload does not issue cleanup at all.
{
  let complete;
  let request;
  const h = harness((...args) => {
    request = args;
    return new Promise((resolve) => { complete = resolve; });
  });
  const done = h.shutdown({ reason: "quit" });
  assert.deepEqual(h.events, ["send"]);
  assert.equal(request[0], "automation.cleanup");
  assert.equal(request[1].sessionKey, "session:test");
  assert.equal(request[2], 2000);
  assert.equal(request[3].aborted, false);
  complete();
  await done;
  assert.deepEqual(h.events, ["send", "stop"]);
  assert.equal(h.timers.size, 0);
  assert.equal(h.globalState.loaded, undefined);
}
{
  const h = harness(() => { throw new Error("reload must not send"); });
  await h.shutdown({ reason: "reload" });
  assert.deepEqual(h.events, ["stop"]);
  assert.equal(h.timers.size, 0);
}

// Even an owner request that never settles cannot hold up shutdown past its budget.
{
  let signal;
  const h = harness((_action, _params, _timeout, s) => { signal = s; return new Promise(() => {}); });
  const done = h.shutdown({});
  const [timer] = h.timers.values();
  assert.equal(timer.ms, 2000);
  timer.fn();
  await done;
  assert.equal(signal.aborted, true);
  assert.deepEqual(h.events, ["send", "stop"]);
  assert.equal(h.timers.size, 0);
}
for (const reason of ["quit", "new", "resume", "fork"]) {
  const h = harness(() => Promise.resolve({}));
  await h.shutdown({ reason });
  assert.deepEqual(h.events, ["send", "stop"]);
  // A /chrome authorize grant stays with its session: a session switch drops it, quit leaves it
  // for a parent session sharing this process.
  assert.equal("auth" in h.globalState, reason === "quit", `grant after ${reason}`);
}
{
  const h = harness(() => Promise.resolve({}));
  await h.shutdown({ reason: "reload" });
  assert.deepEqual(h.globalState.auth, { until: "indefinite" }, "grant after reload");
}
for (const send of [() => Promise.reject(new Error("offline")), () => { throw new Error("offline"); }]) {
  const h = harness(send);
  await h.shutdown({ reason: "exit" });
  assert.deepEqual(h.events, ["send", "stop"]);
  assert.equal(h.timers.size, 0);
}
{
  const h = harness(() => { throw new Error("unscoped cleanup must not send"); }, null);
  await h.shutdown({ reason: "exit" });
  assert.deepEqual(h.events, ["stop"]);
}
console.log("session-cleanup: shutdown ordering, reload, deadline, failures, missing identity, and grant scope passed");