import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { STUDIO_OPS_INACTION, STUDIO_OPS_MANIFEST, STUDIO_OPS_SCENARIOS, StudioOpsEnvironment, arenaChecksum, quantizeObservable, rollout, studioOpsEnergyPenalty, studioOpsNoisePenalty, studioOpsScriptedBaseline, studioOverflights, studioSkyAt, studioVehicleReadiness, studioWeatherAt, type StudioOpsAction, type StudioOpsObservation, type StudioOpsReward, } from "../../index.ts"; import { Plan } from "../../interiors/plan.ts"; import { LUMBRIDGE_HQ } from "../../offices/lumbridge-hq.ts"; import { MATEO_COURT } from "../../offices/mateo-court.ts"; const SEEDS = [1, 0xdecafbad]; type Observation = StudioOpsObservation; function scripted(observation: unknown): StudioOpsAction { return studioOpsScriptedBaseline(observation as Observation); } /** Runs a targeted policy and reports where the episode ended. */ function reach( scenarioId: string, seed: number, policy: (observation: Observation, step: number) => StudioOpsAction, ): { reason: string | null; steps: number; total: number } { const result = rollout( new StudioOpsEnvironment(), (observation, step) => policy(observation as Observation, step), { seed, scenario: scenarioId }, ); return { reason: result.final.info.terminalReason, steps: result.steps, total: result.total }; } describe("studio-ops baselines", () => { it("keeps inaction below zero and never lets it reach the goal", () => { for (const definition of STUDIO_OPS_SCENARIOS.definitions) { for (const seed of SEEDS) { const idle = rollout(new StudioOpsEnvironment(), () => STUDIO_OPS_INACTION, { seed, scenario: definition.id, }); assert.ok(idle.total < 0, `${definition.id}/${seed} inaction=${idle.total}`); assert.notEqual(idle.final.info.terminalReason, "job-complete"); } } }); it("proves a positive scripted completion that beats inaction on every scenario", () => { for (const definition of STUDIO_OPS_SCENARIOS.definitions) { for (const seed of SEEDS) { const idle = rollout(new StudioOpsEnvironment(), () => STUDIO_OPS_INACTION, { seed, scenario: definition.id, }); const run = rollout(new StudioOpsEnvironment(), scripted, { seed, scenario: definition.id, }); const label = `${definition.id}/${seed}`; assert.equal(run.final.info.terminalReason, "job-complete", label); assert.equal(run.final.terminated, true, label); assert.equal(run.final.truncated, false, label); assert.ok(run.total > 0, `${label} scripted=${run.total}`); assert.ok(run.total > idle.total, `${label} ${run.total} <= ${idle.total}`); } } }); it("sums thirteen finite named components and authors no total", () => { const run = rollout(new StudioOpsEnvironment(), scripted, { seed: 5, scenario: "train-sf-clear-morning-desk-check", }); const components = run.final.rewardComponents as StudioOpsReward; assert.deepEqual( Object.keys(components).sort(), Object.keys(STUDIO_OPS_MANIFEST.rewardComponents).sort(), ); const sum = Object.values(components).reduce((total, value) => total + value, 0); assert.ok(Math.abs(sum - run.final.reward) < 1e-12); assert.ok(Object.values(components).every((value) => Number.isFinite(value))); }); }); describe("studio-ops terminals are each individually reachable", () => { it("completes the job", () => { assert.equal(reach("train-sf-clear-morning-desk-check", 1, scripted).reason, "job-complete"); }); it("stalls against resolved office collision", () => { // Straight into a wall, forever. The controller's own recovery gives up. const result = reach("train-la-overcast-inspection", 2, () => ({ ...STUDIO_OPS_INACTION, z: 1, })); assert.equal(result.reason, "collision-stall"); }); it("hits the invalid-interaction limit", () => { const result = reach("train-la-overcast-inspection", 2, () => ({ ...STUDIO_OPS_INACTION, interact: true, })); assert.equal(result.reason, "wrong-interaction-limit"); // Exactly at the limit rather than somewhere after it. assert.equal(result.steps, 8); }); it("exhausts the studio's energy reserve", () => { // Everything on and the car plugged into a post that draws from the same // reserve. This is the terminal the `energyReservePct` observation exists // to make visible; without it the failure would be unattributable. for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const result = reach(definition.id, 3, () => ({ ...STUDIO_OPS_INACTION, speakerVolume: 1, speakerPlay: true, vehiclePrecondition: true, vehicleCharge: true, })); assert.equal(result.reason, "battery-depleted", definition.id); } }); it("misses the departure", () => { const result = reach("dev-sf-windy-evening-delivery", 1, () => STUDIO_OPS_INACTION); assert.equal(result.reason, "departure-missed"); assert.equal(result.steps, 1000); }); it("declares every one of them except the goal as a safety terminal", () => { assert.deepEqual([...STUDIO_OPS_MANIFEST.safetyTerminals], [ "collision-stall", "wrong-interaction-limit", "battery-depleted", "departure-missed", ]); }); }); describe("studio-ops snapshot, trace and replay", () => { it("continues bit-exactly from a mid-episode snapshot on every scenario", () => { for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const environment = new StudioOpsEnvironment(); let observation = environment.reset(311, definition.id).observation; for (let index = 0; index < 25; index += 1) { observation = environment.step(scripted(observation)).observation; } const checkpoint = environment.snapshot(); const action = scripted(observation); const expected = environment.step(action); const restored = environment.restore(checkpoint); assert.equal(restored.info.step, checkpoint.step, definition.id); // Deep-equal on the whole transition: observation, reward, every // component, both flags and the state checksum. assert.deepEqual(environment.step(action), expected, definition.id); } }); it("replays a trace to the identical final checksum and cumulative reward", () => { for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const environment = new StudioOpsEnvironment(); let observation = environment.reset(311, definition.id).observation; for (let index = 0; index < 80; index += 1) { const result = environment.step(scripted(observation)); observation = result.observation; if (result.terminated || result.truncated) break; } const trace = environment.trace(); assert.equal(arenaChecksum({ ...trace, checksum: undefined }), trace.checksum); const replay = new StudioOpsEnvironment().replay(trace); assert.equal(replay.finalStateChecksum, trace.finalStateChecksum, definition.id); assert.equal(replay.cumulativeReward, trace.cumulativeReward, definition.id); assert.equal(replay.steps, trace.steps.length, definition.id); } }); /** Twelve scripted steps, and the environment left mid-episode. */ function midEpisode(): StudioOpsEnvironment { const environment = new StudioOpsEnvironment(); let observation = environment.reset(19, "train-la-hot-afternoon-studio").observation; for (let index = 0; index < 12; index += 1) { observation = environment.step(scripted(observation)).observation; } return environment; } it("throws on a tampered snapshot, and refuses to keep running afterwards", () => { const environment = midEpisode(); const snapshot = environment.snapshot(); assert.throws( () => environment.restore({ ...snapshot, cumulativeReward: snapshot.cumulativeReward + 1 }), /checksum mismatch/, ); // Re-signed, so only the source pins can catch it. const reserved = structuredClone(snapshot.simulation); reserved.energyReserveKWh = -1; const { checksum: _snapshotChecksum, ...snapshotCore } = { ...snapshot, simulation: reserved, }; assert.throws( () => environment.restore({ ...snapshotCore, checksum: arenaChecksum(snapshotCore) }), /simulation snapshot is invalid/, ); // And the environment is now un-reset rather than half-restored: the // episode bookkeeping was written before the simulation payload was // rejected, so continuing would produce a mixture of two episodes. assert.throws(() => environment.trace(), /must be reset/); assert.throws(() => environment.snapshot(), /must be reset/); // A fresh reset brings it back. assert.equal(environment.reset(19, "train-la-hot-afternoon-studio").info.step, 0); }); it("throws on a tampered trace", () => { const environment = midEpisode(); const trace = environment.trace(); assert.throws( () => new StudioOpsEnvironment().replay({ ...trace, cumulativeReward: trace.cumulativeReward + 1, }), /checksum mismatch/, ); // A re-signed reward claim: the envelope's own checksum verifies, so the // only thing standing between this and an accepted rollout is that `replay` // recomputes the reward and compares it. This is the cheat the whole // envelope exists to refuse. const inflated = trace.steps.map((frame, index) => index === 4 ? { ...frame, reward: frame.reward + 1 } : frame, ); const { checksum: _rewardChecksum, ...rewardCore } = { ...trace, steps: inflated }; assert.throws( () => new StudioOpsEnvironment().replay({ ...rewardCore, checksum: arenaChecksum(rewardCore), }), /diverged at step 5/, ); // And a re-signed action swap, which is refused wherever it first shows — // at the frame whose checksum no longer matches, or at the final state if // the swapped action happened to change nothing until the end. const swapped = trace.steps.map((frame, index) => index === 4 ? { ...frame, action: { ...frame.action, speakerPlay: true } } : frame, ); const { checksum: _traceChecksum, ...traceCore } = { ...trace, steps: swapped }; assert.throws( () => new StudioOpsEnvironment().replay({ ...traceCore, checksum: arenaChecksum(traceCore), }), /diverged at step|final state mismatch/, ); }); it("reproduces an episode exactly from the same seed, twice", () => { for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const first = rollout(new StudioOpsEnvironment(), scripted, { seed: 88, scenario: definition.id, }); const second = rollout(new StudioOpsEnvironment(), scripted, { seed: 88, scenario: definition.id, }); assert.deepEqual(first, second, definition.id); } }); }); describe("studio-ops couples its variables rather than stacking five tasks", () => { it("charges more for the same energy under more cloud, strictly", () => { // The reward's own implementation, over a fine grid: `advanceSimulation` // calls exactly this function with exactly these arguments. for (const loadKw of [0.42, 2.4, 13.9, 152]) { let previous = 0; for (let cloud = 0; cloud <= 1.0001; cloud += 0.02) { const penalty = studioOpsEnergyPenalty(loadKw, cloud); assert.ok(penalty < 0, `${loadKw}@${cloud}`); if (cloud > 0) { assert.ok( Math.abs(penalty) > Math.abs(previous), `magnitude did not rise at cloud=${cloud}, load=${loadKw}`, ); } previous = penalty; } } // And it is monotone in the load as well, which is the other half of the // claim that this is an energy price and not a weather penalty. assert.ok( Math.abs(studioOpsEnergyPenalty(150, 0.5)) > Math.abs(studioOpsEnergyPenalty(2, 0.5)), ); }); it("charges more under an overcast sky end to end, even against a heavier load", () => { // Stronger than "all else equal": the hot LA afternoon draws strictly more // power than the overcast one (a 13 K climate gap against a 4 K one) and is // still charged less, because the cloud term dominates the load difference. const energyAt = (scenarioId: string): { energy: number; cloud: number } => { const environment = new StudioOpsEnvironment(); environment.reset(1, scenarioId); const result = environment.step(STUDIO_OPS_INACTION); return { energy: (result.rewardComponents as StudioOpsReward).energy, cloud: (result.observation as Observation).cloudCover, }; }; const clear = energyAt("train-la-hot-afternoon-studio"); const overcast = energyAt("train-la-overcast-inspection"); assert.ok(clear.cloud < 0.25, `clear cloud=${clear.cloud}`); assert.ok(overcast.cloud > 0.7, `overcast cloud=${overcast.cloud}`); assert.ok( Math.abs(overcast.energy) > Math.abs(clear.energy), `${overcast.energy} vs ${clear.energy}`, ); }); it("charges more for playback the closer an aircraft is, and nothing when silent", () => { const base = { speakerPlaying: true, speakerVolume: 0.6, windKph: 5, micLive: false, deskOccupied: false, }; let previous = 0; // Walking the aircraft in from beyond audible range to directly overhead. for (let slant = 5000; slant >= 0; slant -= 100) { const penalty = studioOpsNoisePenalty({ ...base, nearestAircraftSlantM: slant }); if (slant < 5000) { assert.ok(penalty < previous, `did not worsen at slant=${slant}`); } previous = penalty; } assert.ok(previous < 0); // Beyond the overhead range it is flat, not negative: an aircraft that // cannot be heard costs nothing. assert.equal(studioOpsNoisePenalty({ ...base, nearestAircraftSlantM: 9000 }), 0); // And a silent speaker cannot be ruined by anything at all. assert.equal( studioOpsNoisePenalty({ ...base, speakerPlaying: false, nearestAircraftSlantM: 0 }), 0, ); assert.equal( studioOpsNoisePenalty({ ...base, speakerVolume: 0, nearestAircraftSlantM: 0 }), 0, ); }); it("adds wind and microphone bleed to the same penalty", () => { const quiet = { speakerPlaying: true, speakerVolume: 0.5, nearestAircraftSlantM: 40_000, windKph: 5, micLive: false, deskOccupied: false, }; assert.equal(studioOpsNoisePenalty(quiet), 0); assert.ok(studioOpsNoisePenalty({ ...quiet, windKph: 80 }) < 0); assert.ok(studioOpsNoisePenalty({ ...quiet, micLive: true, deskOccupied: true }) < 0); // Bleed needs both: a live microphone in an empty room is not on the take. assert.equal(studioOpsNoisePenalty({ ...quiet, micLive: true }), 0); }); it("computes the noise component from the sky and wind it publishes", () => { // The wiring, end to end: whatever the observation says the sky and the // wind are doing is what the penalty was computed from. A regression that // read a stale step's weather would break here and nowhere else. const environment = new StudioOpsEnvironment(); let observation = environment.reset(31, "train-la-hot-afternoon-studio") .observation as Observation; let sawOverhead = false; for (let index = 0; index < 700; index += 1) { const result = environment.step({ ...STUDIO_OPS_INACTION, micMute: true, speakerVolume: 0.5, speakerPlay: true, }); observation = result.observation as Observation; const expected = studioOpsNoisePenalty({ speakerPlaying: observation.speakerPlaying, speakerVolume: observation.speakerVolume, nearestAircraftSlantM: observation.nearestAircraftSlantM, windKph: observation.windKph, micLive: observation.micPowered && !observation.micMuted, deskOccupied: observation.deskOccupied, }); assert.equal((result.rewardComponents as StudioOpsReward).noise, expected, `step ${index}`); if (observation.aircraftOverheadCount > 0) sawOverhead = true; if (result.terminated || result.truncated) break; } assert.ok(sawOverhead, "no aircraft came overhead; the assertion above proved nothing"); }); it("couples the microphone to the robot's job rather than to a schedule", () => { // The non-negotiable property, observable: the desk in front of // `sf-desk-mic` is occupied exactly while the robot is working the station // that stands at it, and a mic left live through the rest of the episode is // charged for it. const environment = new StudioOpsEnvironment(); let observation = environment.reset(1, "train-sf-clear-morning-desk-check") .observation as Observation; let occupied = 0; let wasted = 0; let ready = 0; for (let index = 0; index < 1200; index += 1) { const result = environment.step({ ...STUDIO_OPS_INACTION, ...scripted(observation), micMute: false }); observation = result.observation as Observation; const components = result.rewardComponents as StudioOpsReward; if (observation.deskOccupied) occupied += 1; if (components.audioWaste < 0) wasted += 1; if (components.audioReady > 0) ready += 1; if (result.terminated || result.truncated) break; } assert.ok(occupied > 0, "the robot never reached the desk its microphone serves"); assert.ok(ready >= occupied, "a live mic at an occupied desk must be paid for"); assert.ok(wasted > 0, "a live mic at an empty desk must be charged for"); }); it("stops charging the mic the moment it is muted", () => { const environment = new StudioOpsEnvironment(); let observation = environment.reset(1, "train-la-overcast-inspection") .observation as Observation; const hot = environment.step({ ...STUDIO_OPS_INACTION, micMute: false }); assert.equal((hot.rewardComponents as StudioOpsReward).audioWaste < 0, true); observation = hot.observation as Observation; assert.equal(observation.deskOccupied, false); const muted = environment.step({ ...STUDIO_OPS_INACTION, micMute: true }); assert.equal((muted.rewardComponents as StudioOpsReward).audioWaste, 0); }); it("shapes the vehicle with a bounded potential that cannot be farmed", () => { // Potential-based: plugging and unplugging round-trips to zero rather than // paying twice, because the term is the *change* in a bounded readiness. const required = 61.6; assert.equal(studioVehicleReadiness(0, 21, required), 0.5); assert.equal(studioVehicleReadiness(required, 21, required), 1); assert.ok(studioVehicleReadiness(61, 21, required) < studioVehicleReadiness(61.5, 21, required)); assert.ok(studioVehicleReadiness(61, 30, required) < studioVehicleReadiness(61, 22, required)); // Bounded on both sides, so the shaping cannot diverge. for (const soc of [-10, 0, 50, 500]) { for (const cabin of [-40, 21, 90]) { const value = studioVehicleReadiness(soc, cabin, required); assert.ok(value >= 0 && value <= 1, `${soc}/${cabin}`); } } }); }); describe("studio-ops weather and sky are scenario parameters, deterministically evolved", () => { const parameters = STUDIO_OPS_SCENARIOS.resolve(7, "dev-sf-windy-evening-delivery").parameters; it("is a pure function of the scenario and the elapsed time", () => { for (const seconds of [0, 13.7, 60, 119.9]) { assert.deepEqual( studioWeatherAt(parameters, seconds), studioWeatherAt(parameters, seconds), ); } assert.deepEqual(studioOverflights(parameters), studioOverflights(parameters)); }); it("actually moves inside one episode, in every field a policy can read", () => { const samples = [0, 20, 40, 60, 80, 100, 119].map((s) => studioWeatherAt(parameters, s)); for (const field of ["cloudCover", "precipitation", "windKph", "windDirDeg"] as const) { const values = new Set(samples.map((sample) => sample[field])); assert.ok(values.size > 1, `${field} never changed`); } const sky = [0, 30, 60, 90, 119].map((s) => studioSkyAt(studioOverflights(parameters), s)); assert.ok(new Set(sky.map((entry) => entry.nearestSlantM)).size > 1); }); it("keeps every reading inside the range its own space declares", () => { for (let seconds = 0; seconds <= 120; seconds += 0.5) { const weather = studioWeatherAt(parameters, seconds); assert.ok(weather.cloudCover >= 0 && weather.cloudCover <= 1); assert.ok(weather.precipitation >= 0 && weather.precipitation <= 1); assert.ok(weather.windKph >= 0); assert.ok(weather.windDirDeg >= 0 && weather.windDirDeg < 360); assert.ok(weather.visibilityKm >= 0.2); } }); it("never claims a profile was observed when it was invented", () => { // Every shipped scenario is a fixture, and the flag says so. The field // exists for an operator who freezes a real observation into one. for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const resolved = STUDIO_OPS_SCENARIOS.resolve(3, definition.id); assert.equal(resolved.parameters.weatherReported, false, definition.id); const environment = new StudioOpsEnvironment(); const observation = environment.reset(3, definition.id).observation as Observation; assert.equal(observation.weatherReported, false, definition.id); } }); it("puts the sky and the weather in the scenario hash, where a trace can carry them", () => { const a = STUDIO_OPS_SCENARIOS.resolve(3, "train-la-overcast-inspection"); const b = STUDIO_OPS_SCENARIOS.resolve(4, "train-la-overcast-inspection"); assert.notEqual(a.hash, b.hash); assert.notEqual(a.parameters.aircraftScheduleSeed, b.parameters.aircraftScheduleSeed); assert.notEqual(a.parameters.cloudPhase, b.parameters.cloudPhase); // The jitter must not turn a named profile into a different one. assert.ok(a.parameters.cloudCoverBase > 0.85 && b.parameters.cloudCoverBase > 0.85); }); }); describe("studio-ops quantises everything a transcendental touched", () => { it("reports no observation finer than the quantum", () => { // The cross-runtime defence, checked where it has to hold: if any of these // carried full double precision, a verifier on other hardware could reject // an honest rollout over the last bit of a sine. const environment = new StudioOpsEnvironment(); let observation = environment.reset(2, "train-la-hot-afternoon-studio") .observation as Observation; const fields = [ "sunAltitudeDeg", "sunAzimuthDeg", "hourOfDay", "cloudCover", "precipitation", "visibilityKm", "windKph", "windDirDeg", "nearestAircraftSlantM", "vehicleSocPct", "vehicleCabinC", "energyReservePct", ] as const; for (let index = 0; index < 400; index += 1) { for (const field of fields) { const value = observation[field]; assert.equal(value, quantizeObservable(value), `${field} at step ${index}`); } const result = environment.step(scripted(observation)); observation = result.observation as Observation; if (result.terminated || result.truncated) break; } }); it("puts no Date anywhere near the checksum", () => { // `canonicalJson` throws on one, so this is a live proof rather than a // convention: the snapshot survives being hashed. const environment = new StudioOpsEnvironment(); environment.reset(2, "train-sf-clear-morning-desk-check"); environment.step(STUDIO_OPS_INACTION); const snapshot = environment.snapshot(); assert.match(arenaChecksum(snapshot.simulation), /^fnv1a64:/); assert.equal(typeof JSON.parse(JSON.stringify(snapshot.simulation)), "object"); }); }); describe("studio-ops wraps the simulators the renderer drives", () => { it("simulates exactly the devices the shipped packs declare and Plan resolved", () => { // Not a headless copy of the device list: the ids the scenarios name are // the pack's own, and they are the ones `Plan` accepted. const plans = { "lumbridge-hq": new Plan(LUMBRIDGE_HQ, { depth: "public", warn: false }), "mateo-court": new Plan(MATEO_COURT, { depth: "public", warn: false }), }; for (const definition of STUDIO_OPS_SCENARIOS.definitions) { const plan = plans[definition.parameters.officeId]; const mic = plan.device(definition.parameters.micId); const speaker = plan.device(definition.parameters.speakerId); assert.ok(mic, `${definition.id} names a microphone the plan did not resolve`); assert.ok(speaker, `${definition.id} names a speaker the plan did not resolve`); assert.equal(mic.kind, "mic"); assert.equal(speaker.kind, "speaker"); assert.equal(mic.provenance, "simulated"); assert.match(mic.disclosure.toLowerCase(), /simulat/); } }); it("names the same simulator stack in its manifest as it imports", () => { assert.equal(STUDIO_OPS_MANIFEST.id, "studio-ops-v1"); for (const fragment of [ "Plan", "robotActivity", "createSimulatedDevices", "createSimulatedVehicleTelemetry", "solarPosition", ]) { assert.ok(STUDIO_OPS_MANIFEST.simulator.includes(fragment), fragment); } }); it("observes the robot job, the hardware, the weather, the car and the sky at once", () => { // The point of the environment, as a shape assertion: forty-four fields // across five groups, none of them constant across the catalogue. const environment = new StudioOpsEnvironment(); const observation = environment.reset(1, "dev-la-marine-layer-loft-delivery") .observation as Observation; assert.equal(Object.keys(observation).length, STUDIO_OPS_MANIFEST.observationFields.length); assert.deepEqual( Object.keys(observation).sort(), [...STUDIO_OPS_MANIFEST.observationFields].sort(), ); assert.equal(observation.officeId, "mateo-court"); assert.equal(observation.levelId, "level-2"); assert.equal(observation.micPowered, true); assert.equal(observation.energyReservePct, 100); assert.equal(observation.stepsToDeparture, 1000); }); it("keeps the loft scenario's desk unreachable, on purpose", () => { // Every LA device is on level 1 and this job runs on level 2, so the // correct play is to mute and get on with it. A policy that has only seen a // reachable desk has not learned the difference between "unmute when // somebody arrives" and "unmute". const environment = new StudioOpsEnvironment(); let observation = environment.reset(1, "dev-la-marine-layer-loft-delivery") .observation as Observation; for (let index = 0; index < 400; index += 1) { const result = environment.step(scripted(observation)); observation = result.observation as Observation; assert.equal(observation.deskOccupied, false, `step ${index}`); if (result.terminated || result.truncated) break; } }); }); describe("studio-ops device commands settle instead of churning", () => { it("stops re-commanding a setpoint the instrument has already reached", () => { // A gain of 12.005 dB is reported back as 12.01, so an action compared // against the reading would disagree with itself forever and be charged a // churn cost on every step for holding a control still. `normalizeAction` // rounds to the precision the instrument reports, which makes the setpoint // a fixed point. const environment = new StudioOpsEnvironment(); environment.reset(1, "train-la-overcast-inspection"); const action: StudioOpsAction = { ...STUDIO_OPS_INACTION, micGain: 12.005, micMute: true, speakerVolume: 0.2225, }; const first = environment.step(action); const settled = environment.step(action); const again = environment.step(action); const controlOf = (result: typeof first): number => (result.rewardComponents as StudioOpsReward).control; assert.ok(controlOf(first) < 0, "the first step really does issue commands"); assert.equal(controlOf(settled), controlOf(again)); assert.equal(controlOf(settled), 0); const observation = again.observation as Observation; assert.equal(observation.micGainDb, 12.01); assert.equal(observation.speakerVolume, 0.223); }); });