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:
@@ -153,8 +153,9 @@ the week. A run by hand always goes ahead.
|
|||||||
plugin against them.
|
plugin against them.
|
||||||
- **Live, once per build** (`tools/drill/drill.js live`): the `rust-staging` server (Oxide, a 3500 map)
|
- **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
|
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
|
`tools/RunicNpcTest.cs`, and runs `rnt.run all`. It starts the server from fresh Kits and RunicNPC data
|
||||||
(their files are deleted first, so the example kits are written afresh) and gives up on the harness after
|
(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
|
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.
|
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`.
|
- **Report:** anything that fails or differs opens one issue for that build, labelled `staging-drill`.
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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.
|
// 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 = [
|
const FRESH_DATA = [
|
||||||
'/oxide/data/Kits/kits_data.json',
|
|
||||||
'/oxide/data/RunicNPC/profiles.json',
|
'/oxide/data/RunicNPC/profiles.json',
|
||||||
'/oxide/data/RunicNPC/placements.json',
|
'/oxide/data/RunicNPC/placements.json',
|
||||||
'/oxide/data/RunicNPC/routes.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, '/oxide/data/RunicNpcTest.json')
|
||||||
await p.rm(rig, '/staging-drill.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
|
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}`)
|
log(`starting ${RIG} on ${branch}`)
|
||||||
const started = Date.now()
|
const started = Date.now()
|
||||||
@@ -309,7 +312,7 @@ async function live(a) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The harness needs the hidden Kits kit rnhrevolver (tools/RunicNpcTest.cs).
|
// 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'))
|
await p.write(rig, '/oxide/plugins/RunicNpcTest.cs', fs.readFileSync(path.join(ROOT, 'tools', 'RunicNpcTest.cs'), 'utf8'))
|
||||||
let accepted = false
|
let accepted = false
|
||||||
@@ -387,8 +390,18 @@ async function waitStatus(p, id, timeoutMs) {
|
|||||||
return last
|
return last
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureKit(p, id) {
|
/** Kits' data file holding only the harness's hidden kit, which the drill lays down before the start. */
|
||||||
const file = '/oxide/data/Kits/kits_data.json'
|
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
|
let data = null
|
||||||
for (let i = 0; i < 12 && !data; i++) {
|
for (let i = 0; i < 12 && !data; i++) {
|
||||||
data = readJsonText(await p.readOr(id, file, null))
|
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}`)
|
if (!data) throw new Error(`Kits wrote no ${file}`)
|
||||||
data._kits = data._kits || {}
|
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 })
|
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',
|
Name: 'rnhrevolver',
|
||||||
Description: 'RunicNPC test harness: revolver + burlap (staging drill)',
|
Description: 'RunicNPC test harness: revolver + burlap (staging drill)',
|
||||||
RequiredPermission: '',
|
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)],
|
WearItems: [item('burlap.shirt', 1, 0), item('burlap.trousers', 1, 1), item('hat.boonie', 1, 2)],
|
||||||
BeltItems: [item('pistol.revolver', 1, 0, 100)],
|
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) {
|
function readJsonText(t) {
|
||||||
@@ -621,7 +642,7 @@ async function fileIssue(title, body) {
|
|||||||
// ---------------------------------------------------------------- main
|
// ---------------------------------------------------------------- main
|
||||||
|
|
||||||
// The pure parts, for drill.test.js; run as a script, it is the command line.
|
// 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) {
|
if (require.main === module) {
|
||||||
const a = args()
|
const a = args()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const assert = require('node:assert')
|
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 MIN = 60000
|
||||||
const T0 = 1000000
|
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', () => {
|
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'))
|
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'])
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user