d3ro-voice/scripts/ci/lib/update-policy-schema.test.mjs
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

55 lines
2.7 KiB
JavaScript

// node --test scripts/ci/lib/update-policy-schema.test.mjs
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { fileURLToPath, URL } from "node:url";
import { assertValidUpdatePolicy, validateUpdatePolicyDocument } from "./update-policy-schema.mjs";
const committedRaw = readFileSync(fileURLToPath(new URL("../../../release/update-policy.json", import.meta.url)), "utf8");
const committed = JSON.parse(committedRaw);
test("the committed release/update-policy.json passes the strict schema", () => {
assert.deepEqual(validateUpdatePolicyDocument(committed), []);
assert.deepEqual(assertValidUpdatePolicy(committedRaw), committed);
});
for (const [field, value, pattern] of [
["stagingPercentage", 5.5, /stagingPercentage/],
["stagingPercentage", "5", /stagingPercentage/],
["stagingPercentage", 101, /stagingPercentage/],
["killSwitch", "true", /killSwitch/],
["killSwitch", 1, /killSwitch/],
["minimumSupportedVersion", "1.9", /minimumSupportedVersion/],
["forceInstallBelow", "v1.0.0", /forceInstallBelow/],
["fullInstallVersionGap", -1, /fullInstallVersionGap/],
["fullInstallOnMajorChange", "yes", /fullInstallOnMajorChange/],
["defaultChannel", "stable", /defaultChannel/],
["schemaVersion", 2, /schemaVersion/],
]) {
test(`rejects ${field}=${JSON.stringify(value)} instead of publishing a silently permissive policy`, () => {
const errors = validateUpdatePolicyDocument({ ...committed, [field]: value });
assert.equal(errors.length, 1, errors.join("\n"));
assert.match(errors[0], pattern);
assert.throws(() => assertValidUpdatePolicy(JSON.stringify({ ...committed, [field]: value })), /refusing to publish/);
});
}
test("rejects missing safety fields, unknown fields and malformed channels", () => {
const { killSwitch: _omit, ...withoutKillSwitch } = committed;
assert.match(validateUpdatePolicyDocument(withoutKillSwitch).join("\n"), /killSwitch/);
assert.match(validateUpdatePolicyDocument({ ...committed, killswitch: true }).join("\n"), /unknown field "killswitch"/);
assert.match(
validateUpdatePolicyDocument({ ...committed, channels: { ...committed.channels, beta: { allowPrerelease: "true" } } }).join("\n"),
/channels\.beta/,
);
assert.match(
validateUpdatePolicyDocument({ ...committed, channels: { ...committed.channels, rogue: { allowPrerelease: true } } }).join("\n"),
/unknown channel "rogue"/,
);
});
test("rejects non-object and non-JSON input", () => {
assert.deepEqual(validateUpdatePolicyDocument([]), ["update-policy.json must be a JSON object"]);
assert.throws(() => assertValidUpdatePolicy("{"), /not valid JSON/);
assert.throws(() => assertValidUpdatePolicy(Buffer.from("null")), /must be a JSON object/);
});