fix(drill): lay the harness's kit down before boot instead of reloading Kits (#22)

Run 48 deleted Kits' file for a fresh start, and the harness's
rnhrevolver kit, added after boot with `oxide.reload Kits` through the
panel's command endpoint, never reached Kits: no reload or compile job
followed, and every test profile was refused for want of the kit.

The drill now replaces kits_data.json with one holding only that kit.
Kits reads it at boot; RunicNPC's first load adds its example kits
beside it (it skips kits already there) and settles its own Kits
reload (D329). The add-after-boot stays as a fallback, now through the
console's websocket, and is a finding when Kits does not load again.

Refs #20, #22

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-11-01 01:50:19 -06:00
parent 54fb1ece67
commit 4fb935d9dd
3 changed files with 47 additions and 16 deletions

View File

@@ -153,8 +153,9 @@ the week. A run by hand always goes ahead.
plugin against them.
- **Live, once per build** (`tools/drill/drill.js live`): the `rust-staging` server (Oxide, a 3500 map)
installs the same pair at start (`tools/drill/rig-start.sh`), loads RunicNPC, Kits and
`tools/RunicNpcTest.cs`, and runs `rnt.run all`. It starts the server from empty Kits and RunicNPC data
(their files are deleted first, so the example kits are written afresh) and gives up on the harness after
`tools/RunicNpcTest.cs`, and runs `rnt.run all`. It starts the server from fresh Kits and RunicNPC data
(RunicNPC's files are deleted and Kits' file holds only the harness's kit, so the example kits are written
afresh) and gives up on the harness after
5 minutes with no new output (45 minutes at most), putting the tail of the server's log in the issue. When
both 6000-map rigs are running, `rust-carbon` is stopped for it and started again.
- **Report:** anything that fails or differs opens one issue for that build, labelled `staging-drill`.

View File

