argus/test/plans.test.mjs
Local Dev a45320db7d Initial commit — Silent Mode baseline (2026-07-29)
Snapshot of the decentralized-web stack at the point of the resolver+Theseus
rebuild deploy. Includes:

- Argus (BNS engine + resolver daemon + Sia gateway)
- AriadneResolver (Windows Inno installer bundle + Android APK sources +
  Firefox extension)
- TheseusNavigator (Electron browser)
- site/ (silentmode.st content, deployed to Sia at bns/silentmode/)
- design docs, roadmap, protocol spec

Secrets excluded via .gitignore: Argus/sia-s3.json, Argus/wallets.json,
Argus/ca/*.key,*.crt. Build outputs, node_modules, and bundled runtimes
also excluded.

Shipped hashes on dl.silentmode.st at this commit:
  AriadneResolver-Setup-0.1.0.exe    5bcb216eef31ea28ed767e4134ab74bd5ac69dfbd365fd249e9e6938e55c986a
  TheseusNavigator-Setup-0.0.1.exe   7c735e88bad2da3347145adba3016c8f626a18b8422289c8c6ba471972e2952b
  TheseusNavigator-0.0.1-portable.exe 008fd84445babeabb401b2bca40ea9466b24b0e6d6c85104da7640c5c5c84521
  ariadne-v0.2.apk                   635c8f04d44ef855a8390b9eeddb8cd2d50622e81cc4e5004e8daffc1bb0425c
2026-07-29 13:54:34 +02:00

125 lines
5.8 KiB
JavaScript

// The plan catalogue and bundle pricing.
//
// Most of these assert honesty properties rather than arithmetic: that a free
// option is never priced, that only a plan where WE hold the bytes is flagged as
// creating an obligation, and that every recurring plan tells the buyer what
// happens to their NAME when hosting lapses. Those are the claims that get
// quietly wrong when copy is edited months later, and they are exactly the ones
// no one notices until a customer is upset.
import test from "node:test";
import assert from "node:assert/strict";
import {
HOSTING_PLANS, VPS_REFERRALS, PLAN_KIND,
planById, paidPlans, freePlans, quoteBundle,
} from "../src/lib/plans.js";
import { quotePrice } from "../src/lib/orders.js";
import { REGISTRAR } from "../src/registrar-config.js";
const NAME_PRICE = quotePrice({ serviceFeeSats: REGISTRAR.serviceFee.sats });
test("only one plan is actually hosting-as-a-service", () => {
const managed = HOSTING_PLANS.filter((p) => p.kind === PLAN_KIND.managed);
assert.equal(managed.length, 1, "selling more 'plans' than we run is how a storefront starts lying");
assert.equal(managed[0].id, "sia-managed-1y");
assert.equal(managed[0].weStore, true);
});
test("every free plan is genuinely free and stores nothing of ours", () => {
for (const p of freePlans()) {
assert.equal(p.priceSats, 0n, `${p.id} is listed free but priced`);
assert.equal(p.weStore, false, `${p.id} is listed free but we hold the bytes`);
assert.equal(p.recurring, false, `${p.id} is free but recurring — that cannot both be true`);
}
assert.ok(freePlans().length >= 3, "most options cost the buyer nothing; the page should say so");
});
test("paid plans are exactly the ones where we store bytes", () => {
const paid = paidPlans().map((p) => p.id).sort();
const stores = HOSTING_PLANS.filter((p) => p.weStore).map((p) => p.id).sort();
assert.deepEqual(paid, stores, "we should charge for storage and nothing else");
});
test("every recurring plan explains what happens to the NAME on lapse", () => {
for (const p of HOSTING_PLANS.filter((x) => x.recurring)) {
assert.ok(p.onLapse, `${p.id} is recurring but says nothing about lapsing`);
assert.match(p.onLapse, /name is unaffected/i, "the flagship claim must survive a lapsed subscription");
assert.match(p.onLapse, /repoint/i, "the buyer needs to know the remedy is free and in their hands");
}
});
test("non-recurring plans do not carry lapse language", () => {
for (const p of HOSTING_PLANS.filter((x) => !x.recurring)) {
assert.equal(p.onLapse, null, `${p.id} cannot lapse, so it must not imply it can`);
}
});
test("referrals are not plans and carry no price of ours", () => {
const ids = new Set(HOSTING_PLANS.map((p) => p.id));
for (const r of VPS_REFERRALS) {
assert.ok(!ids.has(r.id), `${r.id} appears in both lists — a referral is not something we sell`);
assert.ok(r.url.startsWith("https://"), "referral needs a real destination");
assert.equal(r.priceSats, undefined, "we do not price other people's products");
}
});
// ---------- bundle pricing ----------
test("a name on its own is just the name price", () => {
const q = quoteBundle({ namePriceSats: NAME_PRICE });
assert.equal(q.totalSats, NAME_PRICE);
assert.equal(q.lines.length, 1);
assert.equal(q.plan, null);
assert.equal(q.createsHostingObligation, false);
assert.equal(q.recurring, false);
});
test("name + managed hosting is one payment with itemised lines", () => {
const plan = planById("sia-managed-1y");
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
assert.equal(q.totalSats, NAME_PRICE + plan.priceSats);
assert.equal(q.lines.length, 2, "one payment, two lines — not two orders to reconcile");
assert.equal(q.lines[0].id, "name");
assert.match(q.lines[1].label, /Managed Sia hosting/);
assert.match(q.lines[1].label, /1y/, "the period belongs in the label, where the buyer reads it");
});
test("choosing a free plan adds no charge and no obligation", () => {
for (const p of freePlans()) {
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: p.id });
assert.equal(q.totalSats, NAME_PRICE, `${p.id} must not add cost`);
assert.equal(q.lines.length, 1, `${p.id} must not add a billing line`);
assert.equal(q.createsHostingObligation, false, `${p.id} must not make us a host`);
}
});
test("only the managed plan flags a hosting obligation", () => {
// This flag is what the UI keys the "we will be storing your files" disclosure
// off. Getting it wrong either hides a real liability or scares people off a
// free option that never touches our infrastructure.
const managed = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
assert.equal(managed.createsHostingObligation, true);
assert.equal(managed.recurring, true);
const onchain = quoteBundle({ namePriceSats: NAME_PRICE, planId: "onchain" });
assert.equal(onchain.createsHostingObligation, false);
});
test("an unknown plan is refused rather than silently ignored", () => {
// Silently dropping it would sell a name, take the money, and quietly not
// provide the hosting the buyer picked.
assert.throws(() => quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-10y" }), /unknown hosting plan/);
});
test("bundle totals stay bigint throughout", () => {
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
assert.equal(typeof q.totalSats, "bigint");
for (const l of q.lines) assert.equal(typeof l.sats, "bigint", `${l.id} must not drift to Number`);
});
test("every plan maps to a real BNS record type", () => {
const valid = new Set(["h", "u", "ip", "s3", "tls"]);
for (const p of HOSTING_PLANS) {
assert.ok(valid.has(p.record), `${p.id} points at "${p.record}", which the resolver does not implement`);
}
});