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.*系统保留/); });