mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-15 12:04:00 +08:00
Preserve standard OS updates and rollback in version picker
This commit is contained in:
committed by
firestar5683
parent
f88ef758ca
commit
4d585dbcbc
@@ -309,8 +309,11 @@ export const SystemTools = {
|
||||
try {
|
||||
const selected = this.versionCommits.find(item => item.sha === commit)
|
||||
const version = commit === "latest" ? "Latest" : `${versionTitle(selected, branch === "StarPilot")}\nCommit: ${commit}`
|
||||
const policy = commit === "latest" ? "Automatic updates will remain off after installation. You can enable them in settings." : "Automatic updates will be paused for this earlier version."
|
||||
if (!(await GalaxyConfirm({title: "Install selected version?", message: `Branch: ${branch}\nVersion: ${version}\n\n${policy}\n\nThis replaces the current software. Settings and statistics are kept, and local code changes are backed up. Older versions may remove this picker; an SSH recovery copy is saved on the device.\n\nYour device will reboot when installation finishes.`, confirmLabel: "Install & Reboot", danger: true}))) return
|
||||
const policy = commit === "latest" ? "This uses the normal branch updater, including any required OS update during startup. Your automatic-update setting is unchanged." : "Automatic updates will be paused for this earlier version."
|
||||
const recovery = commit === "latest"
|
||||
? "This replaces the current software. Settings and statistics are kept. Local code changes may be overwritten; standard Rollback saves the previous committed version."
|
||||
: "This replaces the current software. Settings and statistics are kept, and local code changes are backed up. Older versions may remove this picker; an SSH recovery copy is saved on the device."
|
||||
if (!(await GalaxyConfirm({title: "Install selected version?", message: `Branch: ${branch}\nVersion: ${version}\n\n${policy}\n\n${recovery}\n\nYour device will reboot when installation finishes.`, confirmLabel: "Install & Reboot", danger: true}))) return
|
||||
// Refresh driving/updater state after the user has reviewed the target.
|
||||
await this.loadFastStatus({throwOnError: true})
|
||||
if (this.branchSwitchBlocked || this.targetBranch !== branch || generation !== this.versionGeneration) {
|
||||
|
||||
@@ -139,7 +139,7 @@ test('return to Latest uses pinned branch and confirms without changing automati
|
||||
instance.fastStatus = {versionPin: {branch: 'feature/test', commit: SHA, installedAt: '2026-09-10'}}
|
||||
await instance.returnToLatest()
|
||||
assert.deepEqual(calls, [{branch: 'feature/test', commit: 'latest'}])
|
||||
assert.ok(confirmations[0].message.includes('remain off'))
|
||||
assert.ok(confirmations[0].message.includes('automatic-update setting is unchanged'))
|
||||
})
|
||||
test('API preserves branch encoding, pagination head, abort signal and confirmed install body', async () => {
|
||||
const calls = []
|
||||
@@ -265,3 +265,18 @@ test('changing branch during a multi-page release search cancels remaining pages
|
||||
instance.selectTargetBranch('Dom');finish();await pending
|
||||
assert.equal(requests,2);assert.equal(instance.versionCommits.length,0);assert.equal(instance.versionLoading,false)
|
||||
})
|
||||
|
||||
test('Latest explains normal OS handling and local-edit behavior; historical confirmation keeps recovery details', async () => {
|
||||
const {instance, confirmations} = fixture()
|
||||
instance.selectTargetBranch('Dom')
|
||||
await instance.installSelectedVersion()
|
||||
assert.match(confirmations[0].message, /normal branch updater/)
|
||||
assert.match(confirmations[0].message, /required OS update/)
|
||||
assert.match(confirmations[0].message, /Local code changes may be overwritten/)
|
||||
assert.doesNotMatch(confirmations[0].message, /code changes are backed up/)
|
||||
await instance.onVersionModeSelect({target:{value:'earlier'}})
|
||||
instance.selectedCommit=SHA
|
||||
await instance.installSelectedVersion()
|
||||
assert.match(confirmations[1].message, /updates will be paused/)
|
||||
assert.match(confirmations[1].message, /code changes are backed up/)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Real local Git update/rollback regressions. No device, OS flashing or reboot."""
|
||||
import ast
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
import pytest
|
||||
from test_version_routes import server
|
||||
from test_version_install_rehearsal import rehearsal, git, load_module, write
|
||||
|
||||
MODULE_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
def unpack(state):
|
||||
return (getattr(state, name) for name in ('root','repo','origin','old','latest','data','installer','history'))
|
||||
|
||||
def test_latest_uses_existing_branch_worker_without_historical_restrictions(server):
|
||||
_, ns = server
|
||||
ns['_branch_switch_worker'] = Mock()
|
||||
ns['version_history'].resolve_version.side_effect = AssertionError('Latest must not require history API')
|
||||
ns['_version_install_worker']('StarPilot', 'latest')
|
||||
ns['_branch_switch_worker'].assert_called_once_with('StarPilot')
|
||||
ns['version_install'].install.assert_not_called()
|
||||
ns['_set_fast_update_error_state'].assert_not_called()
|
||||
|
||||
def legacy_namespace(repo, data, installer):
|
||||
def run(repo, args, **_):
|
||||
return subprocess.run(['git','-C',str(repo),*args], capture_output=True, text=True)
|
||||
def stdout(repo, args, **_):
|
||||
return git(repo, *args)
|
||||
def config(repo, key):
|
||||
return run(repo, ['config','--get',key]).stdout.strip()
|
||||
def fetch(repo, args, **_):
|
||||
result=run(repo,args)
|
||||
return result.returncode, result.stderr
|
||||
params=SimpleNamespace(get_bool=lambda key:(data/'params/d'/key).read_bytes()==b'1',
|
||||
put_bool=lambda key,value:write(data,'params/d/'+key,'1' if value else '0'))
|
||||
app=Flask(__name__)
|
||||
ns=dict(app=app,request=request,jsonify=jsonify,params=params,time=time,datetime=datetime,timezone=timezone,
|
||||
threading=SimpleNamespace(Thread=Mock()), _fast_update_lock=threading.Lock(), _fast_update_state={'running':False},
|
||||
_FAST_UPDATE_TOTAL_STEPS=5, _FAST_BRANCH_SWITCH_FETCH_TIMEOUT_S=20, _FAST_ROLLBACK_FETCH_TIMEOUT_S=20,
|
||||
_ROLLBACK_REF='refs/starpilot/rollback', _ROLLBACK_BRANCH_CONFIG_KEY='starpilot.rollbackbranch',
|
||||
_ROLLBACK_RECORDED_AT_CONFIG_KEY='starpilot.rollbackrecordedat',
|
||||
_get_openpilot_root=lambda:repo, _run_git=run, _git_stdout=stdout,
|
||||
_git_config_get=config, _git_config_set=lambda repo,key,value:git(repo,'config',key,value),
|
||||
_git_config_unset=lambda repo,key:run(repo,['config','--unset',key]),
|
||||
_git_update_ref=lambda repo,ref,sha:git(repo,'update-ref',ref,sha),
|
||||
_git_delete_ref=lambda repo,ref:git(repo,'update-ref','-d',ref),
|
||||
_git_has_commit=lambda repo,sha:run(repo,['cat-file','-e',sha+'^{commit}']).returncode==0,
|
||||
_build_shallow_fetch_args=lambda branch:['fetch','--depth=1','origin',branch],
|
||||
_build_shallow_fetch_commit_args=lambda sha:['fetch','--depth=1','origin',sha],
|
||||
_run_git_with_progress=fetch, _clear_generated_build_state=Mock(), _run_submodule_update_if_needed=Mock(),
|
||||
_set_fast_update_state=Mock(), _set_fast_update_progress=Mock(), _set_fast_update_error_state=Mock(),
|
||||
_finish_update_and_reboot=Mock(), version_install=SimpleNamespace(clear_pin=lambda:installer.clear_pin(data)))
|
||||
names={'_is_valid_git_branch_name','_save_rollback_target','_load_rollback_target','_clear_rollback_target',
|
||||
'_branch_switch_worker','_rollback_worker','run_update_rollback'}
|
||||
for node in ast.walk(ast.parse((MODULE_DIR/'the_galaxy.py').read_text())):
|
||||
if isinstance(node,ast.FunctionDef) and node.name in names:
|
||||
exec(compile(ast.Module(body=[node],type_ignores=[]),'the_galaxy.py','exec'),ns)
|
||||
return ns, app.test_client()
|
||||
|
||||
def test_latest_changed_os_still_uses_standard_update_and_rolls_back(rehearsal):
|
||||
root, repo, origin, old, latest, data, installer, history = unpack(rehearsal)
|
||||
seed=root/'seed'
|
||||
git(seed,'checkout','-b','StarPilot')
|
||||
write(seed,'launch_env.sh','export AGNOS_VERSION="99.0"\n')
|
||||
write(seed,'system/hardware/tici/agnos.json','[{"name":"system","hash":"new-os"}]\n')
|
||||
git(seed,'add','.');git(seed,'commit','-m','Different OS requirement')
|
||||
target=git(seed,'rev-parse','HEAD')
|
||||
git(seed,'push',str(origin),'StarPilot')
|
||||
write(data,'starpilot/version_selection.json',json.dumps({'branch':'Dom','commit':latest}))
|
||||
ns, client=legacy_namespace(repo,data,installer)
|
||||
ns['_branch_switch_worker']('StarPilot')
|
||||
ns['_set_fast_update_error_state'].assert_not_called()
|
||||
assert git(repo,'rev-parse','HEAD')==target
|
||||
assert '99.0' in (repo/'launch_env.sh').read_text()
|
||||
assert not (data/'starpilot/version_selection.json').exists()
|
||||
assert (data/'params/d/AutomaticUpdates').read_bytes()==b'1'
|
||||
assert ns['_load_rollback_target'](repo)['rollbackCommit']==latest
|
||||
ns['_finish_update_and_reboot'].assert_called_once()
|
||||
assert client.post('/api/update/rollback').status_code==202
|
||||
assert (data/'params/d/AutomaticUpdates').read_bytes()==b'0'
|
||||
ns['_rollback_worker']()
|
||||
ns['_set_fast_update_error_state'].assert_not_called()
|
||||
assert git(repo,'rev-parse','HEAD')==latest
|
||||
assert git(repo,'branch','--show-current')=='Dom'
|
||||
assert ns['_load_rollback_target'](repo)['rollbackAvailable'] is False
|
||||
assert (data/'params/d/ExampleSetting').read_text()=='preserve me'
|
||||
|
||||
def test_historical_install_records_previous_commit_for_existing_rollback(rehearsal):
|
||||
root, repo, origin, old, latest, data, installer, history = unpack(rehearsal)
|
||||
ns, client=legacy_namespace(repo,data,installer)
|
||||
git(repo,'fetch','--depth=1','origin',old)
|
||||
installer.install(repo,{'branch':'Dom','commit':old,'pinned':True},data_root=data,
|
||||
check_parked=lambda:installer.require_parked(data),progress=lambda *args:None)
|
||||
ns['_save_rollback_target'](repo,'Dom',latest)
|
||||
assert ns['_load_rollback_target'](repo)['rollbackAvailable'] is True
|
||||
assert client.post('/api/update/rollback').status_code==202
|
||||
ns['_rollback_worker']()
|
||||
ns['_set_fast_update_error_state'].assert_not_called()
|
||||
assert git(repo,'rev-parse','HEAD')==latest
|
||||
assert not (data/'starpilot/version_selection.json').exists()
|
||||
assert (data/'params/d/AutomaticUpdates').read_bytes()==b'0'
|
||||
|
||||
def test_rollback_remains_blocked_while_driving_or_updating(rehearsal):
|
||||
root, repo, origin, old, latest, data, installer, history = unpack(rehearsal)
|
||||
git(repo,'fetch','--depth=1','origin',old)
|
||||
ns,client=legacy_namespace(repo,data,installer)
|
||||
ns['_save_rollback_target'](repo,'Dom',old)
|
||||
write(data,'params/d/IsOnroad','1')
|
||||
assert client.post('/api/update/rollback').status_code==409
|
||||
write(data,'params/d/IsOnroad','0')
|
||||
ns['_fast_update_state']['running']=True
|
||||
assert client.post('/api/update/rollback').status_code==409
|
||||
ns['threading'].Thread.assert_not_called()
|
||||
assert git(repo,'rev-parse','HEAD')==latest
|
||||
@@ -123,3 +123,10 @@ def test_history_has_no_artificial_page_depth_cutoff(server):
|
||||
reply=client.get('/api/update/versions?branch=Dom&page=1201&head='+'b'*40)
|
||||
assert reply.status_code==200
|
||||
ns['version_history'].list_versions.assert_called_once_with('/repo','Dom',page=1201,head='b'*40)
|
||||
|
||||
def test_historical_worker_records_pre_install_branch_and_commit_for_rollback(server):
|
||||
_, ns = server
|
||||
ns['_git_stdout'].side_effect=['a'*40, 'feature/test', 'b'*40]
|
||||
ns['_version_install_worker']('Dom','a'*40)
|
||||
ns['_save_rollback_target'].assert_called_once_with('/repo','feature/test','b'*40)
|
||||
ns['_set_fast_update_error_state'].assert_not_called()
|
||||
|
||||
@@ -3021,6 +3021,12 @@ def _version_install_worker(branch, selection):
|
||||
installed = None
|
||||
try:
|
||||
version_install.require_parked()
|
||||
if selection == "latest":
|
||||
# Preserve the existing branch updater, including boot-time AGNOS handling.
|
||||
# Historical compatibility and GitHub-history restrictions apply only to
|
||||
# exact revisions, never to an ordinary update to the latest branch head.
|
||||
_branch_switch_worker(branch)
|
||||
return
|
||||
_set_fast_update_progress(1, "Resolving selected version", 10.0, branch)
|
||||
target = version_history.resolve_version(repo_path, branch, selection)
|
||||
version_install.require_parked()
|
||||
@@ -3167,6 +3173,7 @@ def _branch_switch_worker(target_branch):
|
||||
_set_fast_update_progress(3, "Switching branch", 100.0, f"Now on '{target_branch}'.")
|
||||
|
||||
_run_submodule_update_if_needed(repo_path, step=4)
|
||||
version_install.clear_pin()
|
||||
_finish_update_and_reboot(
|
||||
f"Switched to '{target_branch}'. Device is rebooting now. Please wait for reconnection."
|
||||
)
|
||||
@@ -3236,6 +3243,7 @@ def _rollback_worker():
|
||||
_set_fast_update_progress(3, "Applying rollback target", 100.0, f"Now on {target_branch} @ {short_commit}.")
|
||||
|
||||
_run_submodule_update_if_needed(repo_path, step=4)
|
||||
version_install.clear_pin()
|
||||
try:
|
||||
_clear_rollback_target(repo_path)
|
||||
except Exception as exception:
|
||||
|
||||
@@ -83,6 +83,11 @@ def validate_target(target):
|
||||
raise InstallError('An exact validated commit and branch are required')
|
||||
|
||||
|
||||
def clear_pin(data_root=Path('/data')):
|
||||
"""Normal branch updates and rollback supersede a historical selection."""
|
||||
(Path(data_root) / 'starpilot/version_selection.json').unlink(missing_ok=True)
|
||||
|
||||
|
||||
def read_pin(repo, data_root=Path('/data')):
|
||||
try:
|
||||
value = json.loads((Path(data_root) / 'starpilot/version_selection.json').read_text())
|
||||
|
||||
Reference in New Issue
Block a user