Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ nemohermes my-assistant channels add telegram
| Flag | Description |
|------|-------------|
| `--dry-run` | Validate the channel name and matching policy preset without prompting for credentials, contacting the gateway, or rebuilding |
| `--force` | Add the channel even when another sandbox already uses the same messaging credential, bypassing the cross-sandbox conflict warning (otherwise `channels add` warns and, when non-interactive, aborts) |

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
Optional Slack allowlists come from `SLACK_ALLOWED_USERS` and `SLACK_ALLOWED_CHANNELS` at rebuild time.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,7 @@ $$nemoclaw my-assistant channels add telegram
| Flag | Description |
|------|-------------|
| `--dry-run` | Validate the channel name and matching policy preset without prompting for credentials, contacting the gateway, or rebuilding |
| `--force` | Add the channel even when another sandbox already uses the same messaging credential, bypassing the cross-sandbox conflict warning (otherwise `channels add` warns and, when non-interactive, aborts) |

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
Optional Slack allowlists come from `SLACK_ALLOWED_USERS` and `SLACK_ALLOWED_CHANNELS` at rebuild time.
Expand Down
6 changes: 3 additions & 3 deletions src/commands/sandbox/channels/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
import {
channelMutationOptions,
channelMutationArgs,
channelMutationFlags,
channelAddFlags,
} from "../../../lib/sandbox/channels-command-support";

export default class ChannelsAddCommand extends NemoClawCommand {
static id = "sandbox:channels:add";
static strict = true;
static summary = "Save messaging channel credentials and rebuild";
static description = "Store credentials for a messaging channel and queue a sandbox rebuild.";
static usage = ["<name> <channel> [--dry-run]"];
static usage = ["<name> <channel> [--dry-run] [--force]"];
static examples = ["<%= config.bin %> sandbox channels add alpha telegram"];
static args = channelMutationArgs;
static flags = channelMutationFlags;
static flags = channelAddFlags;

public async run(): Promise<void> {
const { args, flags } = await this.parse(ChannelsAddCommand);
Expand Down
48 changes: 48 additions & 0 deletions src/commands/sandbox/channels/mutate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,40 @@ describe("channels mutation oclif commands", () => {
expect(mocks.addSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: true,
force: false,
});
});

// Scenario 12 (#4305): --force is threaded through to addSandboxChannel.
it("threads --force through add to typed action options", async () => {
await ChannelsAddCommand.run(["alpha", "telegram", "--force"], rootDir);

expect(mocks.addSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: false,
force: true,
});
});

// Scenario 12 (#4305): omitting --force yields force:false (no implicit override).
it("defaults force to false when --force is omitted on add", async () => {
await ChannelsAddCommand.run(["alpha", "telegram"], rootDir);

expect(mocks.addSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: false,
force: false,
});
});

// Scenario 12 (#4305): --force combines with --dry-run independently.
it("threads both --force and --dry-run on add", async () => {
await ChannelsAddCommand.run(["alpha", "telegram", "--force", "--dry-run"], rootDir);

expect(mocks.addSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: true,
force: true,
});
});

Expand All @@ -41,17 +75,31 @@ describe("channels mutation oclif commands", () => {
expect(mocks.removeSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: false,
force: false,
});
expect(mocks.startSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "telegram",
dryRun: true,
force: false,
});
expect(mocks.stopSandboxChannel).toHaveBeenCalledWith("alpha", {
channel: "slack",
dryRun: false,
force: false,
});
});

// Scenario 12 (#4305): --force is add-only. Only `channels add` can create a
// cross-sandbox credential overlap, so only it exposes the override; surfacing
// a no-op --force on remove/start/stop would mislead users and break the
// CLI/docs flag-parity check.
it("exposes --force only on add, not on remove/start/stop", () => {
expect(ChannelsAddCommand.flags).toHaveProperty("force");
expect(ChannelsRemoveCommand.flags).not.toHaveProperty("force");
expect(ChannelsStartCommand.flags).not.toHaveProperty("force");
expect(ChannelsStopCommand.flags).not.toHaveProperty("force");
});

it("requires a channel before dispatch", async () => {
await expect(ChannelsAddCommand.run(["alpha"], rootDir)).rejects.toThrow(/channel/i);

Expand Down
Loading
Loading