Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,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