From 0640e3b584ff9fa950e866621f0ec17eca49d2fb Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 19:52:10 -0700 Subject: [PATCH 1/4] test(permissions): expect /yolo off hint on default-source skip warning --- src/permission/saved-skip-warning.test.ts | 25 +++++++++++++++++++ .../wiring.skip-permissions-warning.test.ts | 11 ++++---- tests/unit/exec/runner.test.ts | 9 ++++--- 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 src/permission/saved-skip-warning.test.ts diff --git a/src/permission/saved-skip-warning.test.ts b/src/permission/saved-skip-warning.test.ts new file mode 100644 index 000000000..f81ccb379 --- /dev/null +++ b/src/permission/saved-skip-warning.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { globalSettingsPath } from "../config/settings.js"; +import { savedSkipPermissionsWarning } from "./saved-skip-warning.js"; + +describe("savedSkipPermissionsWarning", () => { + test("default machine-wide source appends the /yolo off hint", () => { + const source = globalSettingsPath(); + + const warning = savedSkipPermissionsWarning(source); + + expect(warning).toContain(source); + expect(warning).toContain("edit that file to re-enable"); + expect(warning).toContain("/yolo off"); + }); + + test("custom source keeps file-path wording with no false provenance", () => { + const warning = savedSkipPermissionsWarning( + "/tmp/custom-corbits-settings.json", + ); + + expect(warning).toContain("/tmp/custom-corbits-settings.json"); + expect(warning).toContain("edit that file to re-enable"); + expect(warning).not.toMatch(/machine-wide|saved default|\/yolo off/i); + }); +}); diff --git a/src/tui/runner/wiring.skip-permissions-warning.test.ts b/src/tui/runner/wiring.skip-permissions-warning.test.ts index d5fa9962a..c2e0863f1 100644 --- a/src/tui/runner/wiring.skip-permissions-warning.test.ts +++ b/src/tui/runner/wiring.skip-permissions-warning.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { globalSettingsPath } from "../../config/settings.js"; import { createAppShell } from "../shell/index.js"; import { shellInternals } from "../shell/internals.js"; import { withTestRenderer } from "../harness.js"; @@ -34,12 +35,12 @@ describe("saved skip-permissions startup warning", () => { expect(warning).not.toMatch(/machine-wide|saved default|\/yolo off/i); }); - test("identifies the default settings path", async () => { - const warning = await surfacedWarning( - "/home/operator/.corbits/settings.json", - ); + test("appends the /yolo off hint for the default settings path", async () => { + const source = globalSettingsPath(); + const warning = await surfacedWarning(source); - expect(warning).toContain("/home/operator/.corbits/settings.json"); + expect(warning).toContain(source); expect(warning).toContain("edit that file to re-enable"); + expect(warning).toContain("/yolo off"); }); }); diff --git a/tests/unit/exec/runner.test.ts b/tests/unit/exec/runner.test.ts index 264f9382f..2c7a05650 100644 --- a/tests/unit/exec/runner.test.ts +++ b/tests/unit/exec/runner.test.ts @@ -470,7 +470,11 @@ describe("runExec", () => { async () => { const { runExec: runExecUnderMock } = await import("../../../src/exec/runner.js"); - const defaultSettingsPath = join(home, "settings.json"); + const defaultSettingsPath = join( + home, + ".corbits", + "settings.json", + ); const result = await runExecUnderMock({ ...bareConfig("do the thing"), cwd, @@ -487,9 +491,8 @@ describe("runExec", () => { ); const stderrOutput = stderrChunks.join(""); expect(stderrOutput).toContain( - `Warning: permission prompts are disabled by saved settings at ${defaultSettingsPath}; edit that file to re-enable.\n`, + `Warning: permission prompts are disabled by saved settings at ${defaultSettingsPath}; edit that file to re-enable (/yolo off to re-enable).\n`, ); - expect(stderrOutput).not.toContain("/yolo"); expect(stderrOutput).toMatch(/runtime dispose failed/i); expect(disposeCalls).toBe(1); expect(getActiveDisposeHost()).toBeNull(); From 0bc1e5d6615f123a90bb34f6840c22b4f21bdaa2 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 19:52:50 -0700 Subject: [PATCH 2/4] fix(permissions): restore /yolo off hint for default-source skip warning --- src/permission/saved-skip-warning.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/permission/saved-skip-warning.ts b/src/permission/saved-skip-warning.ts index 307357acf..9bd0dd18d 100644 --- a/src/permission/saved-skip-warning.ts +++ b/src/permission/saved-skip-warning.ts @@ -1,5 +1,15 @@ +import { globalSettingsPath as defaultGlobalSettingsPath } from "../config/settings.js"; + +function isDefaultSettingsSource(sourcePath: string): boolean { + return sourcePath === defaultGlobalSettingsPath(); +} + export function savedSkipPermissionsWarning( globalSettingsPath: string, ): string { - return `Warning: permission prompts are disabled by saved settings at ${globalSettingsPath}; edit that file to re-enable.`; + const base = `Warning: permission prompts are disabled by saved settings at ${globalSettingsPath}; edit that file to re-enable`; + if (isDefaultSettingsSource(globalSettingsPath)) { + return `${base} (/yolo off to re-enable).`; + } + return `${base}.`; } From 2bcfd8558de9e096e757959fc613148fef3e5f13 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:06:15 -0700 Subject: [PATCH 3/4] fix(permissions): classify symlinked default settings source via path identity --- src/permission/saved-skip-warning.test.ts | 20 ++++++++++++++++++-- src/permission/saved-skip-warning.ts | 10 ++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/permission/saved-skip-warning.test.ts b/src/permission/saved-skip-warning.test.ts index f81ccb379..188a07b98 100644 --- a/src/permission/saved-skip-warning.test.ts +++ b/src/permission/saved-skip-warning.test.ts @@ -1,10 +1,13 @@ import { describe, expect, test } from "bun:test"; -import { globalSettingsPath } from "../config/settings.js"; +import { mkdtemp, symlink } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { SETTINGS_DIR_NAME } from "../branding.js"; import { savedSkipPermissionsWarning } from "./saved-skip-warning.js"; describe("savedSkipPermissionsWarning", () => { test("default machine-wide source appends the /yolo off hint", () => { - const source = globalSettingsPath(); + const source = join(homedir(), SETTINGS_DIR_NAME, "settings.json"); const warning = savedSkipPermissionsWarning(source); @@ -13,6 +16,19 @@ describe("savedSkipPermissionsWarning", () => { expect(warning).toContain("/yolo off"); }); + test("symlinked-home alias of the default source still appends the hint", async () => { + const sandbox = await mkdtemp(join(tmpdir(), "corbits-skip-warning-")); + const homeLink = join(sandbox, "home"); + await symlink(homedir(), homeLink); + const aliased = join(homeLink, SETTINGS_DIR_NAME, "settings.json"); + + const warning = savedSkipPermissionsWarning(aliased); + + expect(warning).toContain(aliased); + expect(warning).toContain("edit that file to re-enable"); + expect(warning).toContain("/yolo off"); + }); + test("custom source keeps file-path wording with no false provenance", () => { const warning = savedSkipPermissionsWarning( "/tmp/custom-corbits-settings.json", diff --git a/src/permission/saved-skip-warning.ts b/src/permission/saved-skip-warning.ts index 9bd0dd18d..d147ae436 100644 --- a/src/permission/saved-skip-warning.ts +++ b/src/permission/saved-skip-warning.ts @@ -1,7 +1,13 @@ -import { globalSettingsPath as defaultGlobalSettingsPath } from "../config/settings.js"; +import { + globalSettingsPath as defaultGlobalSettingsPath, + isProgrammaticSettingsOverride, +} from "../config/settings.js"; function isDefaultSettingsSource(sourcePath: string): boolean { - return sourcePath === defaultGlobalSettingsPath(); + return !isProgrammaticSettingsOverride( + sourcePath, + defaultGlobalSettingsPath(), + ); } export function savedSkipPermissionsWarning( From 920d2c510fe4285e15c1062be2343d4996f71a90 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 26 Sep 2026 08:38:03 -0700 Subject: [PATCH 4/4] fix(permissions): omit /yolo hint from exec skip-permissions warning Exec cannot run slash commands, so the recovery hint is TUI-only. Default-source exec stays path-only; custom sources stay path-only. --- src/exec/runner.ts | 2 +- src/permission/saved-skip-warning.test.ts | 47 ++++++++++++++++++----- src/permission/saved-skip-warning.ts | 3 +- src/tui/runner/wiring.ts | 2 +- tests/unit/exec/runner.test.ts | 3 +- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/exec/runner.ts b/src/exec/runner.ts index bbb3f2a39..74da365cd 100644 --- a/src/exec/runner.ts +++ b/src/exec/runner.ts @@ -611,7 +611,7 @@ export async function runExec(config: Config): Promise { if (config.skipPermissionsFromSettings) { stderr.write( - `${savedSkipPermissionsWarning(config.globalSettingsPath)}\n`, + `${savedSkipPermissionsWarning(config.globalSettingsPath, "exec")}\n`, ); } diff --git a/src/permission/saved-skip-warning.test.ts b/src/permission/saved-skip-warning.test.ts index 188a07b98..0c6b47a29 100644 --- a/src/permission/saved-skip-warning.test.ts +++ b/src/permission/saved-skip-warning.test.ts @@ -6,36 +6,65 @@ import { SETTINGS_DIR_NAME } from "../branding.js"; import { savedSkipPermissionsWarning } from "./saved-skip-warning.js"; describe("savedSkipPermissionsWarning", () => { - test("default machine-wide source appends the /yolo off hint", () => { + test("TUI default machine-wide source appends the /yolo off hint", () => { const source = join(homedir(), SETTINGS_DIR_NAME, "settings.json"); - const warning = savedSkipPermissionsWarning(source); + const warning = savedSkipPermissionsWarning(source, "tui"); expect(warning).toContain(source); expect(warning).toContain("edit that file to re-enable"); expect(warning).toContain("/yolo off"); }); - test("symlinked-home alias of the default source still appends the hint", async () => { + test("TUI symlinked-home alias of the default source still appends the hint", async () => { const sandbox = await mkdtemp(join(tmpdir(), "corbits-skip-warning-")); const homeLink = join(sandbox, "home"); await symlink(homedir(), homeLink); const aliased = join(homeLink, SETTINGS_DIR_NAME, "settings.json"); - const warning = savedSkipPermissionsWarning(aliased); + const warning = savedSkipPermissionsWarning(aliased, "tui"); expect(warning).toContain(aliased); expect(warning).toContain("edit that file to re-enable"); expect(warning).toContain("/yolo off"); }); - test("custom source keeps file-path wording with no false provenance", () => { - const warning = savedSkipPermissionsWarning( - "/tmp/custom-corbits-settings.json", + test("exec default source stays path-only with no slash", () => { + const source = join(homedir(), SETTINGS_DIR_NAME, "settings.json"); + + const warning = savedSkipPermissionsWarning(source, "exec"); + + expect(warning).toContain(source); + expect(warning).toContain("edit that file to re-enable"); + expect(warning).not.toContain("/yolo"); + expect(warning).toBe( + `Warning: permission prompts are disabled by saved settings at ${source}; edit that file to re-enable.`, ); + }); + + test("exec symlinked-home alias of the default source stays path-only", async () => { + const sandbox = await mkdtemp(join(tmpdir(), "corbits-skip-warning-")); + const homeLink = join(sandbox, "home"); + await symlink(homedir(), homeLink); + const aliased = join(homeLink, SETTINGS_DIR_NAME, "settings.json"); + + const warning = savedSkipPermissionsWarning(aliased, "exec"); - expect(warning).toContain("/tmp/custom-corbits-settings.json"); + expect(warning).toContain(aliased); expect(warning).toContain("edit that file to re-enable"); - expect(warning).not.toMatch(/machine-wide|saved default|\/yolo off/i); + expect(warning).not.toContain("/yolo"); + }); + + test("custom source keeps file-path wording with no false provenance on both surfaces", () => { + for (const surface of ["tui", "exec"] as const) { + const warning = savedSkipPermissionsWarning( + "/tmp/custom-corbits-settings.json", + surface, + ); + + expect(warning).toContain("/tmp/custom-corbits-settings.json"); + expect(warning).toContain("edit that file to re-enable"); + expect(warning).not.toMatch(/machine-wide|saved default|\/yolo off/i); + } }); }); diff --git a/src/permission/saved-skip-warning.ts b/src/permission/saved-skip-warning.ts index d147ae436..41830e826 100644 --- a/src/permission/saved-skip-warning.ts +++ b/src/permission/saved-skip-warning.ts @@ -12,9 +12,10 @@ function isDefaultSettingsSource(sourcePath: string): boolean { export function savedSkipPermissionsWarning( globalSettingsPath: string, + surface: "tui" | "exec", ): string { const base = `Warning: permission prompts are disabled by saved settings at ${globalSettingsPath}; edit that file to re-enable`; - if (isDefaultSettingsSource(globalSettingsPath)) { + if (surface === "tui" && isDefaultSettingsSource(globalSettingsPath)) { return `${base} (/yolo off to re-enable).`; } return `${base}.`; diff --git a/src/tui/runner/wiring.ts b/src/tui/runner/wiring.ts index fd6aa0837..2ff3104dc 100644 --- a/src/tui/runner/wiring.ts +++ b/src/tui/runner/wiring.ts @@ -96,7 +96,7 @@ export function surfaceSavedSkipPermissionsWarning( if (!config.skipPermissionsFromSettings) return; surfaceSystemNotice( shell, - savedSkipPermissionsWarning(config.globalSettingsPath), + savedSkipPermissionsWarning(config.globalSettingsPath, "tui"), ); } diff --git a/tests/unit/exec/runner.test.ts b/tests/unit/exec/runner.test.ts index 2c7a05650..043633580 100644 --- a/tests/unit/exec/runner.test.ts +++ b/tests/unit/exec/runner.test.ts @@ -491,8 +491,9 @@ describe("runExec", () => { ); const stderrOutput = stderrChunks.join(""); expect(stderrOutput).toContain( - `Warning: permission prompts are disabled by saved settings at ${defaultSettingsPath}; edit that file to re-enable (/yolo off to re-enable).\n`, + `Warning: permission prompts are disabled by saved settings at ${defaultSettingsPath}; edit that file to re-enable.\n`, ); + expect(stderrOutput).not.toContain("/yolo"); expect(stderrOutput).toMatch(/runtime dispose failed/i); expect(disposeCalls).toBe(1); expect(getActiveDisposeHost()).toBeNull();