Files
MikrotikManager/backend/src/modules/mcp/routing-controller-cas-acceptance.ts
T

74 lines
5.2 KiB
TypeScript

import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import type { PoolClient } from "pg"
import { pool, createPool } from "../../db/index.js"
import { controllerProfileCurrent, publishControllerState, saveControllerProfileCas, withControllerProfileLease } from "./routing-controller-fence.js"
/** Disposable SQL only; no RouterOS/native calls. Copies table constraints/defaults without foreign keys. */
export async function acceptRoutingControllerCas() {
const schema = "controller_cas_" + randomUUID().replaceAll("-", "")
const database = createPool(4)
await pool.query(`CREATE SCHEMA ${schema}`)
let human: PoolClient | undefined
try {
await pool.query(`CREATE TABLE ${schema}.mcp_routing_profiles (LIKE public.mcp_routing_profiles INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES)`)
human = await database.connect()
await human.query(`SET search_path TO ${schema},public`)
const input = { profileId: randomUUID(), connectionId: randomUUID(), expectedVersion: null as number | null,
profile: { marker: "first-human-grant" }, enabled: true, expiresAt: new Date(Date.now() + 600000).toISOString(),
credentialId: randomUUID(), generation: "original-generation", policyVersion: 1 }
assert.equal((await saveControllerProfileCas(human, input))?.version, 1)
assert.equal(await saveControllerProfileCas(human, input), undefined, "same-owner create collision must not overwrite")
assert.equal(await saveControllerProfileCas(human, { ...input, connectionId: randomUUID() }), undefined)
const previousSwitch = new Date(Date.now() - 1000).toISOString()
await human.query("UPDATE mcp_routing_profiles SET state=$2::jsonb WHERE id=$1", [input.profileId,
JSON.stringify({ switches: [previousSwitch], lastSwitchAt: previousSwitch, marker: "old-controller-state" })])
const fence = { id: input.profileId, connection_id: input.connectionId, version: 1 }
let entered!: () => void, resume!: () => void
const started = new Promise<void>(resolve => { entered = resolve })
const blocked = new Promise<void>(resolve => { resume = resolve })
let competitorEntered = false
const tick = withControllerProfileLease(database, input.profileId, async lease => {
await lease.query(`SET search_path TO ${schema},public`)
assert.equal(await controllerProfileCurrent(lease, fence), true)
entered(); await blocked
assert.equal(await controllerProfileCurrent(lease, fence), false)
assert.equal(await publishControllerState(lease, fence, { marker: "stale-tick" }), false)
assert.equal(await publishControllerState(lease, fence, { pausedReason: "stale-stop" }, true), false)
})
await started
try {
await withControllerProfileLease(database, input.profileId, async () => { competitorEntered = true })
assert.equal(competitorEntered, false, "second tick cannot enter the same profile")
const changed = { ...input, expectedVersion: 1, profile: { marker: "new-human-grant" }, generation: "new-generation", credentialId: randomUUID() }
assert.equal((await saveControllerProfileCas(human, changed))?.version, 2)
assert.equal(await saveControllerProfileCas(human, changed), undefined, "stale human save must fail CAS")
} finally { resume(); await tick }
const saved = (await human.query("SELECT * FROM mcp_routing_profiles WHERE id=$1", [input.profileId])).rows[0]
assert.equal(saved.enabled, true); assert.equal(saved.version, 2)
assert.deepEqual(saved.config, { marker: "new-human-grant" })
assert.deepEqual(saved.state, { switches: [previousSwitch], lastSwitchAt: previousSwitch }, "Human save preserves switch-rate history but replaces prior controller state")
assert.equal(saved.generation, "new-generation")
const current = { ...fence, version: 2 }
assert.equal(await publishControllerState(human, current, { marker: "fresh-tick" }), true)
// Emergency/portal pauses currently disable directly, without incrementing version.
await human.query("UPDATE mcp_routing_profiles SET enabled=false,state=$2::jsonb WHERE id=$1", [input.profileId, JSON.stringify({ pausedReason: "human-emergency-pause" })])
assert.equal(await controllerProfileCurrent(human, current), false)
assert.equal(await publishControllerState(human, current, { marker: "stale-after-pause" }), false)
assert.equal(await publishControllerState(human, current, { pausedReason: "stale-stop" }, true), false)
assert.deepEqual((await human.query("SELECT state FROM mcp_routing_profiles WHERE id=$1", [input.profileId])).rows[0].state, { pausedReason: "human-emergency-pause" })
await assert.rejects(withControllerProfileLease(database, input.profileId, async () => { throw new Error("fixture failure") }), /fixture failure/)
let released = false
await withControllerProfileLease(database, input.profileId, async () => { released = true })
assert.equal(released, true, "throwing tick releases its session lock")
return { accepted: true, concurrentTicks: "one-owner", staleStateWrites: 0, staleSaveWrites: 0, createCollisionWrites: 0, nativeRequests: 0 }
} finally {
human?.release()
await database.end()
await pool.query(`DROP SCHEMA ${schema} CASCADE`)
}
}