mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Learning UI
Expose fleet-learning operations through Galaxy presentation endpoints. Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""Galaxy's read-only FLM owner and fresh parked authority lifetime."""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from openpilot.starpilot.flm.operation_owner import FlmAnalysisOwner, FlmOperationError
|
||||
from openpilot.starpilot.galaxy.drive_history import SEGMENT_NAME
|
||||
|
||||
|
||||
def validate_action(operation: str, payload: object) -> dict:
|
||||
if type(payload) is not dict:
|
||||
raise ValueError('Invalid FLM operation')
|
||||
if any(type(key) is not str for key in payload):
|
||||
raise ValueError('Invalid FLM operation')
|
||||
fields = {key: value for key, value in payload.items() if type(key) is str}
|
||||
if operation == 'live-action':
|
||||
if fields.get('action') not in ('save','delete','trial','apply','restore','accept','disable','reset') or type(fields.get('token')) is not str:
|
||||
raise ValueError('Invalid live FLM action')
|
||||
elif operation in ('start', 'train'):
|
||||
names = fields.get('segments')
|
||||
if set(fields) != ({'segments'} if operation == 'start' else {'segments', 'token'}) or type(names) is not list:
|
||||
raise ValueError('Invalid segment selection')
|
||||
if (not 1 <= len(names) <= 5 or any(type(name) is not str or len(name) > 180 or
|
||||
SEGMENT_NAME.fullmatch(name) is None for name in names) or
|
||||
len(set(names)) != len(names)):
|
||||
raise ValueError('Select one to five distinct closed segments')
|
||||
if operation == 'train' and (type(fields['token']) is not str or re.fullmatch(r'[a-f0-9]{64}', fields['token']) is None):
|
||||
raise ValueError('Invalid training source')
|
||||
elif operation in ('recommend', 'save-report'):
|
||||
required = {'operationId', 'feedback'} | ({'token', 'generatedId', 'id', 'label'} if operation == 'save-report' else set())
|
||||
feedback = fields.get('feedback')
|
||||
if set(fields) != required or type(feedback) is not dict:
|
||||
raise ValueError('Invalid report profile request')
|
||||
if (set(feedback) - {'acceptedDimensions', 'ignoredDimensions'} or
|
||||
any(type(values) is not list or len(values) > 128 or any(type(key) is not str or len(key) > 128 for key in values)
|
||||
for values in feedback.values())):
|
||||
raise ValueError('Invalid evidence feedback')
|
||||
validate_action('report', {'operationId': fields.get('operationId')})
|
||||
if operation == 'save-report':
|
||||
if (type(fields['generatedId']) is not str or len(fields['generatedId']) > 128 or
|
||||
type(fields['id']) is not str or re.fullmatch(r'[a-zA-Z0-9_-]{1,48}', fields['id']) is None or
|
||||
type(fields['label']) is not str or not fields['label'].strip() or len(fields['label']) > 80 or
|
||||
type(fields['token']) is not str or re.fullmatch(r'[a-f0-9]{64}', fields['token']) is None):
|
||||
raise ValueError('Invalid generated profile identity')
|
||||
elif operation in ('cancel', 'report'):
|
||||
operation_id = fields.get('operationId')
|
||||
if (set(fields) != {'operationId'} or type(operation_id) is not str or
|
||||
re.fullmatch(r'[0-9a-f]{32}:[1-9][0-9]{0,15}', operation_id) is None):
|
||||
raise ValueError('Invalid operation identity')
|
||||
else:
|
||||
raise ValueError('Invalid FLM operation')
|
||||
return fields
|
||||
|
||||
|
||||
class FlmOperations:
|
||||
def __init__(self, root: Path, *, context=None, owner=None):
|
||||
if context is None:
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.galaxy.settings import LiveContextSource
|
||||
context = LiveContextSource(Params())
|
||||
self.context = context
|
||||
self.live = None
|
||||
self._training_session = None
|
||||
self.owner = owner if owner is not None else FlmAnalysisOwner(root=root, parked=context.parked, completed=self._record_progress)
|
||||
|
||||
def request(self, operation: str, payload: dict | None = None, *, session_valid=lambda: True, session_guard=None) -> dict:
|
||||
if operation in ("live", "live-action"):
|
||||
if self.live is None:
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.flm.operation_owner import FlmTrialOwner
|
||||
self.live = FlmTrialOwner(self.context.params if hasattr(self.context, "params") else Params(), self.context)
|
||||
if operation == "live" and payload is None:
|
||||
return self.live.snapshot()
|
||||
validate_action(operation,payload)
|
||||
return self.live.action(payload,session_valid=session_valid)
|
||||
if operation in ('train', 'recommend', 'save-report'):
|
||||
selected = validate_action(operation, payload)
|
||||
if self.live is None:
|
||||
self.request('live')
|
||||
if not session_valid():
|
||||
raise FlmOperationError('not_parked')
|
||||
if operation == 'train':
|
||||
context = self.live.training_context(selected['token'])
|
||||
previous = self._training_session
|
||||
self._training_session = (context['sourceToken'], session_valid, session_guard)
|
||||
try:
|
||||
return self.owner.start(tuple(selected['segments']), gm_context=context)
|
||||
except Exception:
|
||||
self._training_session = previous
|
||||
raise
|
||||
evidence = self.owner.report(selected['operationId']).get('gmEvidence')
|
||||
if evidence is None:
|
||||
raise ValueError('This report has no GM trial profiles')
|
||||
from openpilot.starpilot.flm.gm_recommend import build_report
|
||||
regenerated = build_report(evidence['context'], evidence['summaries'], evidence['stats'], feedback=selected['feedback'])
|
||||
if operation == 'recommend':
|
||||
return {'version': 1, 'operationId': selected['operationId'], 'recommendation': regenerated}
|
||||
return self.live.save_generated(regenerated, selected['generatedId'], selected['id'], selected['label'],
|
||||
selected['token'], session_valid=session_valid)
|
||||
if operation == 'status' and payload is None:
|
||||
return self.owner.snapshot()
|
||||
selected = validate_action(operation, payload)
|
||||
if operation == 'start':
|
||||
return self.owner.start(tuple(selected['segments']))
|
||||
if operation == 'cancel':
|
||||
return self.owner.cancel(selected['operationId'])
|
||||
return self.owner.report(selected['operationId'])
|
||||
|
||||
def _record_progress(self, report):
|
||||
session = self._training_session
|
||||
if (self.live is None or session is None or session[2] is None or
|
||||
report['gmEvidence']['context']['sourceToken'] != session[0]):
|
||||
return False
|
||||
# HTTP cancel joins the monitor while holding this same guard. Optional
|
||||
# progress metadata must decline a busy guard, never wait behind cancel.
|
||||
guard = session[2]
|
||||
if not guard.acquire(blocking=False):
|
||||
return False
|
||||
try:
|
||||
with self.owner.completion_guard(report['operationId']) as current:
|
||||
return current and session[1]() and self.live.record_cleanup(report, session_valid=session[1])
|
||||
finally:
|
||||
guard.release()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self.owner.close()
|
||||
finally:
|
||||
self.context.close()
|
||||
|
||||
|
||||
def error_status(error: FlmOperationError) -> int:
|
||||
return 400 if error.code == 'invalid_request' else (409 if error.code in (
|
||||
'busy', 'not_parked', 'operation_changed', 'canceled') else 503)
|
||||
@@ -0,0 +1,297 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { FlmLiveEditor, validLiveFlm, validGmEvidence, gmEventSeries, FlmFeed, FlmPage, flmChart, selectableSegments, validFlmReport, validFlmStatus } from '../web/js/flm.js'
|
||||
|
||||
const flush = async () => { for (let i = 0; i < 12; i++) await Promise.resolve() }
|
||||
const route = '1234abcd--0123456789'
|
||||
const segment = `${route}--0`
|
||||
const inventory = { schemaVersion: 1, source: 'local', partialHistory: true, scanIncomplete: true,
|
||||
routes: [{ routeId: route, segmentCount: 2, segments: [
|
||||
{ number: 0, segmentName: segment, files: { rlog: true, qlog: true, fcamera: false, dcamera: false, ecamera: false, qcamera: false } },
|
||||
{ number: 1, segmentName: `${route}--1`, files: { rlog: false, qlog: true, fcamera: false, dcamera: false, ecamera: false, qcamera: false } },
|
||||
] }] }
|
||||
assert.deepEqual(selectableSegments(inventory), [{ routeId: route, number: 0, name: segment }])
|
||||
assert.deepEqual(selectableSegments({ ...inventory, routes: [] }), [])
|
||||
assert.deepEqual(selectableSegments({ ...inventory, routes: [{ ...inventory.routes[0], segments: [{ ...inventory.routes[0].segments[0], segmentName: '../unsafe' }] }] }), [])
|
||||
|
||||
const operation = { version: 1, operationId: 'owner:1', state: 'running', selected: 1, processed: 0, errorCode: null }
|
||||
assert.equal(validFlmStatus({ ...operation, state: 'idle', operationId: null, selected: 0 }), true)
|
||||
assert.equal(validFlmStatus(operation), true)
|
||||
assert.equal(validFlmStatus({ ...operation, processed: 2 }), false)
|
||||
assert.equal(validFlmStatus({ ...operation, operationId: '<script>' }), false)
|
||||
const points = [
|
||||
{ mono_ns: 1e9, desired_lat_accel: 0, actual_lat_accel: .1, continuity_id: 0 },
|
||||
{ mono_ns: 1.1e9, desired_lat_accel: .3, actual_lat_accel: .2, continuity_id: 0 },
|
||||
{ mono_ns: 1.5e9, desired_lat_accel: -.2, actual_lat_accel: -.1, continuity_id: 1 },
|
||||
{ mono_ns: 1.6e9, desired_lat_accel: -.3, actual_lat_accel: -.2, continuity_id: 1 },
|
||||
]
|
||||
const geometry = flmChart(points)
|
||||
assert.equal(geometry.valid, true)
|
||||
assert.deepEqual(geometry.paths.map((paths) => paths.length), [2, 2], 'recorded continuity break splits both lines')
|
||||
assert.equal(geometry.duration, '0.6')
|
||||
assert.equal(flmChart([{ ...points[0], desired_lat_accel: Infinity }]).valid, false)
|
||||
assert.equal(flmChart([points[0], { ...points[1], continuity_id: 0, mono_ns: points[0].mono_ns }]).paths[0].length, 0,
|
||||
'repeated source time cannot form a line')
|
||||
const report = { schemaVersion: 1, purpose: 'offline_tracking_diagnostics', operationId: 'owner:1',
|
||||
tuneRecommendation: null, vehicleQualification: false, segments: [{ source: { segmentName: segment, sha256: 'a'.repeat(64), compressedBytes: 120, codec: 'zst' },
|
||||
analysis: { route, number: 0, status: 'measured', car_params_sha256: 'b'.repeat(64), messages: 10, torque_frames: 4,
|
||||
eligible_samples: 4, exclusions: [['driver_override_or_boundary', 2]], mean_abs_error: .12,
|
||||
root_mean_square_error: .18, series: points, windows: [], windows_truncated: false } }] }
|
||||
assert.equal(validFlmReport(report, 'owner:1'), true)
|
||||
assert.equal(validFlmReport({ ...report, operationId: 'owner:0' }, 'owner:1'), false)
|
||||
assert.equal(validFlmReport({ ...report, tuneRecommendation: {} }, 'owner:1'), false)
|
||||
assert.equal(validFlmReport({ ...report, segments: [{ ...report.segments[0], analysis: { ...report.segments[0].analysis, mean_abs_error: Infinity } }] }, 'owner:1'), false)
|
||||
for (const status of ['insufficient_samples', 'missing_car_params', 'unsupported_car', 'unsupported_controller']) {
|
||||
assert.equal(validFlmReport({ ...report, segments: [{ ...report.segments[0], analysis: { ...report.segments[0].analysis,
|
||||
status, mean_abs_error: null, root_mean_square_error: null, series: [] } }] }, 'owner:1'), true)
|
||||
}
|
||||
assert.match(FlmPage.template, /scan was incomplete|scan was incomplete/i)
|
||||
assert.match(FlmPage.template, /No tune recommendation/)
|
||||
assert.doesNotMatch(FlmPage.template, /v-html|Apply tune|Save tune|Cloud/)
|
||||
const app = readFileSync(new URL('../web/js/app.js', import.meta.url), 'utf8')
|
||||
assert.match(app, /route\.path === '\/tuning\/flm'/)
|
||||
globalThis.document = { createElement: () => ({
|
||||
set innerHTML(value) {
|
||||
this.textContent = value.replaceAll('&', '&')
|
||||
this.children = [{ getAttribute: () => this.textContent.match(/foo="([\s\S]*?)"/)?.[1] ?? '' }]
|
||||
}, textContent: '', children: [],
|
||||
}) }
|
||||
const { compile, reactive, computed } = await import('../web/vendor/vue/vue.esm-browser.js')
|
||||
const reactivePage = reactive({ selected: [segment], busy: false, requesting: false,
|
||||
operation: { ...operation, state: 'completed', processed: 1 } })
|
||||
Object.defineProperty(reactivePage, 'analysisAvailable', { get: () => FlmPage.computed.analysisAvailable.call(reactivePage) })
|
||||
const canAnalyze = computed(() => FlmPage.computed.canAnalyze.call(reactivePage))
|
||||
assert.equal(canAnalyze.value, true)
|
||||
reactivePage.requesting = true
|
||||
assert.equal(canAnalyze.value, false, 'Analyze stays locked while completed report is still loading')
|
||||
assert.equal(reactivePage.analysisAvailable, true, 'background reads do not change the appearance of Analyze')
|
||||
reactivePage.requesting = false
|
||||
assert.equal(canAnalyze.value, true, 'Vue recomputes after report request retires')
|
||||
const rendered = JSON.stringify(compile(FlmPage.template)({ ...FlmPage.data(), ...FlmPage.methods, unauthorized: () => {},
|
||||
mode: 'local', inventoryStatus: 'ready', inventory,
|
||||
inventoryError: '', available: selectableSegments(inventory), operation: { ...operation, state: 'completed', processed: 1 },
|
||||
report, operationError: '', busy: false, requesting: false, selected: [segment], analysisAvailable: true, canAnalyze: true, operationFeed: {},
|
||||
inventoryFeed: {}, toggle: () => {}, analyze: () => {}, resultLabel: FlmPage.methods.resultLabel,
|
||||
metric: FlmPage.methods.metric, exclusions: FlmPage.methods.exclusions, go: () => {} }, []))
|
||||
assert.match(rendered, /FLM/)
|
||||
assert.match(rendered, /Mean absolute error/)
|
||||
assert.match(rendered, /No tune recommendation/)
|
||||
assert.match(rendered, /Desired/)
|
||||
assert.match(rendered, /Actual/)
|
||||
assert.match(rendered, /1234abcd--0123456789--0/)
|
||||
|
||||
function fixture() {
|
||||
const requests = [], states = [], timers = new Map()
|
||||
let next = 0, revoked = 0
|
||||
const feed = new FlmFeed({ publish: (state) => states.push(state), unauthorized: () => revoked++,
|
||||
fetcher: (url, options) => new Promise((resolve) => requests.push({ url, options, resolve })),
|
||||
later: (fn, ms) => { timers.set(++next, { fn, ms }); return next }, cancelTimer: (id) => timers.delete(id) })
|
||||
async function reply(index, body, code = 200) {
|
||||
requests[index].resolve({ status: code, ok: code === 200, json: async () => body })
|
||||
await flush()
|
||||
}
|
||||
return { feed, requests, states, timers, reply, get revoked() { return revoked } }
|
||||
}
|
||||
|
||||
const normal = fixture()
|
||||
normal.feed.start()
|
||||
assert.equal(normal.requests[0].url, './api/flm/status')
|
||||
await normal.reply(0, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
normal.feed.startAnalysis([segment])
|
||||
assert.equal(normal.requests[1].url, './api/flm/start')
|
||||
assert.deepEqual(JSON.parse(normal.requests[1].options.body), { segments: [segment] })
|
||||
await normal.reply(1, operation)
|
||||
assert.equal(normal.requests[2].url, './api/flm/status')
|
||||
await normal.reply(2, operation)
|
||||
assert.equal(normal.feed.status.state, 'running')
|
||||
normal.feed.cancelAnalysis()
|
||||
assert.equal(normal.requests[3].url, './api/flm/cancel')
|
||||
assert.deepEqual(JSON.parse(normal.requests[3].options.body), { operationId: 'owner:1' })
|
||||
await normal.reply(3, { ...operation, state: 'canceled' })
|
||||
await normal.reply(4, { ...operation, state: 'canceled' })
|
||||
assert.equal(normal.feed.status.state, 'canceled')
|
||||
normal.feed.stop()
|
||||
|
||||
const completed = fixture()
|
||||
completed.feed.start()
|
||||
await completed.reply(0, { ...operation, state: 'completed', processed: 1 })
|
||||
assert.equal(completed.requests[1].url, './api/flm/report?operationId=owner%3A1')
|
||||
assert.equal(completed.states.at(-1).requesting, true)
|
||||
await completed.reply(1, report)
|
||||
assert.equal(completed.feed.report.segments.length, 1)
|
||||
assert.equal(completed.states.at(-1).requesting, false, 'retired report request is published')
|
||||
completed.feed.stop()
|
||||
|
||||
const immediate = fixture()
|
||||
immediate.feed.start()
|
||||
await immediate.reply(0, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
immediate.feed.startAnalysis([segment])
|
||||
await immediate.reply(1, { ...operation, state: 'completed', processed: 1 })
|
||||
assert.equal(immediate.feed.status.state, 'completed', 'start preserves an already-completed owner snapshot')
|
||||
assert.equal(immediate.feed.status.processed, 1)
|
||||
await immediate.reply(2, { ...operation, state: 'completed', processed: 1 })
|
||||
await immediate.reply(3, report)
|
||||
assert.equal(immediate.feed.report.operationId, 'owner:1')
|
||||
immediate.feed.stop()
|
||||
|
||||
const revoked = fixture()
|
||||
revoked.feed.start()
|
||||
await revoked.reply(0, { code: 'setup_required' }, 503)
|
||||
assert.equal(revoked.revoked, 1)
|
||||
assert.equal(revoked.feed.report, null)
|
||||
const logout = fixture()
|
||||
logout.feed.start()
|
||||
await logout.reply(0, {}, 401)
|
||||
assert.equal(logout.revoked, 1)
|
||||
const revokedMutation = fixture()
|
||||
revokedMutation.feed.start()
|
||||
await revokedMutation.reply(0, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
revokedMutation.feed.startAnalysis([segment])
|
||||
await revokedMutation.reply(1, { code: 'access_unavailable' }, 503)
|
||||
assert.equal(revokedMutation.revoked, 1)
|
||||
assert.equal(revokedMutation.feed.report, null)
|
||||
|
||||
const lateReport = fixture()
|
||||
lateReport.feed.start()
|
||||
await lateReport.reply(0, { ...operation, state: 'completed', processed: 1 })
|
||||
lateReport.feed.stop()
|
||||
await lateReport.reply(1, report)
|
||||
assert.equal(lateReport.feed.report, null, 'report cannot reappear after page hide or logout')
|
||||
|
||||
const late = fixture()
|
||||
late.feed.start()
|
||||
late.feed.stop()
|
||||
late.feed.start()
|
||||
await late.reply(1, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
const count = late.states.length
|
||||
await late.reply(0, { ...operation, state: 'completed', processed: 1 })
|
||||
assert.equal(late.states.length, count)
|
||||
assert.equal(late.feed.status.state, 'idle')
|
||||
late.feed.stop()
|
||||
|
||||
const timeout = fixture()
|
||||
timeout.feed.start()
|
||||
const [timeoutId, deadline] = [...timeout.timers.entries()][0]
|
||||
timeout.timers.delete(timeoutId)
|
||||
deadline.fn()
|
||||
await flush()
|
||||
assert.match(timeout.feed.error, /timed out/)
|
||||
timeout.feed.refresh()
|
||||
assert.equal(timeout.requests.length, 2)
|
||||
await timeout.reply(1, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
await timeout.reply(0, operation)
|
||||
assert.equal(timeout.feed.status.state, 'idle', 'late response cannot overwrite refreshed status')
|
||||
timeout.feed.stop()
|
||||
|
||||
const mutationLate = fixture()
|
||||
mutationLate.feed.start()
|
||||
await mutationLate.reply(0, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
mutationLate.feed.startAnalysis([segment])
|
||||
mutationLate.feed.stop()
|
||||
|
||||
const priorDocument = globalThis.document
|
||||
const listeners = new Map()
|
||||
globalThis.document = { hidden: true, addEventListener: (name, cb) => listeners.set(name, cb),
|
||||
removeEventListener: (name, cb) => { if (listeners.get(name) === cb) listeners.delete(name) } }
|
||||
const page = { mode: 'local', $data: { operation: null, report: null, operationError: '', busy: false },
|
||||
unauthorized() { throw new Error('revoked') } }
|
||||
FlmPage.created.call(page)
|
||||
const inventoryRequests = [], operationRequests = []
|
||||
page.inventoryFeed.fetcher = (url, options) => new Promise((resolve) => inventoryRequests.push({ url, options, resolve }))
|
||||
page.operationFeed.fetcher = (url, options) => new Promise((resolve) => operationRequests.push({ url, options, resolve }))
|
||||
FlmPage.mounted.call(page)
|
||||
assert.equal(inventoryRequests.length + operationRequests.length, 0, 'hidden mount makes no filesystem/API request')
|
||||
document.hidden = false
|
||||
listeners.get('visibilitychange')()
|
||||
assert.equal(inventoryRequests.length, 1)
|
||||
assert.equal(operationRequests.length, 1)
|
||||
document.hidden = true
|
||||
listeners.get('visibilitychange')()
|
||||
assert.equal(page.$data.report, null)
|
||||
FlmPage.beforeUnmount.call(page)
|
||||
assert.equal(listeners.size, 0)
|
||||
globalThis.document = priorDocument
|
||||
mutationLate.feed.start()
|
||||
await mutationLate.reply(2, { version: 1, operationId: null, state: 'idle', selected: 0, processed: 0, errorCode: null })
|
||||
await mutationLate.reply(1, operation)
|
||||
assert.equal(mutationLate.feed.status.state, 'idle')
|
||||
assert.equal(mutationLate.feed.busy, false)
|
||||
assert.equal(mutationLate.states.at(-1).requesting, false, 'old request cleanup cannot mark new generation busy')
|
||||
mutationLate.feed.stop()
|
||||
|
||||
// Analyze is beside the selection, before even a long segment list; no duplicate plots destination.
|
||||
assert.ok(FlmPage.template.indexOf('@click="analyze"') < FlmPage.template.indexOf('v-for="segment in available"'))
|
||||
assert.equal(FlmPage.template.includes("go('/tuning/plots')"), false)
|
||||
|
||||
// Existing offline analysis remains separate from explicit authenticated parked edits.
|
||||
const liveSurface = { profile: 'torque_universal', knobs: { ff_gain_left: .3 }, baseValues: null }
|
||||
const liveState = { version: 1, available: true, editable: true, resettable: true, token: 'a'.repeat(64),
|
||||
vehicle: 'CHEVROLET_MALIBU_ASCM', controller: 'starpilot', profile: 'torque_universal',
|
||||
knobs: { ff_gain_left: { min: -.4, max: .6 } }, defaults: liveSurface,
|
||||
curveDefaults: [.16, .18, .20, .23, .27], manual: '{}', manualConflict: false, preconditions: {}, inactiveKnobs: {},
|
||||
state: { saved: { one: { label: 'One', surface: liveSurface } }, active: 'one', applied: true,
|
||||
trial: 'b'.repeat(32), baselineActive: null, baselineApplied: false, manual: {}, baselineManual: null, appliedManual: null, cleanupProgress: false } }
|
||||
assert.equal(validLiveFlm(liveState), true)
|
||||
for (const bad of [ { ...liveState, token: 'old' }, { ...liveState, curveDefaults: [NaN] },
|
||||
{ ...liveState, state: { ...liveState.state, active: 'missing' } },
|
||||
{ ...liveState, defaults: { ...liveSurface, knobs: { ff_gain_left: -.5 } } } ]) assert.equal(validLiveFlm(bad), false)
|
||||
const editor = { ...FlmLiveEditor.data(), live: liveState, closed: false,
|
||||
unauthorized: () => { throw new Error('unexpected auth failure') } }
|
||||
for (const [key, method] of Object.entries(FlmLiveEditor.methods)) editor[key] = method.bind(editor)
|
||||
editor.choose('one')
|
||||
assert.deepEqual(editor.curve, liveState.curveDefaults, 'curve starts from owner-derived source values')
|
||||
assert.equal(editor.useCurve, false)
|
||||
editor.draft.knobs.ff_gain_left = .2
|
||||
assert.equal(liveState.state.saved.one.surface.knobs.ff_gain_left, .3, 'draft never mutates the server snapshot')
|
||||
const posted = []
|
||||
const savedFetch = globalThis.fetch
|
||||
const savedHidden = document.hidden
|
||||
try {
|
||||
document.hidden = false
|
||||
globalThis.fetch = async (url, options) => { posted.push([url, JSON.parse(options.body)]); return { ok: true, status: 200, json: async () => liveState } }
|
||||
await editor.action('trial', { id: 'one' })
|
||||
assert.deepEqual(posted[0], ['./api/flm/live-action', { action: 'trial', token: liveState.token, id: 'one' }])
|
||||
await editor.restore()
|
||||
await flush()
|
||||
assert.deepEqual(posted[1][1], { action: 'restore', token: liveState.token, trial: liveState.state.trial })
|
||||
editor.live = { ...liveState, editable: false }
|
||||
await editor.action('trial', { id: 'one' })
|
||||
assert.equal(posted.length, 2, 'moving view cannot issue a mutation')
|
||||
editor.live = liveState
|
||||
document.hidden = true
|
||||
await editor.action('disable')
|
||||
assert.equal(posted.length, 2, 'hidden editor does not send new requests')
|
||||
} finally { globalThis.fetch = savedFetch; document.hidden = savedHidden }
|
||||
|
||||
|
||||
const staleRich = { ...liveState, available: false, editable: false, resettable: true,
|
||||
controller: 'standard', profile: 'torque_universal', reason: 'Selected controller changed.',
|
||||
state: { ...liveState.state, saved: { one: { label: 'Rich', surface: { ...liveSurface, profile: 'gm_bolt_2022_2023' } } } } }
|
||||
assert.equal(validLiveFlm(staleRich), true, 'stale rich data admits only the exact reset-only owner response')
|
||||
assert.equal(validLiveFlm({ ...staleRich, editable: true }), false)
|
||||
assert.equal(validLiveFlm({ ...staleRich, available: true }), false, 'strict editable profile matching remains')
|
||||
try {
|
||||
document.hidden = false
|
||||
editor.live = staleRich
|
||||
globalThis.fetch = async (url, options) => { posted.push([url, JSON.parse(options.body)]); return { ok: true, status: 200, json: async () => liveState } }
|
||||
const count = posted.length
|
||||
await editor.action('trial', { id: 'one' })
|
||||
assert.equal(posted.length, count)
|
||||
await editor.action('reset')
|
||||
assert.deepEqual(posted.at(-1)[1], { action: 'reset', token: staleRich.token })
|
||||
} finally { globalThis.fetch = savedFetch; document.hidden = savedHidden }
|
||||
const gmEvidence = { fit: false, vehicleQualification: false,
|
||||
context: { version: 1, sourceToken: 'a'.repeat(64), capability: { fingerprint: 'CHEVROLET_MALIBU_ASCM', controller: 'starpilot' } },
|
||||
summaries: [{ dimensionId: 'understeer:left:mid', severity: 1 }],
|
||||
paths: ['baseline_fix', 'cleanup_pass'].map((key) => ({ key, suggestions: [], profiles: [] })) }
|
||||
assert.equal(validGmEvidence(gmEvidence), true)
|
||||
assert.equal(validGmEvidence({ ...gmEvidence, fit: true }), false)
|
||||
assert.equal(validFlmReport({ ...report, purpose: 'gm_flm_evidence_profiles', gmEvidence }, 'owner:1'), true)
|
||||
assert.equal(validFlmReport({ ...report, purpose: 'gm_flm_evidence_profiles', gmEvidence: { ...gmEvidence, fit: true } }, 'owner:1'), false)
|
||||
assert.match(FlmPage.template, /Generate GM trials/)
|
||||
assert.match(FlmLiveEditor.template, /preconditions/)
|
||||
assert.match(FlmLiveEditor.template, /inactiveKnobs/)
|
||||
|
||||
assert.equal(gmEventSeries({ times: [0, .1], desired: [.2, .3], actual: [.1, .2] }).length, 2)
|
||||
assert.deepEqual(gmEventSeries({ times: [0, 0], desired: [.2, .3], actual: [.1, .2] }), [])
|
||||
assert.deepEqual(gmEventSeries({ times: [0, .1], desired: [.2, Infinity], actual: [.1, .2] }), [])
|
||||
Reference in New Issue
Block a user