Skip to content
Open
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
10 changes: 8 additions & 2 deletions infra/monitor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ import {
getAllTasks as getAllInternalTasks,
} from "../task/task-store.mjs";
import { createAgentEndpoint } from "../agent/agent-endpoint.mjs";
import { createAgentEventBus } from "../agent/agent-event-bus.mjs";
import { createAgentEventBus } from "../agent/agent-event-bus.mjs";`r`nimport { onConfigReload } from "../ui/tui/config-events.js";
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The new import line contains literal rncharacters (likely from an escaped CRLF) which makes the file invalid JavaScript. Split this into a normal standaloneimport { onConfigReload ... }` statement on its own line and remove the stray characters/backticks.

Suggested change
import { createAgentEventBus } from "../agent/agent-event-bus.mjs";`r`nimport { onConfigReload } from "../ui/tui/config-events.js";
import { createAgentEventBus } from "../agent/agent-event-bus.mjs";
import { onConfigReload } from "../ui/tui/config-events.js";

Copilot uses AI. Check for mistakes.
import { createReviewAgent } from "../agent/review-agent.mjs";

import { createErrorDetector } from "./error-detector.mjs";
Expand Down Expand Up @@ -13286,6 +13286,10 @@ async function reloadConfig(reason) {
}
}

const stopTuiConfigReloadListener = onConfigReload((payload = {}) => {
runDetached("config-reload:tui", () => reloadConfig(payload.reason || "tui-settings"));
});

process.on("SIGINT", async () => {
shuttingDown = true;
stopWorkspaceSyncTimers();
Expand Down Expand Up @@ -13333,7 +13337,7 @@ process.on("SIGINT", async () => {
});

// Windows: closing the terminal window doesn't send SIGINT/SIGTERM reliably.
process.on("exit", () => {
process.on("exit", () => {`r`n try { stopTuiConfigReloadListener?.(); } catch { /* best effort */ }
shuttingDown = true;
Comment on lines +13340 to 13341
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The exit handler line is missing proper newlines/bracing and includes stray rntext, leaving the handler body unterminated. Format the handler as a normal multi-line function and ensurestopTuiConfigReloadListener()` is called inside a try/finally (or best-effort try/catch) without breaking the rest of the shutdown logic.

Copilot uses AI. Check for mistakes.
stopWorkspaceSyncTimers();
stopAgentAlertTailer();
Expand Down Expand Up @@ -15037,3 +15041,5 @@ export {
};




121 changes: 121 additions & 0 deletions tests/tui-settings-screen.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import React from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import SettingsScreen from "../ui/tui/SettingsScreen.js";
import { renderInk } from "./tui/render-ink.mjs";
import { waitFor } from "./tui/render-helpers.mjs";

function createConfigDir(config) {
const dir = mkdtempSync(join(tmpdir(), "bosun-settings-"));
const path = join(dir, "bosun.config.json");
writeFileSync(path, JSON.stringify(config, null, 2));
return { dir, path };
}

function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}

