Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/action/server_goal_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ class ServerGoalHandle {
* @returns {undefined}
*/
execute(callback) {

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The execute() JSDoc says it “begins execution”, but the implementation now silently returns (and won’t invoke the callback) when the goal is already executing, inactive, or destroyed. Please update the method documentation to reflect these no-op cases so API consumers understand when execute() has an effect.

Copilot uses AI. Check for mistakes.
if (this._destroyed) {
return;
}

// Guard: already executing — no-op
if (this.status === ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
return;
}

// Guard: only transition if goal is still active
if (!this.isActive) {
return;
}

if (!this.isCancelRequested) {
this._updateState(GoalEvent.EXECUTE);
}
Expand All @@ -105,6 +119,15 @@ class ServerGoalHandle {
return;
}

if (this.status !== ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
this._actionServer._node
.getLogger()
.warn(
'publishFeedback() called on a goal handle not in executing state, ignoring'

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning doesn’t include any context (e.g., goal ID and current status), which makes it hard to diagnose which goal is misbehaving when multiple goals are active. Consider including goalId (and/or status) in the log message.

Suggested change
'publishFeedback() called on a goal handle not in executing state, ignoring'
`publishFeedback() called on a goal handle not in executing state (goalId=${JSON.stringify(
this.goalId
)}, status=${this.status}), ignoring`

Copilot uses AI. Check for mistakes.
);
return;
}

let feedbackMessage =
new this._actionServer.typeClass.impl.FeedbackMessage();
feedbackMessage['goal_id'] = this.goalId;
Expand Down
156 changes: 156 additions & 0 deletions test/test-action-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,4 +773,160 @@ describe('rclnodejs action server', function () {
ServiceIntrospectionStates.CONTENTS
);
});

it('publishFeedback on accepted (not executing) goal should be ignored', async function () {
let serverGoalHandle;

function handleAcceptedCallback(goalHandle) {
serverGoalHandle = goalHandle;
}

let server = new rclnodejs.ActionServer(
node,
fibonacci,
'fibonacci',
(goalHandle) => {
goalHandle.succeed();
return new Fibonacci.Result();
},
null,
handleAcceptedCallback
);

let goal = new Fibonacci.Goal();
await client.waitForServer(1000);

let feedbackReceived = false;
const handle = await client.sendGoal(goal, () => {
feedbackReceived = true;
});
assert.ok(handle.accepted);
assert.strictEqual(serverGoalHandle.status, GoalStatus.STATUS_ACCEPTED);

// publishFeedback on accepted goal should not publish
const feedback = new Fibonacci.Feedback();
feedback.sequence = [1, 1, 2, 3];
serverGoalHandle.publishFeedback(feedback);

await assertUtils.createDelay(200);
assert.strictEqual(feedbackReceived, false);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test adds a 200ms delay to check for absence of feedback; similar feedback tests in this file use shorter delays (e.g., 50ms). Consider reducing this delay (or reusing a shared constant) to keep the suite fast while still being reliable.

Copilot uses AI. Check for mistakes.

// Clean up: execute and succeed
serverGoalHandle.execute();
await handle.getResult();
server.destroy();
});

it('publishFeedback on executing goal should publish normally', async function () {
const sequence = [1, 1, 2, 3];

function executeFeedbackCallback(goalHandle) {
assert.strictEqual(goalHandle.status, GoalStatus.STATUS_EXECUTING);

const feedback = new Fibonacci.Feedback();
feedback.sequence = sequence;
goalHandle.publishFeedback(feedback);
goalHandle.succeed();

return new Fibonacci.Result();
}

let server = new rclnodejs.ActionServer(
node,
fibonacci,
'fibonacci',
executeFeedbackCallback
);

let goal = new Fibonacci.Goal();
await client.waitForServer(1000);

let feedbackMessage;
const handle = await client.sendGoal(
goal,
(feedback) => (feedbackMessage = feedback)
);
assert.ok(handle.accepted);

await assertUtils.createDelay(200);
assert.ok(feedbackMessage);
assert.ok(deepEqual(Int32Array.from(sequence), feedbackMessage.sequence));

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test adds a 200ms delay before asserting feedback was received; earlier feedback tests in this file use ~50ms. Consider lowering the delay (or using a shared helper/constant) to reduce overall test runtime.

Copilot uses AI. Check for mistakes.

server.destroy();
});

it('publishFeedback after succeed should be ignored', async function () {
let serverGoalHandle;

function executeCallback(goalHandle) {
serverGoalHandle = goalHandle;
goalHandle.succeed();
return new Fibonacci.Result();
}

let server = new rclnodejs.ActionServer(
node,
fibonacci,
'fibonacci',
executeCallback
);

let goal = new Fibonacci.Goal();
await client.waitForServer(1000);

let feedbackCount = 0;
const handle = await client.sendGoal(goal, () => {
feedbackCount++;
});
assert.ok(handle.accepted);

await handle.getResult();
assert.strictEqual(serverGoalHandle.status, GoalStatus.STATUS_SUCCEEDED);

// publishFeedback after terminal state should not publish
const feedback = new Fibonacci.Feedback();
feedback.sequence = [1, 1, 2, 3];
serverGoalHandle.publishFeedback(feedback);

await assertUtils.createDelay(200);
assert.strictEqual(feedbackCount, 0);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses a 200ms delay to confirm feedback isn’t published after a terminal state; consider reducing it (or using a shared constant) for faster, more consistent test timing across the suite.

Copilot uses AI. Check for mistakes.

server.destroy();
});

it('execute() on already executing goal should be a no-op', async function () {
let executeCalled = 0;

function handleAcceptedCallback(goalHandle) {
goalHandle.execute();
// Call execute again — should be a no-op
goalHandle.execute();
}

function executeCallback(goalHandle) {
executeCalled++;
goalHandle.succeed();
return new Fibonacci.Result();
}

let server = new rclnodejs.ActionServer(
node,
fibonacci,
'fibonacci',
executeCallback,
null,
handleAcceptedCallback
);

let goal = new Fibonacci.Goal();
await client.waitForServer(1000);

const handle = await client.sendGoal(goal);
assert.ok(handle.accepted);

await handle.getResult();
assert.strictEqual(executeCalled, 1);

server.destroy();
});
});
Loading