mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
fix: report accurate is_running after trader update to stop redundant start
Live verification of the relaunch (update) branch surfaced this: the update response omitted is_running, so the launcher fired a redundant start that got 400 'Trader is already running', showing an error toast and skipping the dashboard navigation even though the relaunch succeeded. - PUT /traders/:id now reports is_running (a running trader is restarted with the new config, so it stays running). - The already-running rejection carries error_key trader.start.already_running and the shared launcher treats it as success (launch is idempotent).
This commit is contained in:
@@ -731,7 +731,10 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
"trader_id": traderID,
|
"trader_id": traderID,
|
||||||
"trader_name": req.Name,
|
"trader_name": req.Name,
|
||||||
"ai_model": req.AIModelID,
|
"ai_model": req.AIModelID,
|
||||||
"message": "Trader updated successfully",
|
// A running trader is restarted with the new config (async above), so
|
||||||
|
// report it as running — callers must not fire a redundant start.
|
||||||
|
"is_running": wasRunning,
|
||||||
|
"message": "Trader updated successfully",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,7 +795,10 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
|||||||
if existingTrader != nil {
|
if existingTrader != nil {
|
||||||
status := existingTrader.GetStatus()
|
status := existingTrader.GetStatus()
|
||||||
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already running"})
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Trader is already running",
|
||||||
|
"error_key": "trader.start.already_running",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Trader exists but is stopped - remove from memory to reload fresh config
|
// Trader exists but is stopped - remove from memory to reload fresh config
|
||||||
|
|||||||
@@ -129,6 +129,33 @@ describe('launchAutopilot', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('treats a racing already-running rejection as success', async () => {
|
||||||
|
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||||
|
mocks.api.getTraders.mockResolvedValue([
|
||||||
|
{ trader_id: 't-1', trader_name: 'NOFX Autopilot', is_running: false },
|
||||||
|
])
|
||||||
|
mocks.api.updateTrader.mockResolvedValue({
|
||||||
|
trader_id: 't-1',
|
||||||
|
is_running: false,
|
||||||
|
})
|
||||||
|
mocks.api.startTrader.mockRejectedValue(
|
||||||
|
new ApiError(
|
||||||
|
'Trader is already running',
|
||||||
|
'trader.start.already_running',
|
||||||
|
undefined,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const outcome = await launchAutopilot({
|
||||||
|
ensureStrategy: vi.fn().mockResolvedValue('strat-1'),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(outcome).toEqual(
|
||||||
|
expect.objectContaining({ ok: true, traderId: 't-1' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('surfaces the server-side preflight result when start is rejected', async () => {
|
it('surfaces the server-side preflight result when start is rejected', async () => {
|
||||||
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||||
mocks.api.startTrader.mockRejectedValue(
|
mocks.api.startTrader.mockRejectedValue(
|
||||||
|
|||||||
@@ -101,7 +101,16 @@ export async function launchAutopilot(
|
|||||||
: await api.createTrader(traderRequest)
|
: await api.createTrader(traderRequest)
|
||||||
|
|
||||||
if (!autopilot.is_running) {
|
if (!autopilot.is_running) {
|
||||||
await api.startTrader(autopilot.trader_id)
|
try {
|
||||||
|
await api.startTrader(autopilot.trader_id)
|
||||||
|
} catch (err) {
|
||||||
|
// Launch is idempotent: the update path restarts a running trader
|
||||||
|
// asynchronously, so a racing "already running" rejection is success.
|
||||||
|
const alreadyRunning =
|
||||||
|
err instanceof ApiError &&
|
||||||
|
err.errorKey === 'trader.start.already_running'
|
||||||
|
if (!alreadyRunning) throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user