describe("tui settings screen", () => {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

There are now two separate SettingsScreen test files (tests/tui/settings-screen.test.mjs and tests/tui-settings-screen.test.mjs) that largely overlap in intent. Consider consolidating into one location/pattern to avoid redundant coverage and future drift (pick either the tests/tui/* convention or the root tests/tui-*.test.mjs convention used elsewhere).

Suggested change
describe("tui settings screen", () => {
describe.skip("tui settings screen (moved to tests/tui/settings-screen.test.mjs)", () => {

Copilot uses AI. Check for mistakes.
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.KANBAN_BACKEND;
delete process.env.TELEGRAM_UI_PORT;
delete process.env.TELEGRAM_BOT_TOKEN;
});

afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
Comment on lines +24 to +35
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

originalEnv is captured as a reference (const originalEnv = process.env) and later restored via process.env = originalEnv, which can reintroduce mutations from other tests. Use the repo’s common pattern of snapshotting (const originalEnv = { ...process.env }) and restoring with a shallow copy in afterEach/beforeEach.

Copilot uses AI. Check for mistakes.
});

it("renders grouped config fields with masked secrets and source labels", async () => {
process.env.TELEGRAM_BOT_TOKEN = "env-token";

const { dir } = createConfigDir({
kanban: { backend: "github" },
telegram: { uiPort: 3080, token: "config-token" },
costRates: { inputPer1M: 1.5 },
});

const view = await renderInk(
React.createElement(SettingsScreen, {
configDir: dir,
config: { kanban: { backend: "github" }, telegram: { uiPort: 3080, token: "config-token" }, costRates: { inputPer1M: 1.5 } },
}),
{ columns: 220, rows: 60 },
);

const text = view.text();
expect(text).toContain("General");
expect(text).toContain("Kanban");
expect(text).toContain("Integrations");
expect(text).toContain("Cost Rates");

Check failure on line 59 in tests/tui-settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / Build + Tests

tests/tui-settings-screen.test.mjs > tui settings screen > renders grouped config fields with masked secrets and source labels

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Cost Rates' - Expected + Received - Cost Rates + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: - (default) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (default) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpoints: - (default

Check failure on line 59 in tests/tui-settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / 🔁 Existing E2E Suite

tests/tui-settings-screen.test.mjs > tui settings screen > renders grouped config fields with masked secrets and source labels

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Cost Rates' - Expected + Received - Cost Rates + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: - (default) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (default) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpoints: - (default
expect(text).toContain("backend");
expect(text).toContain("github");
expect(text).toContain("from config");
expect(text).toContain("from env");
expect(text).toContain("****");
expect(text).not.toContain("env-token");

Comment on lines +38 to +66
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This test expects schema-driven fields like telegram.uiPort / telegram.token and costRates.*, but bosun.schema.json does not define a telegram object or costRates, so SettingsScreen won’t render these rows and waitFor(() => view.text().includes("uiPort: 3080")) will time out. Update the test to use fields that exist in the schema (e.g., cloudflareDnsMaxRetries, kanban.backend, kanban.github.project.webhook.secret, etc.) and the correct env var names.

Copilot uses AI. Check for mistakes.
await view.unmount();
});

it("saves an edited enum field atomically and emits config reload", async () => {
const { dir, path } = createConfigDir({
kanban: { backend: "internal" },
});
const emitReload = vi.fn();

const view = await renderInk(
React.createElement(SettingsScreen, {
configDir: dir,
config: { kanban: { backend: "internal" } },
onConfigReload: emitReload,
}),
{ columns: 220, rows: 60 },
);

await waitFor(() => view.text().includes("backend: internal"));
await view.press("\r");
await view.press("\u001b[C", 80);

await waitFor(() => readJson(path).kanban.backend === "github");
expect(readJson(path).kanban.backend).toBe("github");
expect(emitReload).toHaveBeenCalledTimes(1);
expect(emitReload).toHaveBeenCalledWith(expect.objectContaining({ configPath: path }));

await view.unmount();
});

it("blocks invalid numeric edits and keeps the file unchanged", async () => {
const { dir, path } = createConfigDir({
telegram: { uiPort: 3080 },
});

const view = await renderInk(
React.createElement(SettingsScreen, {
configDir: dir,
config: { telegram: { uiPort: 3080 } },
}),
{ columns: 220, rows: 60 },
);

await waitFor(() => view.text().includes("uiPort: 3080"));
await view.press("j", 40);
await view.press("\r");
await view.press("999999999999", 40);
await view.press("\u0013", 80);

await waitFor(() => view.text().includes("Validation error"));
expect(readJson(path).telegram.uiPort).toBe(3080);

await view.unmount();
});
});
107 changes: 107 additions & 0 deletions tests/tui/settings-screen.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import React from "react";
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";

import SettingsScreen from "../../ui/tui/SettingsScreen.js";
import { renderInk } from "./render-ink.mjs";

function makeConfigDir(config) {
const dir = mkdtempSync(join(tmpdir(), "bosun-settings-"));
writeFileSync(join(dir, "bosun.config.json"), `${JSON.stringify(config, null, 2)}\n`, "utf8");
return dir;
}

describe("tui settings screen", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.TELEGRAM_BOT_TOKEN;
delete process.env.KANBAN_BACKEND;
delete process.env.LINEAR_API_KEY;
});

afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});

it("renders grouped schema fields and masks secrets by default", async () => {
process.env.TELEGRAM_BOT_TOKEN = "env-secret-token";

const configDir = makeConfigDir({
projectName: "demo",
kanban: { backend: "github" },
linear: { apiKey: "super-secret" },
telegramBotToken: "local-secret",
costRates: { inputPer1M: 1, outputPer1M: 2 },
});

const view = await renderInk(
React.createElement(SettingsScreen, { configDir, config: {} }),
{ columns: 220, waitMs: 120 },
);

const text = view.text();
expect(text).toContain("General");
expect(text).toContain("Kanban");
expect(text).toContain("Integrations");
expect(text).toContain("Cost Rates");

Check failure on line 51 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / Build + Tests

tests/tui/settings-screen.test.mjs > tui settings screen > renders grouped schema fields and masks secrets by default

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Cost Rates' - Expected + Received - Cost Rates + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: demo (from config) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (default) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpoints: - (

Check failure on line 51 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / 🔁 Existing E2E Suite

tests/tui/settings-screen.test.mjs > tui settings screen > renders grouped schema fields and masks secrets by default

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Cost Rates' - Expected + Received - Cost Rates + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: demo (from config) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (default) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpoints: - (
expect(text).toContain("linear.apiKey");
expect(text).toContain("telegramBotToken");
expect(text).toContain("from env");
expect(text).toContain("****");
expect(text).not.toContain("env-secret-token");

await view.unmount();
});

it("writes enum changes to bosun.config.json and emits reload", async () => {

Check failure on line 61 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / Build + Tests

tests/tui/settings-screen.test.mjs > tui settings screen > writes enum changes to bosun.config.json and emits reload

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ tests/tui/settings-screen.test.mjs:61:5

Check failure on line 61 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / 🔁 Existing E2E Suite

tests/tui/settings-screen.test.mjs > tui settings screen > writes enum changes to bosun.config.json and emits reload

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ tests/tui/settings-screen.test.mjs:61:5
const configDir = makeConfigDir({ kanban: { backend: "github" } });
const emitReload = vi.fn();

const view = await renderInk(
React.createElement(SettingsScreen, { configDir, config: {}, onConfigReload: emitReload }),
{ columns: 220, waitMs: 120 },
);

for (let index = 0; index < 120 && !view.text().includes("> kanban.backend"); index += 1) {
await view.press("j", 5);
}
await view.press("\u001b[C", 120);

const updated = JSON.parse(readFileSync(join(configDir, "bosun.config.json"), "utf8"));
expect(updated.kanban.backend).not.toBe("github");
expect(emitReload).toHaveBeenCalledTimes(1);
expect(emitReload).toHaveBeenCalledWith(expect.objectContaining({
configPath: join(configDir, "bosun.config.json"),
reason: "settings-save",
}));

await view.unmount();
});

it("prevents invalid numeric saves and leaves file unchanged", async () => {
const configDir = makeConfigDir({ cloudflareDnsMaxRetries: 3 });
const before = readFileSync(join(configDir, "bosun.config.json"), "utf8");
const view = await renderInk(
React.createElement(SettingsScreen, { configDir, config: {} }),
{ columns: 220, waitMs: 120 },
);

for (let index = 0; index < 220 && !view.text().includes("> cloudflareDnsMaxRetries"); index += 1) {
await view.press("j", 5);
}
await view.press("\r", 60);
await view.press("abc", 20);
await view.press("\u0013", 120);

expect(view.text()).toContain("Validation error");

Check failure on line 101 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / Build + Tests

tests/tui/settings-screen.test.mjs > tui settings screen > prevents invalid numeric saves and leaves file unchanged

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Validation error' - Expected + Received - Validation error + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: - (default) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (from config) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpo

Check failure on line 101 in tests/tui/settings-screen.test.mjs

View workflow job for this annotation

GitHub Actions / 🔁 Existing E2E Suite

tests/tui/settings-screen.test.mjs > tui settings screen > prevents invalid numeric saves and leaves file unchanged

AssertionError: expected 'Settings\nEdit bosun.config.json inli…' to contain 'Validation error' - Expected + Received - Validation error + Settings + Edit bosun.config.json inline. Enter edits, Ctrl+S saves, Esc cancels, Space toggles booleans, arrows cycle enums, U unmasks secrets. + + General + > $schema: - (default) + projectName: - (default) + mode: - (default) + orchestratorScript: - (default) + orchestratorArgs: - (default) + logDir: - (default) + logMaxSizeMb: 500 (default) + logCleanupIntervalMin: 30 (default) + telegramVerbosity: summary (default) + watchPath: - (default) + watchEnabled: - (default) + echoLogs: - (default) + autoFixEnabled: - (default) + workflowFirst: false (default) + workflowOwnsTaskLifecycle: false (default) + workflowWorktreeRecoveryCooldownMin: 15 (default) + workflowDefaults.profile: - (default) + workflowDefaults.autoInstall: - (default) + workflowDefaults.templates: - (default) + workflowDefaults.templateOverridesById: - (default) + workflows: - (default) + prAutomation.attachMode: all (default) + prAutomation.trustedAuthors: - (default) + prAutomation.allowTrustedFixes: false (default) + prAutomation.allowTrustedMerges: false (default) + prAutomation.assistiveActions.installOnSetup: false (default) + gates.prs.repoVisibility: unknown (default) + gates.prs.automationPreference: runtime-first (default) + gates.prs.githubActionsBudget: ask-user (default) + gates.checks.mode: all (default) + gates.checks.requiredPatterns: - (default) + gates.checks.optionalPatterns: - (default) + gates.checks.ignorePatterns: - (default) + gates.checks.requireAnyRequiredCheck: true (default) + gates.checks.treatPendingRequiredAsBlocking: true (default) + gates.checks.treatNeutralAsPass: false (default) + gates.execution.sandboxMode: - (default) + gates.execution.containerIsolationEnabled: false (default) + gates.execution.containerRuntime: - (default) + gates.execution.networkAccess: - (default) + gates.worktrees.requireBootstrap: - (default) + gates.worktrees.requireReadiness: - (default) + gates.worktrees.enforcePushHook: true (default) + gates.runtime.enforceBacklog: true (default) + gates.runtime.agentTriggerControl: true (default) + markdownSafety.enabled: true (default) + markdownSafety.auditLogPath: - (default) + markdownSafety.allowlist: - (default) + telegramUiTunnel: named (default) + telegramUiAllowQuickTunnelFallback: false (default) + cloudflareTunnelCredentials: - (default) + cloudflareBaseDomain: - (default) + cloudflareTunnelHostname: - (default) + cloudflareUsernameHostnamePolicy: per-user-fixed (default) + cloudflareDnsSyncEnabled: true (default) + cloudflareDnsMaxRetries: 3 (from config) + cloudflareDnsRetryBaseMs: 750 (default) + telegramUiFallbackAuthEnabled: true (default) + telegramUiFallbackAuthRateLimitIpPerMin: 10 (default) + telegramUiFallbackAuthRateLimitGlobalPerMin: 60 (default) + telegramUiFallbackAuthMaxFailures: 5 (default) + telegramUiFallbackAuthLockoutMs: 600000 (default) + telegramUiFallbackAuthRotateDays: 30 (default) + telegramUiFallbackAuthTransientCooldownMs: 5000 (default) + voice.enabled: true (default) + voice.provider: auto (default) + voice.providers: - (default) + voice.model: gpt-4o-realtime-preview-2024-12-17 (default) + voice.visionModel: gpt-4.1-mini (default) + voice.openaiApiKey: - (default) + voice.openaiAccessToken: - (default) + voice.azureApiKey: - (default) + voice.azureAccessToken: - (default) + voice.azureEndpoint: - (default) + voice.azureDeployment: gpt-4o-realtime-preview (default) + voice.claudeApiKey: - (default) + voice.claudeAccessToken: - (default) + voice.geminiApiKey: - (default) + voice.geminiAccessToken: - (default) + voice.voiceId: alloy (default) + voice.turnDetection: semantic_vad (default) + voice.instructions: - (default) + voice.fallbackMode: browser (default) + voice.failover.enabled: true (default) + voice.failover.maxAttempts: 2 (default) + voice.voiceEndpo
const after = readFileSync(join(configDir, "bosun.config.json"), "utf8");
expect(after).toBe(before);

await view.unmount();
});
});
Loading
Loading