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
16 changes: 8 additions & 8 deletions packages/cli/__tests__/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
vi.unstubAllGlobals();
});

it("writes dashboard notifier config with the urgent-action routing default", async () => {
it("writes dashboard notifier config with the all-priorities routing default", async () => {
const program = createProgram();

await program.parseAsync([
Expand All @@ -290,8 +290,8 @@
expect(parsed.notifiers?.["dashboard"]).toEqual({ plugin: "dashboard", limit: 75 });
expect(parsed.notificationRouting?.urgent).toContain("dashboard");
expect(parsed.notificationRouting?.action).toContain("dashboard");
expect(parsed.notificationRouting?.warning ?? []).not.toContain("dashboard");
expect(parsed.notificationRouting?.info ?? []).not.toContain("dashboard");
expect(parsed.notificationRouting?.warning).toContain("dashboard");
expect(parsed.notificationRouting?.info).toContain("dashboard");
});

it("prints status without mutating config", async () => {
Expand Down Expand Up @@ -2100,7 +2100,7 @@
expect(mockValidateToken).not.toHaveBeenCalled();
expect(mockWriteFileSync).toHaveBeenCalled();
const writtenYaml = mockWriteFileSync.mock.calls[0][1] as string;
expect(writtenYaml).toContain("${OPENCLAW_HOOKS_TOKEN}");

Check warning on line 2103 in packages/cli/__tests__/commands/setup.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected template string expression
});

it("reads token from OpenClaw config without copying it into AO config", async () => {
Expand Down Expand Up @@ -2717,7 +2717,7 @@
expect(setup?.commands.some((command) => command.name() === "desktop")).toBe(true);
});

it("installs the bundled app and wires desktop routing to all priorities", async () => {
it("installs the bundled app and wires desktop routing to urgent only", async () => {
const program = createProgram();

await program.parseAsync(["node", "test", "setup", "desktop", "--non-interactive"]);
Expand All @@ -2735,9 +2735,9 @@
dashboardUrl: "http://localhost:3000",
});
expect(parsed.notificationRouting?.["urgent"]).toContain("desktop");
expect(parsed.notificationRouting?.["action"]).toContain("desktop");
expect(parsed.notificationRouting?.["warning"]).toContain("desktop");
expect(parsed.notificationRouting?.["info"]).toContain("desktop");
expect(parsed.notificationRouting?.["action"] ?? []).not.toContain("desktop");
expect(parsed.notificationRouting?.["warning"] ?? []).not.toContain("desktop");
expect(parsed.notificationRouting?.["info"] ?? []).not.toContain("desktop");
});

it("configures terminal-notifier backend without installing AO Notifier.app", async () => {
Expand Down Expand Up @@ -2978,7 +2978,7 @@
defaults?: { notifiers?: string[] };
};
expect(parsed.notificationRouting?.["urgent"]).toEqual(["slack", "desktop"]);
expect(parsed.notificationRouting?.["action"]).toEqual(["slack", "desktop"]);
expect(parsed.notificationRouting?.["action"]).toEqual(["slack"]);
expect(parsed.defaults?.notifiers).toEqual(["slack"]);
});

Expand Down
44 changes: 31 additions & 13 deletions packages/cli/__tests__/commands/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ import { basename, join } from "node:path";
import { tmpdir } from "node:os";
import { parse as parseYaml } from "yaml";
import { EventEmitter } from "node:events";
import {
getDefaultRuntime,
recordActivityEvent,
type SessionManager,
} from "@aoagents/ao-core";
import { getDefaultRuntime, recordActivityEvent, type SessionManager } from "@aoagents/ao-core";

// ---------------------------------------------------------------------------
// Hoisted mocks
Expand Down Expand Up @@ -2366,7 +2362,7 @@ describe("start command — platform-aware runtime fallback", () => {
// ---------------------------------------------------------------------------

describe("start command — autoCreateConfig", () => {
it("writes a flat local config and returns the global project identity", async () => {
it("writes a flat local config, returns global project identity, and creates startup notifier defaults", async () => {
const { detectEnvironment } = await import("../../src/lib/detect-env.js");
vi.mocked(detectEnvironment).mockResolvedValue({
isGitRepo: true,
Expand Down Expand Up @@ -2418,15 +2414,40 @@ describe("start command — autoCreateConfig", () => {
const globalContent = readFileSync(globalConfigPath, "utf-8");
const globalParsed = parseYaml(globalContent) as {
defaults?: { notifiers?: unknown[] };
notifiers?: Record<
string,
{ plugin?: string; backend?: string; dashboardUrl?: string; limit?: number }
>;
notificationRouting?: Record<string, string[]>;
projects?: Record<string, { path?: string; sessionPrefix?: string }>;
};
expect(globalParsed.defaults?.notifiers).toEqual(["composio", "desktop"]);
expect(globalParsed.defaults?.notifiers).toEqual([]);
expect(globalParsed.notifiers?.["dashboard"]).toEqual({ plugin: "dashboard", limit: 50 });
expect(globalParsed.notifiers?.["desktop"]).toMatchObject({
plugin: "desktop",
backend: "ao-app",
dashboardUrl: "http://localhost:3000",
});
expect(globalParsed.notificationRouting).toEqual({
urgent: ["desktop", "dashboard"],
action: ["dashboard"],
warning: ["dashboard"],
info: ["dashboard"],
});
expect(globalContent).not.toContain("composio");

const projectIds = Object.keys(globalParsed.projects ?? {});
expect(projectIds).toHaveLength(1);
expect(config.configPath).toBe(configPath);
expect(Object.keys(config.projects)).toEqual(projectIds);
expect(config.projects[projectIds[0]!]!.path).toBe(realpathSync(tmpDir));
expect(config.defaults.notifiers).toEqual([]);
expect(config.notificationRouting).toEqual({
urgent: ["desktop", "dashboard"],
action: ["dashboard"],
warning: ["dashboard"],
info: ["dashboard"],
});
});

it("removes the flat local config when global registration fails", async () => {
Expand Down Expand Up @@ -2459,12 +2480,9 @@ describe("start command — autoCreateConfig", () => {

writeFileSync(
process.env["AO_GLOBAL_CONFIG"]!,
[
"projects:",
` ${basename(tmpDir)}:`,
` path: ${join(tmpDir, "other-repo")}`,
"",
].join("\n"),
["projects:", ` ${basename(tmpDir)}:`, ` path: ${join(tmpDir, "other-repo")}`, ""].join(
"\n",
),
);

await expect(autoCreateConfig(tmpDir)).rejects.toThrow("already registered");
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/__tests__/lib/path-equality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ describe("canonicalCompareKey", () => {
process.env["HOME"] = tmpDir;
try {
const key = canonicalCompareKey("~");
// On Windows the result is lowercased; on POSIX it's case-preserved.
expect(key.toLowerCase()).toBe(tmpDir.toLowerCase());
expect(key).toBe(canonicalCompareKey(tmpDir));
} finally {
if (originalHome === undefined) delete process.env["HOME"];
else process.env["HOME"] = originalHome;
Expand Down
229 changes: 229 additions & 0 deletions packages/cli/__tests__/lib/startup-notifier-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { parse as parseYaml } from "yaml";
import { ensureStartupNotifierDefaults } from "../../src/lib/startup-notifier-defaults.js";

describe("startup notifier defaults", () => {
let tempDir: string;
let configPath: string;

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "ao-startup-notifiers-"));
configPath = join(tempDir, "agent-orchestrator.yaml");
});

afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});

function readConfig(): {
defaults?: { notifiers?: string[] };
notifiers?: Record<
string,
{ plugin?: string; backend?: string; dashboardUrl?: string; limit?: number }
>;
notificationRouting?: Record<string, string[]>;
} {
return parseYaml(readFileSync(configPath, "utf-8")) as {
defaults?: { notifiers?: string[] };
notifiers?: Record<
string,
{ plugin?: string; backend?: string; dashboardUrl?: string; limit?: number }
>;
notificationRouting?: Record<string, string[]>;
};
}

it("replaces legacy implicit Composio routes with dashboard all-priority and desktop urgent-only defaults", () => {
writeFileSync(
configPath,
[
"port: 3000",
"defaults:",
" notifiers:",
" - composio",
" - desktop",
"notifiers: {}",
"notificationRouting:",
" urgent: [desktop, composio]",
" action: [desktop, composio]",
" warning: [composio]",
" info: [composio]",
"projects: {}",
"",
].join("\n"),
);

expect(
ensureStartupNotifierDefaults({
configPath,
dashboardUrl: "http://localhost:3000",
desktopMode: "enable",
}),
).toBe(true);

const parsed = readConfig();
expect(parsed.defaults?.notifiers).toEqual([]);
expect(parsed.notifiers?.["dashboard"]).toEqual({ plugin: "dashboard", limit: 50 });
expect(parsed.notifiers?.["desktop"]).toMatchObject({
plugin: "desktop",
backend: "ao-app",
dashboardUrl: "http://localhost:3000",
});
expect(parsed.notificationRouting).toEqual({
urgent: ["desktop", "dashboard"],
action: ["dashboard"],
warning: ["dashboard"],
info: ["dashboard"],
});
expect(readFileSync(configPath, "utf-8")).not.toContain("composio");
});

it("removes default desktop routing when startup desktop setup cannot complete", () => {
writeFileSync(
configPath,
[
"port: 3000",
"defaults:",
" notifiers: []",
"notifiers:",
" desktop:",
" plugin: desktop",
" backend: ao-app",
" dashboardUrl: http://localhost:3000",
" dashboard:",
" plugin: dashboard",
" limit: 50",
"notificationRouting:",
" urgent: [desktop, dashboard]",
" action: [dashboard]",
" warning: [dashboard]",
" info: [dashboard]",
"projects: {}",
"",
].join("\n"),
);

expect(
ensureStartupNotifierDefaults({
configPath,
dashboardUrl: "http://localhost:3000",
desktopMode: "disable-default",
}),
).toBe(true);

const parsed = readConfig();
expect(parsed.notifiers?.["desktop"]).toBeUndefined();
expect(parsed.notificationRouting).toEqual({
urgent: ["dashboard"],
action: ["dashboard"],
warning: ["dashboard"],
info: ["dashboard"],
});
});

it("preserves custom desktop routing when startup AO Notifier.app setup cannot complete", () => {
writeFileSync(
configPath,
[
"port: 3000",
"defaults:",
" notifiers: []",
"notifiers:",
" desktop:",
" plugin: desktop",
" backend: terminal-notifier",
" dashboardUrl: http://localhost:3000",
"notificationRouting:",
" urgent: [desktop]",
"projects: {}",
"",
].join("\n"),
);

expect(
ensureStartupNotifierDefaults({
configPath,
dashboardUrl: "http://localhost:3000",
desktopMode: "disable-default",
}),
).toBe(true);

const parsed = readConfig();
expect(parsed.notifiers?.["desktop"]).toMatchObject({
plugin: "desktop",
backend: "terminal-notifier",
});
expect(parsed.notificationRouting?.urgent).toEqual(["desktop", "dashboard"]);
});

it("preserves defaults.notifiers fallback semantics when writing startup routes", () => {
writeFileSync(
configPath,
[
"port: 3000",
"defaults:",
" notifiers:",
" - slack",
"notifiers:",
" slack:",
" plugin: slack",
" webhookUrl: https://hooks.slack.com/services/T/B/C",
"projects: {}",
"",
].join("\n"),
);

expect(
ensureStartupNotifierDefaults({
configPath,
dashboardUrl: "http://localhost:3000",
desktopMode: "enable",
}),
).toBe(true);

const parsed = readConfig();
expect(parsed.defaults?.notifiers).toEqual(["slack"]);
expect(parsed.notificationRouting).toEqual({
urgent: ["slack", "desktop", "dashboard"],
action: ["slack", "dashboard"],
warning: ["slack", "dashboard"],
info: ["slack", "dashboard"],
});
});

it("preserves configured manual opt-in notifiers while removing only implicit manual defaults", () => {
writeFileSync(
configPath,
[
"port: 3000",
"defaults:",
" notifiers:",
" - slack",
" - composio",
"notifiers:",
" slack:",
" plugin: slack",
" webhookUrl: https://hooks.slack.com/services/T/B/C",
"notificationRouting:",
" urgent: [slack, composio]",
"projects: {}",
"",
].join("\n"),
);

ensureStartupNotifierDefaults({
configPath,
dashboardUrl: "http://localhost:3000",
desktopMode: "enable",
});

const parsed = readConfig();
expect(parsed.defaults?.notifiers).toEqual(["slack"]);
expect(parsed.notificationRouting?.urgent).toEqual(["slack", "desktop", "dashboard"]);
expect(parsed.notifiers?.["slack"]?.plugin).toBe("slack");
expect(readFileSync(configPath, "utf-8")).not.toContain("composio");
});
});
Loading
Loading