diff --git a/forge/ee/routes/teamBroker/3rdPartyBroker.js b/forge/ee/routes/teamBroker/3rdPartyBroker.js index e114ae662a..ac51c68577 100644 --- a/forge/ee/routes/teamBroker/3rdPartyBroker.js +++ b/forge/ee/routes/teamBroker/3rdPartyBroker.js @@ -447,10 +447,18 @@ module.exports = async function (app) { } else if (agent?.state === 'starting') { // Another start is already launching the agent: starting is a no-op reply.status(200).send({}) - } else if (agent && agent.state !== 'running') { - await app.containers.sendBrokerAgentCommand(agent, 'start') + } else if (agent) { + // The agent is up. The stored 'running' state doesn't tell us whether + // it's actually collecting, so ask the agent. Only send start when it + // isn't connected (stopped via the API or after the 24h auto-shutdown), + // since start fully tears the agent down and reconnects it, which would + // take a healthy agent offline and drop in-flight messages. + const status = await app.containers.getBrokerAgentState(agent) + if (!status?.connected) { + await app.containers.sendBrokerAgentCommand(agent, 'start') + } reply.status(200).send({}) - } else if (!agent) { + } else { // Stays 'starting' until the driver has launched the agent, so a suspend // arriving meanwhile is refused rather than missing a process it can't stop yet const agent = await app.db.models.TeamBrokerAgent.create({ @@ -467,9 +475,6 @@ module.exports = async function (app) { agent.state = 'running' await agent.save() reply.status(200).send({}) - } else { - // Agent already running: starting is a no-op - reply.status(200).send({}) } } else { if (request.broker.state === 'running') { diff --git a/test/unit/forge/ee/routes/teamBroker/3rdPartyBroker_spec.js b/test/unit/forge/ee/routes/teamBroker/3rdPartyBroker_spec.js index 17fd0368fa..826ef6894c 100644 --- a/test/unit/forge/ee/routes/teamBroker/3rdPartyBroker_spec.js +++ b/test/unit/forge/ee/routes/teamBroker/3rdPartyBroker_spec.js @@ -781,14 +781,47 @@ describe('3rd Party Broker API', function () { await app.db.models.TeamBrokerAgent.destroy({ where: { TeamId: app.team.id } }) }) - it('start replies when the team-broker agent is already running', async function () { + it('start resumes a team-broker agent that is up but no longer collecting', async function () { + // After a stop or the 24h auto-shutdown the agent stays recorded as running + // but is no longer connected, so switching the toggle on must re-issue the + // start command to resume collection. + const agent = await app.db.models.TeamBrokerAgent.create({ state: 'running', TeamId: app.team.id }) + const getBrokerAgentState = sinon.stub(app.containers, 'getBrokerAgentState').resolves({ connected: false, error: null }) + const sendBrokerAgentCommand = sinon.stub(app.containers, 'sendBrokerAgentCommand').resolves() + try { + const response = await app.inject({ + method: 'POST', + url: `/api/v1/teams/${app.team.hashid}/brokers/team-broker/start`, + cookies: { sid: TestObjects.tokens.bob } + }) + response.statusCode.should.equal(200) + sendBrokerAgentCommand.calledOnce.should.be.true() + sendBrokerAgentCommand.firstCall.args[0].should.have.property('id', agent.id) + sendBrokerAgentCommand.firstCall.args[1].should.equal('start') + } finally { + sendBrokerAgentCommand.restore() + getBrokerAgentState.restore() + } + }) + + it('start leaves a team-broker agent that is already collecting alone', async function () { + // A connected agent is already collecting; sending it start would tear the + // broker down and reconnect, so this must stay a no-op. await app.db.models.TeamBrokerAgent.create({ state: 'running', TeamId: app.team.id }) - const response = await app.inject({ - method: 'POST', - url: `/api/v1/teams/${app.team.hashid}/brokers/team-broker/start`, - cookies: { sid: TestObjects.tokens.bob } - }) - response.statusCode.should.equal(200) + const getBrokerAgentState = sinon.stub(app.containers, 'getBrokerAgentState').resolves({ connected: true, error: null }) + const sendBrokerAgentCommand = sinon.stub(app.containers, 'sendBrokerAgentCommand').resolves() + try { + const response = await app.inject({ + method: 'POST', + url: `/api/v1/teams/${app.team.hashid}/brokers/team-broker/start`, + cookies: { sid: TestObjects.tokens.bob } + }) + response.statusCode.should.equal(200) + sendBrokerAgentCommand.called.should.be.false() + } finally { + sendBrokerAgentCommand.restore() + getBrokerAgentState.restore() + } }) it('stop replies when the team-broker agent is not running', async function () {