174 lines
5.0 KiB
JavaScript
174 lines
5.0 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import { registerHooks } from "node:module";
|
|
|
|
const functionUrl = new URL("../functions/abConfigAdmin.ts", import.meta.url);
|
|
const state = {
|
|
configs: [],
|
|
nextId: 1,
|
|
};
|
|
|
|
function clone(value) {
|
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function matches(document, query) {
|
|
return Object.keys(query).every(key => document[key] === query[key]);
|
|
}
|
|
|
|
function collection(name) {
|
|
assert.equal(name, "ab_config");
|
|
return {
|
|
async get() {
|
|
return { data: clone(state.configs) };
|
|
},
|
|
where(query) {
|
|
return {
|
|
async getOne() {
|
|
return { data: clone(state.configs.find(item => matches(item, query)) || null) };
|
|
},
|
|
async update(payload) {
|
|
const matched = state.configs.filter(item => matches(item, query));
|
|
matched.forEach(item => Object.assign(item, clone(payload)));
|
|
return { updated: matched.length };
|
|
},
|
|
async remove() {
|
|
const oldLength = state.configs.length;
|
|
state.configs = state.configs.filter(item => !matches(item, query));
|
|
return { deleted: oldLength - state.configs.length };
|
|
},
|
|
};
|
|
},
|
|
async add(payload) {
|
|
const document = { _id: `config-${state.nextId++}`, ...clone(payload) };
|
|
state.configs.push(document);
|
|
return { id: document._id };
|
|
},
|
|
};
|
|
}
|
|
|
|
globalThis.__abConfigAdminCloudMock = {
|
|
database() {
|
|
return { collection };
|
|
},
|
|
};
|
|
|
|
registerHooks({
|
|
resolve(specifier, context, nextResolve) {
|
|
if (specifier === "@lafjs/cloud") {
|
|
return {
|
|
url: "data:text/javascript,export default globalThis.__abConfigAdminCloudMock",
|
|
shortCircuit: true,
|
|
};
|
|
}
|
|
return nextResolve(specifier, context);
|
|
},
|
|
});
|
|
|
|
const { default: abConfigAdmin } = await import(functionUrl);
|
|
|
|
function reset() {
|
|
state.configs = [];
|
|
state.nextId = 1;
|
|
}
|
|
|
|
function config(extra = {}) {
|
|
return {
|
|
experimentId: "exp_layer_1_001",
|
|
layer: "1",
|
|
name: "Layer 1",
|
|
enabled: true,
|
|
resetAssignments: false,
|
|
variants: [{ group: "A", ratio: 50 }, { group: "B", ratio: 50 }],
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
async function save(items) {
|
|
return await abConfigAdmin({
|
|
body: {
|
|
action: "save",
|
|
adminToken: "abtest-admin-token",
|
|
config: items,
|
|
},
|
|
});
|
|
}
|
|
|
|
test("resetVersion changes only on an enabled false-to-true transition", async () => {
|
|
reset();
|
|
|
|
await save([config()]);
|
|
assert.equal(state.configs[0].resetVersion, 0);
|
|
|
|
await save([config({ resetAssignments: true })]);
|
|
const firstVersion = state.configs[0].resetVersion;
|
|
assert.ok(firstVersion > 0);
|
|
|
|
await save([config({
|
|
experimentId: "exp_layer_1_002",
|
|
resetAssignments: true,
|
|
variants: [{ group: "A", ratio: 80 }, { group: "B", ratio: 20 }],
|
|
})]);
|
|
assert.equal(state.configs.length, 1);
|
|
assert.equal(state.configs[0].experimentId, "exp_layer_1_002");
|
|
assert.equal(state.configs[0].resetVersion, firstVersion);
|
|
|
|
await save([config({ experimentId: "exp_layer_1_002", resetAssignments: false })]);
|
|
assert.equal(state.configs[0].resetVersion, firstVersion);
|
|
|
|
await save([config({ experimentId: "exp_layer_1_002", resetAssignments: true })]);
|
|
assert.ok(state.configs[0].resetVersion > firstVersion);
|
|
});
|
|
|
|
test("a disabled layer does not create a reset version", async () => {
|
|
reset();
|
|
|
|
await save([config({ enabled: false })]);
|
|
await save([config({ enabled: false, resetAssignments: true })]);
|
|
assert.equal(state.configs[0].resetVersion, 0);
|
|
|
|
await save([config({ enabled: true, resetAssignments: true })]);
|
|
assert.equal(state.configs[0].resetVersion, 0);
|
|
|
|
await save([config({ enabled: true, resetAssignments: false })]);
|
|
await save([config({ enabled: true, resetAssignments: true })]);
|
|
assert.ok(state.configs[0].resetVersion > 0);
|
|
|
|
await save([config({ enabled: false, resetAssignments: true })]);
|
|
assert.equal(state.configs[0].resetVersion, 0);
|
|
|
|
await save([config({ enabled: true, resetAssignments: true })]);
|
|
assert.equal(state.configs[0].resetVersion, 0);
|
|
});
|
|
|
|
test("an existing enabled reset without a version is initialized on publish", async () => {
|
|
reset();
|
|
state.configs.push({ _id: "legacy", ...config({ resetAssignments: true }) });
|
|
|
|
const result = await save([config({ resetAssignments: true })]);
|
|
|
|
assert.equal(result.code, 1);
|
|
assert.ok(state.configs[0].resetVersion > 0);
|
|
});
|
|
|
|
test("Default is a case-insensitive reserved group name", async () => {
|
|
reset();
|
|
|
|
const result = await save([config({
|
|
variants: [{ group: "DEFAULT", ratio: 50 }, { group: "B", ratio: 50 }],
|
|
})]);
|
|
|
|
assert.equal(result.code, 0);
|
|
assert.match(result.msg, /Default.*reserved/i);
|
|
assert.equal(state.configs.length, 0);
|
|
});
|
|
|
|
test("the admin page explains edge-triggered reset behavior", async () => {
|
|
const ui = await readFile(new URL("../../xxwz.html", import.meta.url), "utf8");
|
|
|
|
assert.match(ui, /关闭→开启并发布/);
|
|
assert.match(ui, /关闭后未登录用户不补做/);
|
|
assert.match(ui, /Default.*系统保留/);
|
|
});
|