Files
nntrivi2001 4aae61297f feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
  Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
  with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
  Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
  its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
  status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
  reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
  ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files

Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
2026-10-06 17:12:28 +07:00

156 lines
6.1 KiB
JavaScript

/**
* @file Tests server/lib/privacy.js — the secret patterns (each shape redacted,
* look-alikes such as `max_tokens: 5` left alone), secret-named JSON keys, the
* payload entry point being a byte-identical no-op while off or when nothing
* matched, `trim_event_data` redacting before it trims, and the ignored-folder
* list's path-boundary match and validation.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, beforeEach, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const privacy = require("../lib/privacy");
const { trimEventData } = require("../lib/event-payload");
const R = privacy.REDACTED;
describe("redactText", () => {
const cases = [
["anthropic key", "key sk-ant-api03-AbCdEfGhIjKlMnOpQrStUv here", `key ${R} here`],
["openai key", "OPENAI sk-proj-AbCdEfGhIjKlMnOpQrStUvWx", `OPENAI ${R}`],
["github token", "ghp_0123456789abcdefghijABCDEFGHIJ", R],
["aws key id", "id AKIAABCDEFGHIJKLMNOP", `id ${R}`],
["slack", "xoxb-1234567890-abcdef", R],
[
"bearer header",
"curl -H 'Authorization: Bearer abc.def-ghi_jkl'",
`curl -H 'Authorization: Bearer ${R}'`,
],
["password assignment", "mysql password=hunter22 -h db", `mysql password=${R} -h db`],
["env token", "export GITHUB_TOKEN=abcd1234efgh", `export GITHUB_TOKEN=${R}`],
["json-ish key", '{"api_key": "abcd1234"}', `{"api_key": ${R}}`],
[
"private key",
"a\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaA\n-----END OPENSSH PRIVATE KEY-----\nz",
`a\n${R}\nz`,
],
];
for (const [name, input, want] of cases) {
it(`redacts ${name}`, () => assert.equal(privacy.redactText(input), want));
}
it("leaves look-alikes alone", () => {
for (const s of [
"max_tokens: 4096",
"token_count=5 input_tokens: 9",
"the author wrote a secretive note",
"sk-short",
"Bearer of bad news",
]) {
assert.equal(privacy.redactText(s), s);
}
});
it("is idempotent", () => {
const once = privacy.redactText("password=hunter22 and ghp_0123456789abcdefghijABCDEFGHIJ");
assert.equal(privacy.redactText(once), once);
});
it("stays linear on a pathological run", () => {
const t = Date.now();
privacy.redactText("a-".repeat(200_000));
assert.ok(Date.now() - t < 2000);
});
});
describe("redactValue", () => {
it("redacts any string under a secret-named key, whatever it looks like", () => {
const out = privacy.redactValue({
tool_input: { env: { DB_PASSWORD: "x", API_KEY: "y", max_tokens: "5", authorization: "z" } },
author: "me",
});
assert.deepEqual(out, {
tool_input: { env: { DB_PASSWORD: R, API_KEY: R, max_tokens: "5", authorization: R } },
author: "me",
});
});
});
describe("redactEventJson / trim_event_data", () => {
let saved;
beforeEach(() => {
saved = process.env[privacy.REDACT_KEY];
});
afterEach(() => {
if (saved === undefined) delete process.env[privacy.REDACT_KEY];
else process.env[privacy.REDACT_KEY] = saved;
});
const payload = JSON.stringify({
session_id: "s",
tool_input: { command: "curl -H 'Authorization: Bearer abcdefgh12345678' x" },
});
it("is a byte-identical no-op while off", () => {
delete process.env[privacy.REDACT_KEY];
assert.equal(privacy.redactEventJson(payload), payload);
assert.equal(trimEventData(payload), payload);
});
it("redacts and counts when on, and leaves a clean payload byte-identical", () => {
process.env[privacy.REDACT_KEY] = "1";
const out = JSON.parse(trimEventData(payload));
assert.equal(out.tool_input.command, `curl -H 'Authorization: Bearer ${R}' x`);
assert.equal(out._redacted, 1);
const clean = JSON.stringify({ session_id: "s", tool_input: { command: "ls" } });
assert.equal(trimEventData(clean), clean);
});
it("redacts a private key before the trim could cut it in two", () => {
process.env[privacy.REDACT_KEY] = "1";
const key = `-----BEGIN RSA PRIVATE KEY-----\n${"A".repeat(20_000)}\n-----END RSA PRIVATE KEY-----`;
const json = JSON.stringify({ tool_name: "Read", tool_response: { file: { text: key } } });
const out = trimEventData(json);
assert.ok(!out.includes("AAAAAAAAAA"));
assert.ok(out.includes(R));
});
it("leaves summaries alone while off", () => {
delete process.env[privacy.REDACT_KEY];
assert.equal(privacy.redactSummary("password=hunter22"), "password=hunter22");
process.env[privacy.REDACT_KEY] = "1";
assert.equal(privacy.redactSummary("password=hunter22"), `password=${R}`);
});
});
describe("ignored folders", () => {
const env = (list) => ({ [privacy.IGNORE_KEY]: list.join(path.delimiter) });
it("matches the folder and what lies under it, never a sibling sharing the prefix", () => {
const e = env(["/work/secret", "/tmp/x/"]);
assert.equal(privacy.isIgnoredCwd("/work/secret", e), true);
assert.equal(privacy.isIgnoredCwd("/work/secret/app", e), true);
assert.equal(privacy.isIgnoredCwd("/work/secret-two", e), false);
assert.equal(privacy.isIgnoredCwd("/tmp/x/y", e), true);
assert.equal(privacy.isIgnoredCwd("/work", e), false);
assert.equal(privacy.isIgnoredCwd(undefined, e), false);
assert.equal(privacy.isIgnoredCwd("/anything", {}), false);
});
it("never treats a root or a relative entry as a folder to ignore", () => {
assert.deepEqual(privacy.ignoredCwds(env(["/", "rel/dir", "/ok"])).length, 2);
assert.equal(privacy.isIgnoredCwd("/home/u", env(["/"])), false);
});
it("validates the Settings list", () => {
assert.deepEqual(privacy.validateIgnoreList(["/a/b/", " /a/b ", "", "/c"]), ["/a/b", "/c"]);
assert.throws(() => privacy.validateIgnoreList(["relative"]), { code: "EBADPATH" });
assert.throws(() => privacy.validateIgnoreList(["/"]), { code: "EBADPATH" });
assert.throws(() => privacy.validateIgnoreList([`/a${path.delimiter}/b`]), {
code: "EBADPATH",
});
assert.throws(() => privacy.validateIgnoreList(["/a\nb"]), { code: "EBADPATH" });
assert.throws(() => privacy.validateIgnoreList("/a"), { code: "EBADPATH" });
});
});