@@ -37,8 +37,10 @@ const HARNESS_STALL_MS = 5 * 60000 // no new line in its file for this long: it
// Kits' own file, and never again, so a file kept from an older build keeps that build's kits; the drill
// starts from none of it. Files, never directories: a directory the panel's file API creates is not
// writable by the game (docs/runicnpc/PLAN.md §1.5), and the game makes these itself on its first boot.
// Kits' file is not deleted but replaced by one holding only the harness's kit (KITS_FILE, harnessKits):
// Kits reads it at boot and RunicNPC adds its examples beside it, so no reload is needed for it (#22).
const KITS_FILE = '/oxide/data/Kits/kits_data.json'
const FRESH_DATA = [
'/oxide/data/Kits/kits_data.json',
'/oxide/data/RunicNPC/profiles.json',
'/oxide/data/RunicNPC/placements.json',
'/oxide/data/RunicNPC/routes.json',
@@ -277,6 +279,7 @@ async function live(a) {
await p.rm(rig, '/oxide/data/RunicNpcTest.json')
await p.rm(rig, '/staging-drill.json')
for (const f of FRESH_DATA) await p.rm(rig, f) // already gone is fine: rm swallows it
await p.write(rig, KITS_FILE, JSON.stringify(harnessKits(), null, 2))
log(`starting ${RIG} on ${branch}`)
const started = Date.now()
@@ -309,7 +312,7 @@ async function live(a) {
}
// The harness needs the hidden Kits kit rnhrevolver (tools/RunicNpcTest.cs).
await ensureKit(p, rig)
await ensureKit(p, rig, finding)
await p.write(rig, '/oxide/plugins/RunicNpcTest.cs', fs.readFileSync(path.join(ROOT, 'tools', 'RunicNpcTest.cs'), 'utf8'))
let accepted = false
@@ -387,8 +390,18 @@ async function waitStatus(p, id, timeoutMs) {
return last
}
async function ensureKit(p, id) {
const file = '/oxide/data/Kits/kits_data.json'
/** Kits' data file holding only the harness's hidden kit, which the drill lays down before the start. */
function harnessKits() {
return { _kits: { rnhrevolver: harnessKit() } }
}
/**
* The harness's kit is laid down before the start (harnessKits), so it is normally there. If it is not
* (RunicNPC rewrote the file without it), it is added and Kits reloaded through the console's websocket:
* the panel's fire-and-forget command left no reload in run 48 (#22), so the reply is logged.
*/
async function ensureKit(p, id, finding) {
const file = KITS_FILE
let data = null
for (let i = 0; i < 12 && !data; i++) {
data = readJsonText(await p.readOr(id, file, null))
@@ -396,9 +409,21 @@ async function ensureKit(p, id) {
}
if (!data) throw new Error(`Kits wrote no ${file}`)
data._kits = data._kits || {}
if (data._kits.rnhrevolver) return
if (data._kits.rnhrevolver) {
log('the rnhrevolver kit is in Kits\' file, laid down before the start')
return
}
data._kits.rnhrevolver = harnessKit()
await p.write(id, file, JSON.stringify(data, null, 2))
const reply = await p.console(id, 'oxide.reload Kits', 15)
const loaded = reply.some((l) => /Loaded plugin Kits/.test(l))
log(`added the rnhrevolver kit and reloaded Kits: ${loaded ? 'Kits loaded again' : `no "Loaded plugin Kits" in 15 s (${reply.length} console line(s))`}`)
if (!loaded) finding('Kits did not reload after the drill added the harness\'s rnhrevolver kit; the harness\'s profiles may be refused.')
}
function harnessKit() {
const item = (Shortname, Amount, Position, Condition = 0) => ({ Shortname, Skin: 0, Amount, Condition, MaxCondition: Condition, Ammo: 0, Position, Frequency: -1 })
data._kits.rnhrevolver = {
return {
Name: 'rnhrevolver',
Description: 'RunicNPC test harness: revolver + burlap (staging drill)',
RequiredPermission: '',
@@ -413,10 +438,6 @@ async function ensureKit(p, id) {
WearItems: [item('burlap.shirt', 1, 0), item('burlap.trousers', 1, 1), item('hat.boonie', 1, 2)],
BeltItems: [item('pistol.revolver', 1, 0, 100)],
}
await p.write(id, file, JSON.stringify(data, null, 2))
await p.command(id, 'oxide.reload Kits')
await sleep(10000)
log('added the rnhrevolver kit and reloaded Kits')
}
function readJsonText(t) {
@@ -621,7 +642,7 @@ async function fileIssue(title, body) {
// ---------------------------------------------------------------- main
// The pure parts, for drill.test.js; run as a script, it is the command line.
module.exports = { harnessProgress, unfinishedFinding, FRESH_DATA, HARNESS_STALL_MS }
module.exports = { harnessProgress, unfinishedFinding, FRESH_DATA, KITS_FILE, harnessKits, HARNESS_STALL_MS }
if (require.main === module) {
const a = args()

View File

@@ -3,7 +3,7 @@
const test = require('node:test')
const assert = require('node:assert')
const { harnessProgress, unfinishedFinding, FRESH_DATA, HARNESS_STALL_MS } = require('./drill.js')
const { harnessProgress, unfinishedFinding, FRESH_DATA, KITS_FILE, harnessKits, HARNESS_STALL_MS } = require('./drill.js')
const MIN = 60000
const T0 = 1000000
@@ -52,7 +52,16 @@ test('a capped wait keeps the 45-minute message', () => {
})
test('the clean start deletes files only: every path ends in .json and none is a directory', () => {
assert.ok(FRESH_DATA.includes('/oxide/data/Kits/kits_data.json'))
assert.ok(FRESH_DATA.includes('/oxide/data/RunicNPC/state.json'))
for (const f of FRESH_DATA) assert.match(f, /^\/oxide\/data\/(Kits|RunicNPC)\/[a-z_]+\.json$/)
for (const f of FRESH_DATA) assert.match(f, /^\/oxide\/data\/RunicNPC\/[a-z_]+\.json$/)
})
test("Kits' file is replaced, not deleted, by one holding only the harness's kit (#22)", () => {
assert.ok(!FRESH_DATA.includes(KITS_FILE))
assert.equal(KITS_FILE, '/oxide/data/Kits/kits_data.json')
const kits = harnessKits()._kits
assert.deepEqual(Object.keys(kits), ['rnhrevolver'])
assert.equal(kits.rnhrevolver.Name, 'rnhrevolver')
assert.equal(kits.rnhrevolver.IsHidden, true)
assert.deepEqual(kits.rnhrevolver.BeltItems.map((i) => i.Shortname), ['pistol.revolver'])
})