Beta-tester safety: preview caps on the level override, and close a race in stop()
Two things needed before testers get level overrides. 1. The override decided what a position could SPEND as well as what it could SEE. A tester at Scintilla overridden to Corona would have had 150,000 real network impressions instead of 2,500, and a 200,000-impression grant pool to hand to real members — real inventory, spent for real. Ad allowances and split-test launches are now metered on trueLevel, and grant pools are capped to small preview amounts (5,000 impressions / 25 copy / 2 pages / 5 ad batches) when a position is overridden: enough to exercise the whole flow, trivial to lose. 2. suite-traffic.stop() read the ledger, made TWO network round trips to the ad network, then wrote back the object it had read seconds earlier — silently discarding any campaign another member launched in that window, with the impressions already spent on the network. It now re-reads after the awaits and mutates the fresh copy. Worth recording that this was the ONLY such race: Node's single thread makes a synchronous read-modify-write atomic, so the meter, grants and split ledgers were never at risk. The danger was only ever the await gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+14
-6
@@ -47,11 +47,19 @@ function tools() {
|
||||
});
|
||||
}
|
||||
|
||||
function poolFor(tool, level) {
|
||||
// A tester on a level override can SEE every tier, but must not be able to
|
||||
// hand out a real Corona pool — 200,000 network impressions given to real
|
||||
// members is real inventory, spent for real. Preview pools are big enough to
|
||||
// exercise the whole flow and small enough that losing them costs nothing.
|
||||
const PREVIEW_POOL = { traffic: 5000, copy: 25, page: 2, textad: 5 };
|
||||
|
||||
function poolFor(tool, level, preview) {
|
||||
const t = POOLS[tool];
|
||||
if (!t) return 0;
|
||||
const lv = Math.max(1, Math.min(8, Number(level) || 1));
|
||||
return t.pool[lv - 1] || 0;
|
||||
const base = t.pool[lv - 1] || 0;
|
||||
if (preview) return Math.min(base, PREVIEW_POOL[tool] || 0);
|
||||
return base;
|
||||
}
|
||||
|
||||
function rows(mk) {
|
||||
@@ -86,10 +94,10 @@ function receivedDetail(memberId) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function status(memberId, level) {
|
||||
function status(memberId, level, preview) {
|
||||
const t = {};
|
||||
Object.keys(POOLS).forEach(function (k) {
|
||||
const pool = poolFor(k, level);
|
||||
const pool = poolFor(k, level, preview);
|
||||
const given = givenBy(memberId, k);
|
||||
t[k] = {
|
||||
label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max,
|
||||
@@ -97,7 +105,7 @@ function status(memberId, level) {
|
||||
};
|
||||
});
|
||||
return {
|
||||
level: Number(level) || 0, canGrant: Number(level) >= MIN_LEVEL,
|
||||
level: Number(level) || 0, canGrant: Number(level) >= MIN_LEVEL, preview: !!preview,
|
||||
tools: t, resets: monthKey(),
|
||||
history: rows().filter(function (r) { return Number(r.by) === Number(memberId); }).slice(-40).reverse(),
|
||||
received: receivedDetail(memberId)
|
||||
@@ -119,7 +127,7 @@ function grant(opts) {
|
||||
if (n <= 0) throw new Error('Choose how much to give.');
|
||||
if (n > POOLS[tool].max) throw new Error('The most you can give one person at a time is ' + POOLS[tool].max.toLocaleString() + '.');
|
||||
|
||||
const pool = poolFor(tool, level);
|
||||
const pool = poolFor(tool, level, opts.preview);
|
||||
const given = givenBy(by, tool);
|
||||
if (given + n > pool) {
|
||||
throw new Error('That is more than you have left to give this month — ' +
|
||||
|
||||
Reference in New Issue
Block a user