bot / test / brains.test.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// The advertised-brain versions (new-in v2.1.0): registry.brains() answers
// "which brain is each learned game playing RIGHT NOW", read through the very
// CheckpointStore the provider plays — so a trainer promotion re-advertises
// itself with no restart. Runs against the committed dist/; checkpoints are
// written with train-kit's own writer, so the files are real-shaped.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

const bot = await import("../stack-a/dist/main.js");
// train-kit is stack A's (its @tensorflow/tfjs-node peer lives there) and
// this file is outside both npm roots, so the INSTALLED dist is named by path
// — through the package's own exports target, not a rebuilt copy. That the
// comparison crosses copies is the point (the bot compiles train-kit from
// source; this is the sha-pinned package).
const { saveModel, modelVersion } = await import(
  new URL("../stack-a/node_modules/ardegazu-train-kit/dist/index.js", import.meta.url).href
);

const tabular = (episodes, q) => ({ v: 1, game: "seance", episodes, updatedAt: 1, q });
const mlp = (episodes, w) => ({
  v: 1,
  game: "neon-grid",
  episodes,
  updatedAt: 1,
  net: { sizes: [2, 1], layers: [{ w, b: [0.5] }] },
});

// the promoted models this file plays with
const SEANCE_A = tabular(59500, { "0|1": [0.25, -1, 0, 3] });
const SEANCE_B = tabular(60100, { "0|1": [0.25, -1, 0, 4] }); // moved weights
const NEON_A = mlp(13208, [0.1, -0.2]);

/** Atomic replace, exactly as the trainer promotes: temp file + rename over. */
function promote(dir, game, model) {
  const live = join(dir, `${game}.json`);
  saveModel(`${live}.next`, model);
  renameSync(`${live}.next`, live);
}

// ---- the real providers, through their real stores --------------------------

test("brains(): stock for every provider before any model is loaded", () => {
  // importing dist/main.js registers the three learned-game providers; their
  // stores do not exist yet, so nobody may claim a brain
  assert.deepEqual(bot.brains(), { seance: "stock", "neon-grid": "stock", "valley-blocks": "stock" });
});

