fix(teams): the two defects the phase 9 rig walk found
Walked against real MariaDB, the real app, and a fake standing in for Discord
that mounts the bot's real internal routes — everything up to the Discord API
call was production code. 47 assertions, and it found two things every unit
test in the phase had passed over.
1. **Every query failed: two result columns named `team_id`.** `desiredTeams`
and `holdersWithoutClaim` both select `t.id AS team_id`, and the shared
column list added `i.team_id` beside it. The `mariadb` driver refuses a
result set with a repeated field name outright, so the pass died at its
first query with "Error in results, duplicate field name `team_id`" — on the
one code path every unit test stubs.
It was also the wrong column: `desiredTeams` LEFT JOINs, so `i.team_id` is
NULL for exactly the Teams that have no channel yet, which is the create
case. The two queries that do not join `teams` now ask for it by name.
The regression test checks the INTERPOLATED sql captured from a fake
`query`, not the source text — in the source the shared list is still a
`${COLUMNS}` placeholder, and a first attempt that read the file passed
happily with the bug reintroduced.
2. **"Sync now" said "Nothing was done" while it was doing it.** Saving the
settings with voice switched on asks for a pass. An operator who then
presses Sync now — the obvious next thing — hit `running` and got back
`ran: false, reason: "a pass is already running"`, which the panel renders
as nothing having happened, while the pass they triggered was busy creating
their channels. A pass in flight is now JOINED and its real outcome
returned, the same choice `teamSync.reconcileNow` makes for the same reason.
Tests: 1162 server (+2), 53 bot, 284 client.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -264,6 +264,61 @@ test('a category ref that is not a channel id is never stored', async () => {
|
||||
await assert.rejects(() => settings.setCategoryRef('../../etc/passwd'))
|
||||
})
|
||||
|
||||
// ── The SQL itself ─────────────────────────────────────────────────────────
|
||||
|
||||
test('no query selects the same result column twice', async () => {
|
||||
// A defect the live rig found and no stubbed test could: `desiredTeams` and
|
||||
// `holdersWithoutClaim` both select `t.id AS team_id`, and the shared column
|
||||
// list used to add `i.team_id` beside it. The `mariadb` driver refuses a result
|
||||
// set with a repeated field name outright — "Error in results, duplicate field
|
||||
// name `team_id`" — so every pass failed at its first query, on a code path
|
||||
// every other test in this file stubs.
|
||||
//
|
||||
// The check runs against the INTERPOLATED sql, captured from a fake `query`,
|
||||
// not against the source text: in the source the shared list is still a
|
||||
// `${COLUMNS}` placeholder, so a reader — and a first attempt at this test —
|
||||
// cannot see the duplicate at all.
|
||||
const db = require('../src/utils/db')
|
||||
const realQuery = db.query
|
||||
const seenSql = []
|
||||
db.query = async (sql) => { seenSql.push(sql); return [] }
|
||||
|
||||
// Re-require: the module destructures `query` at load time, so patching after
|
||||
// it is already in the cache would leave it holding the real one.
|
||||
delete require.cache[require.resolve('../src/model/teams/teamVoice.db.js')]
|
||||
/* eslint-disable-next-line global-require */
|
||||
const freshDb = require('../src/model/teams/teamVoice.db.js')
|
||||
|
||||
try {
|
||||
await freshDb.desiredTeams({ platform: 'discord', resource: 'voice', minMembers: 5 })
|
||||
await freshDb.holdersWithoutClaim({ platform: 'discord', resource: 'voice', minMembers: 5 })
|
||||
await freshDb.listForPlatform('discord', 'voice')
|
||||
await freshDb.getForTeam(1, 'discord', 'voice')
|
||||
await freshDb.discordSubjectsFor(1)
|
||||
} finally {
|
||||
db.query = realQuery
|
||||
delete require.cache[require.resolve('../src/model/teams/teamVoice.db.js')]
|
||||
}
|
||||
|
||||
assert.equal(seenSql.length, 5, 'every query in the file should have been captured')
|
||||
|
||||
for (const sql of seenSql) {
|
||||
const selectList = sql.slice(sql.search(/SELECT/i) + 6, sql.search(/\sFROM\s/i))
|
||||
const names = selectList
|
||||
.split(',')
|
||||
.map((piece) => piece.trim())
|
||||
.filter(Boolean)
|
||||
.map((piece) => {
|
||||
const aliased = piece.match(/\sAS\s+(\w+)$/i)
|
||||
if (aliased) return aliased[1].toLowerCase()
|
||||
return piece.replace(/^DISTINCT\s+/i, '').replace(/^\w+\./, '').toLowerCase()
|
||||
})
|
||||
const seen = new Set()
|
||||
const duplicated = names.filter((name) => (seen.has(name) ? true : (seen.add(name), false)))
|
||||
assert.deepEqual(duplicated, [], `duplicate result column "${duplicated[0]}" in: ${selectList.trim()}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('a garbage category ref already in the database reads as unset', async () => {
|
||||
store.set('teams_voice_category_ref', 'nonsense')
|
||||
assert.equal(await settings.categoryRef(), null)
|
||||
|
||||
@@ -307,6 +307,29 @@ test('a pass that throws is reported, not raised — it hangs off a background t
|
||||
assert.equal(sync.lastPass().ran, false)
|
||||
})
|
||||
|
||||
test('a pass already in flight is JOINED, not refused', async () => {
|
||||
// Found on the live rig. Saving the settings with voice switched on asks for a
|
||||
// pass; an operator who then presses "Sync now" — the obvious next thing to do —
|
||||
// got `ran: false, reason: "a pass is already running"`, and the panel told them
|
||||
// **"Nothing was done"** while the pass they had just triggered was creating
|
||||
// their channels. `teamSync.reconcileNow` joins for the same reason: the caller
|
||||
// wants "Discord now matches", and a pass that started a moment ago delivers it.
|
||||
let release
|
||||
const gate = new Promise((resolve) => { release = resolve })
|
||||
let passes = 0
|
||||
patch(voice, 'plan', async () => { passes += 1; await gate; return plan })
|
||||
|
||||
const first = sync.passNow('first')
|
||||
const second = sync.passNow('second')
|
||||
release()
|
||||
const [a, b] = await Promise.all([first, second])
|
||||
|
||||
assert.equal(passes, 1, 'the work was done once')
|
||||
assert.equal(a.ran, true)
|
||||
assert.equal(b.ran, true, 'the second caller got the real outcome, not a refusal')
|
||||
assert.deepEqual(a, b)
|
||||
})
|
||||
|
||||
test('a pass records what it concluded, for the panel', async () => {
|
||||
plan.provision = [item()]
|
||||
await sync.passNow('test')
|
||||
|
||||
Reference in New Issue
Block a user