-
Notifications
You must be signed in to change notification settings - Fork 0
feat(gateway): /toggle-enforce-branches command (v0.5.34) #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
royosherove
wants to merge
2
commits into
main
Choose a base branch
from
feat/toggle-enforce-branches
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /** | ||
| * gateway/toggle-enforce-branches-command.ts — /toggle-enforce-branches command | ||
| * | ||
| * Toggles the runtime kill-switch for `@inceptionstack/pi-branch-enforcer`. | ||
| * | ||
| * The extension (v3.3.0+) checks for `~/.pi-branch-enforcer/disabled` on every | ||
| * `tool_call`. Creating the file disables enforcement; removing it re-enables. | ||
| * Effect is **immediate** — no agent restart needed; the next bash command | ||
| * sees the new state. | ||
| * | ||
| * Usage: | ||
| * /toggle-enforce-branches → toggle (off→on or on→off) | ||
| * /toggle-enforce-branches on → force enable | ||
| * /toggle-enforce-branches off → force disable | ||
| * /toggle-enforce-branches status → just report current state | ||
| * | ||
| * The command intentionally lives in roundhouse rather than the extension so | ||
| * that the user-facing surface (Telegram) stays in one place. The extension | ||
| * just exposes the marker file as a stable contract. | ||
| */ | ||
|
|
||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs"; | ||
|
|
||
| /** Stable contract with pi-branch-enforcer >=3.3.0. */ | ||
| const MARKER_DIR = join(homedir(), ".pi-branch-enforcer"); | ||
| const MARKER_PATH = join(MARKER_DIR, "disabled"); | ||
|
|
||
| export interface ToggleEnforceBranchesContext { | ||
| thread: any; | ||
| text: string; | ||
| postWithFallback: (thread: any, text: string) => Promise<void>; | ||
| } | ||
|
|
||
| /** True if enforcement is currently disabled (marker file present). */ | ||
| function isDisabled(): boolean { | ||
| try { return existsSync(MARKER_PATH); } catch { return false; } | ||
| } | ||
|
|
||
| /** Create the marker file (disable enforcement). Idempotent. */ | ||
| function disable(): void { | ||
| mkdirSync(MARKER_DIR, { recursive: true }); | ||
| writeFileSync(MARKER_PATH, `disabled at ${new Date().toISOString()}\n`); | ||
| } | ||
|
|
||
| /** Remove the marker file (re-enable enforcement). Idempotent. */ | ||
| function enable(): void { | ||
| try { unlinkSync(MARKER_PATH); } catch (err: any) { | ||
| if (err?.code !== "ENOENT") throw err; | ||
| } | ||
| } | ||
|
|
||
| function statusLine(disabled: boolean): string { | ||
| return disabled | ||
| ? "🔓 *Branch enforcer:* DISABLED — pushes to `main`/`master` are allowed" | ||
| : "🔒 *Branch enforcer:* ENABLED — pushes to `main`/`master` are blocked"; | ||
| } | ||
|
|
||
| export async function handleToggleEnforceBranches(ctx: ToggleEnforceBranchesContext): Promise<void> { | ||
| const { thread, text, postWithFallback } = ctx; | ||
| const arg = text.split(/\s+/)[1]?.toLowerCase() ?? ""; | ||
|
|
||
| // Pure status query — no state change. | ||
| if (arg === "status") { | ||
| await postWithFallback(thread, statusLine(isDisabled())); | ||
| return; | ||
| } | ||
|
|
||
| // Resolve target state. | ||
| const currentlyDisabled = isDisabled(); | ||
| let targetDisabled: boolean; | ||
| if (arg === "on" || arg === "enable") { | ||
| targetDisabled = false; | ||
| } else if (arg === "off" || arg === "disable") { | ||
| targetDisabled = true; | ||
| } else if (arg === "" || arg === "toggle") { | ||
| targetDisabled = !currentlyDisabled; | ||
| } else { | ||
| await postWithFallback( | ||
| thread, | ||
| "Usage: `/toggle-enforce-branches [on|off|status]`\n\n" + | ||
| "_(no arg toggles the current state)_", | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Apply. | ||
| try { | ||
| if (targetDisabled) disable(); else enable(); | ||
| } catch (err) { | ||
| await postWithFallback(thread, `⚠️ Failed to toggle: ${(err as Error).message}`); | ||
| return; | ||
| } | ||
|
|
||
| const noChange = targetDisabled === currentlyDisabled; | ||
| const prefix = noChange ? "ℹ️ Already in target state.\n\n" : "✅ Updated.\n\n"; | ||
| const detail = targetDisabled | ||
| ? "\n\n_Effect:_ next `bash` tool call will skip enforcement. Re-enable with `/toggle-enforce-branches on`." | ||
| : "\n\n_Effect:_ next `bash` tool call will resume enforcement."; | ||
| await postWithFallback(thread, prefix + statusLine(targetDisabled) + detail); | ||
|
|
||
| console.log( | ||
| `[roundhouse] /toggle-enforce-branches: ${currentlyDisabled ? "disabled" : "enabled"} → ` + | ||
| `${targetDisabled ? "disabled" : "enabled"}`, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /** | ||
| * Tests for /toggle-enforce-branches command | ||
| * | ||
| * Strategy: characterization tests against the real filesystem using a | ||
| * dedicated tmp HOME so we don't touch ~/.pi-branch-enforcer/disabled on | ||
| * the dev machine. The handler reads HOME via os.homedir() — which on | ||
| * Linux respects $HOME — so overriding $HOME redirects all marker writes. | ||
| * | ||
| * We import the command module fresh per test (vitest module reset) so | ||
| * the homedir() value captured at module-load time picks up the new $HOME. | ||
| */ | ||
| import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; | ||
| import { mkdtempSync, rmSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { tmpdir, homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
|
|
||
| let tmpHome: string; | ||
| let originalHome: string | undefined; | ||
| let posts: string[]; | ||
| let postWithFallback: (thread: any, text: string) => Promise<void>; | ||
|
|
||
| beforeEach(() => { | ||
| tmpHome = mkdtempSync(join(tmpdir(), "roundhouse-toggle-test-")); | ||
| originalHome = process.env.HOME; | ||
| process.env.HOME = tmpHome; | ||
| posts = []; | ||
| postWithFallback = async (_t, text) => { posts.push(text); }; | ||
| // Force re-resolution of homedir() inside the command module | ||
| vi.resetModules(); | ||
| // Sanity: confirm os.homedir() now returns our tmpHome | ||
| expect(homedir()).toBe(tmpHome); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| if (originalHome !== undefined) process.env.HOME = originalHome; | ||
| else delete process.env.HOME; | ||
| rmSync(tmpHome, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| const markerPath = () => join(tmpHome, ".pi-branch-enforcer", "disabled"); | ||
|
|
||
| async function callHandler(text: string): Promise<void> { | ||
| const mod = await import("../src/gateway/toggle-enforce-branches-command"); | ||
| await mod.handleToggleEnforceBranches({ | ||
| thread: { id: "test" }, | ||
| text, | ||
| postWithFallback, | ||
| }); | ||
| } | ||
|
|
||
| describe("/toggle-enforce-branches", () => { | ||
| test("status_initialState_reportsEnabled", async () => { | ||
| await callHandler("/toggle-enforce-branches status"); | ||
| expect(posts).toHaveLength(1); | ||
| expect(posts[0]).toContain("ENABLED"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| }); | ||
|
|
||
| test("status_markerExists_reportsDisabled", async () => { | ||
| mkdirSync(join(tmpHome, ".pi-branch-enforcer"), { recursive: true }); | ||
| writeFileSync(markerPath(), "test"); | ||
| await callHandler("/toggle-enforce-branches status"); | ||
| expect(posts[0]).toContain("DISABLED"); | ||
| }); | ||
|
|
||
| test("toggle_fromEnabled_disablesAndCreatesMarker", async () => { | ||
| await callHandler("/toggle-enforce-branches"); | ||
| expect(existsSync(markerPath())).toBe(true); | ||
| expect(posts[0]).toContain("DISABLED"); | ||
| expect(posts[0]).toContain("Updated"); | ||
| }); | ||
|
|
||
| test("toggle_fromDisabled_enablesAndRemovesMarker", async () => { | ||
| mkdirSync(join(tmpHome, ".pi-branch-enforcer"), { recursive: true }); | ||
| writeFileSync(markerPath(), "x"); | ||
| await callHandler("/toggle-enforce-branches"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| expect(posts[0]).toContain("ENABLED"); | ||
| }); | ||
|
|
||
| test("explicitOff_whenAlreadyEnabled_disables", async () => { | ||
| await callHandler("/toggle-enforce-branches off"); | ||
| expect(existsSync(markerPath())).toBe(true); | ||
| expect(posts[0]).toContain("DISABLED"); | ||
| }); | ||
|
|
||
| test("explicitOff_whenAlreadyDisabled_isIdempotentNoChange", async () => { | ||
| mkdirSync(join(tmpHome, ".pi-branch-enforcer"), { recursive: true }); | ||
| writeFileSync(markerPath(), "x"); | ||
| await callHandler("/toggle-enforce-branches off"); | ||
| expect(existsSync(markerPath())).toBe(true); | ||
| expect(posts[0]).toContain("Already in target state"); | ||
| expect(posts[0]).toContain("DISABLED"); | ||
| }); | ||
|
|
||
| test("explicitOn_whenDisabled_enables", async () => { | ||
| mkdirSync(join(tmpHome, ".pi-branch-enforcer"), { recursive: true }); | ||
| writeFileSync(markerPath(), "x"); | ||
| await callHandler("/toggle-enforce-branches on"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| expect(posts[0]).toContain("ENABLED"); | ||
| }); | ||
|
|
||
| test("explicitOn_whenAlreadyEnabled_isIdempotentNoChange", async () => { | ||
| await callHandler("/toggle-enforce-branches on"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| expect(posts[0]).toContain("Already in target state"); | ||
| }); | ||
|
|
||
| test("aliases_enableAndDisable_work", async () => { | ||
| await callHandler("/toggle-enforce-branches disable"); | ||
| expect(existsSync(markerPath())).toBe(true); | ||
| posts.length = 0; | ||
| await callHandler("/toggle-enforce-branches enable"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| }); | ||
|
|
||
| test("unknownArg_postsUsageWithoutChanging", async () => { | ||
| await callHandler("/toggle-enforce-branches blah"); | ||
| expect(existsSync(markerPath())).toBe(false); | ||
| expect(posts[0]).toContain("Usage:"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
toggle-enforce-branchesentry uses hyphens, but TelegramBotCommand.commandonly allows lowercase letters, digits, and underscores; whenregisterCommands()callssetMyCommands, Telegram returnsBOT_COMMAND_INVALIDfor this payload, so startup command registration fails and the bot command menu is not updated. This impacts every Telegram deployment that enables command registration, not just this new command, because the API call validates the full command list.Useful? React with 👍 / 👎.