test("brains(): promoted tabular + promoted MLP + a missing model", () => {
  const dir = mkdtempSync(join(tmpdir(), "bot-brains-"));
  try {
    saveModel(join(dir, "seance.json"), SEANCE_A);
    saveModel(join(dir, "neon-grid.json"), NEON_A);
    // valley-blocks.json deliberately absent — that provider plays stock
    const logs = [];
    bot.initPolicies({ modelDir: dir, reloadMs: 0 }, (l) => logs.push(l));

    assert.deepEqual(bot.brains(), {
      seance: modelVersion(SEANCE_A),
      "neon-grid": modelVersion(NEON_A),
      "valley-blocks": "stock",
    });
    // the version really is "<episodes>@<rev8>" off the model being played
    assert.match(bot.brains().seance, /^59500@[0-9a-f]{8}$/);
    assert.match(bot.brains()["neon-grid"], /^13208@[0-9a-f]{8}$/);
    // both models loaded, and no promotion line on a first load
    assert.deepEqual(logs, [
      "model seance loaded (59500 episodes)",
      "model neon-grid loaded (13208 episodes)",
    ]);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

test("brains(): a checkpoint for the wrong game is not a brain", () => {
  const dir = mkdtempSync(join(tmpdir(), "bot-brains-"));
  try {
    // a valid MLP checkpoint, but it is neon-grid's — valley-blocks' guard
    // must refuse it rather than advertise a foreign brain
    saveModel(join(dir, "valley-blocks.json"), NEON_A);
    bot.initPolicies({ modelDir: dir, reloadMs: 0 }, () => {});
    assert.equal(bot.brains()["valley-blocks"], "stock");
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

test("brains() is rebuilt per call and tracks a live promotion", async () => {
  const dir = mkdtempSync(join(tmpdir(), "bot-brains-"));
  try {
    saveModel(join(dir, "seance.json"), SEANCE_A);
    const logs = [];
    bot.initPolicies({ modelDir: dir, reloadMs: 20 }, (l) => logs.push(l));

    const before = bot.brains();
    assert.equal(before.seance, modelVersion(SEANCE_A));
    // a fresh object every call — never a shared snapshot a caller could pin
    assert.notEqual(before, bot.brains());
    assert.deepEqual(before, bot.brains());

    promote(dir, "seance", SEANCE_B);
    const deadline = Date.now() + 5000;
    while (bot.brains().seance !== modelVersion(SEANCE_B) && Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 20));
    }
    // the store reload alone moved the advertised value: no restart, no cache
    assert.equal(bot.brains().seance, modelVersion(SEANCE_B));
    assert.notEqual(modelVersion(SEANCE_A), modelVersion(SEANCE_B));
    assert.ok(logs.includes(`brain seance -> ${modelVersion(SEANCE_B)} (promoted)`));
    // and exactly one promotion line — an unchanged reload says nothing
    assert.equal(logs.filter((l) => l.startsWith("brain seance ->")).length, 1);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

test("a metadata-only touch reloads but does not re-advertise", async () => {
  const dir = mkdtempSync(join(tmpdir(), "bot-brains-"));
  try {
    saveModel(join(dir, "seance.json"), SEANCE_A);
    const logs = [];
    bot.initPolicies({ modelDir: dir, reloadMs: 20 }, (l) => logs.push(l));
    const v = bot.brains().seance;

    promote(dir, "seance", { ...SEANCE_A, updatedAt: 999 });
    const deadline = Date.now() + 2000;
    while (logs.length < 2 && Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 20));
    }
    assert.equal(logs.length, 2); // the reload happened…
    assert.equal(logs[1], "model seance loaded (59500 episodes)");
    assert.equal(bot.brains().seance, v); // …and changed nothing advertisable
    assert.equal(logs.filter((l) => l.startsWith("brain seance ->")).length, 0);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

test("a corrupt promoted file leaves the previous brain advertised", async () => {
  const dir = mkdtempSync(join(tmpdir(), "bot-brains-"));
  try {
    saveModel(join(dir, "seance.json"), SEANCE_A);
    bot.initPolicies({ modelDir: dir, reloadMs: 20 }, () => {});
    const v = bot.brains().seance;
    writeFileSync(join(dir, "seance.json"), "{ not json");
    await new Promise((r) => setTimeout(r, 120));
    assert.equal(bot.brains().seance, v);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
});

// ---- the clamp, over the pure core -----------------------------------------

test("brainsOf(): an empty registry advertises nothing", () => {
  assert.equal(bot.brainsOf([]), null);
});

test("brainsOf(): a provider with no version fn advertises nothing", () => {
  assert.equal(bot.brainsOf([{ game: "seance" }]), null);
  assert.deepEqual(bot.brainsOf([{ game: "seance" }, { game: "neon-grid", version: () => "1@ab" }]), {
    "neon-grid": "1@ab",
  });
});

test("brainsOf(): surprising version values are dropped, not shipped", () => {
  const bad = [
    null,
    undefined,
    42, // not a string — never stringified
    {},
    ["1@ab"],
    "", // empty
    "a".repeat(33), // 33 > the 32-char value cap
    "59500@ab12cd34 (promoted)", // spaces and parens are outside the charset
    "13208@ab/cd", // slash
    "héllo", // non-ASCII
  ];
  for (const v of bad) {
    assert.equal(bot.brainsOf([{ game: "seance", version: () => v }]), null, `kept ${String(v)}`);
  }
  // the boundary values that MUST survive
  for (const v of ["stock", "0@00000000", "a".repeat(32), "A.z-9_@1"]) {
    assert.deepEqual(bot.brainsOf([{ game: "seance", version: () => v }]), { seance: v });
  }
});

test("brainsOf(): surprising game ids are dropped", () => {
  for (const g of [null, undefined, 7, "", "Seance", "-lead", "sea nce", "x".repeat(25)]) {
    assert.equal(bot.brainsOf([{ game: g, version: () => "1@ab" }]), null, `kept ${String(g)}`);
  }
  for (const g of ["seance", "neon-grid", "valley-blocks", "0", "x".repeat(24)]) {
    assert.deepEqual(bot.brainsOf([{ game: g, version: () => "1@ab" }]), { [g]: "1@ab" });
  }
});

test("brainsOf(): a throwing provider costs its own entry only", () => {
  const out = bot.brainsOf([
    {
      game: "seance",
      version: () => {
        throw new Error("boom");
      },
    },
    { game: "neon-grid", version: () => "1@ab" },
  ]);
  assert.deepEqual(out, { "neon-grid": "1@ab" });
});

test("brainsOf(): at most 8 entries ride a beacon", () => {
  const ps = [];
  for (let i = 0; i < 12; i++) ps.push({ game: `g${i}`, version: () => `${i}@ab` });
  const out = bot.brainsOf(ps);
  assert.equal(Object.keys(out).length, 8);
  assert.deepEqual(Object.keys(out), ["g0", "g1", "g2", "g3", "g4", "g5", "g6", "g7"]);
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/bot.git