555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
169 lines
6.2 KiB
TypeScript
169 lines
6.2 KiB
TypeScript
/**
|
|
* Unit tests for init internals — decision tree primitives + plugin sync.
|
|
*
|
|
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
|
|
* in sync — if you add a behavioral assertion here, mirror it on the Python
|
|
* side and vice versa.
|
|
*
|
|
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
|
|
* flap silently mints a new shadow over a working key).
|
|
* - `plugin_sync` must only update entries that already exist, preserve
|
|
* trailing newlines, and never mangle other lines.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { pingKey } from "../src/commands/init.js";
|
|
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
|
|
|
|
// ── pingKey ──────────────────────────────────────────────────────────────
|
|
|
|
describe("pingKey — network vs auth distinction", () => {
|
|
const origFetch = globalThis.fetch;
|
|
afterEach(() => {
|
|
globalThis.fetch = origFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns true for 200", async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
|
});
|
|
|
|
it("returns false for 401 (definitively invalid)", async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
|
});
|
|
|
|
it("returns false for 403 (definitively invalid)", async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
|
});
|
|
|
|
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
|
});
|
|
|
|
it("returns true on network error (prefer reuse over re-mint)", async () => {
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
|
});
|
|
|
|
it("returns true on timeout (prefer reuse)", async () => {
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
|
|
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── updateShellRc ────────────────────────────────────────────────────────
|
|
|
|
describe("updateShellRc — exists-only contract", () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
|
});
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("updates existing export and preserves trailing newline", () => {
|
|
const rc = path.join(tmpDir, ".zshrc");
|
|
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
|
|
expect(updateShellRc(rc, "newkey")).toBe(true);
|
|
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
|
|
});
|
|
|
|
it("does NOT create a new export when none exists", () => {
|
|
const rc = path.join(tmpDir, ".zshrc");
|
|
fs.writeFileSync(rc, "alias ll='ls -la'\n");
|
|
expect(updateShellRc(rc, "newkey")).toBe(false);
|
|
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
|
|
});
|
|
|
|
it("preserves surrounding content", () => {
|
|
const rc = path.join(tmpDir, ".zshrc");
|
|
const original =
|
|
"# my zshrc\n" +
|
|
"alias ll='ls -la'\n" +
|
|
"export MEM0_API_KEY='old'\n" +
|
|
"export OTHER=keepme\n";
|
|
fs.writeFileSync(rc, original);
|
|
updateShellRc(rc, "newkey");
|
|
const after = fs.readFileSync(rc, "utf-8");
|
|
expect(after).toContain("alias ll='ls -la'\n");
|
|
expect(after).toContain("export OTHER=keepme\n");
|
|
expect(after).toContain("# my zshrc\n");
|
|
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
|
|
});
|
|
|
|
it("is idempotent when value already matches", () => {
|
|
const rc = path.join(tmpDir, ".zshrc");
|
|
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
|
|
expect(updateShellRc(rc, "same")).toBe(false);
|
|
});
|
|
|
|
it("is a no-op for missing files", () => {
|
|
const rc = path.join(tmpDir, ".zshrc"); // does not exist
|
|
expect(updateShellRc(rc, "x")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── updateClaudeSettings ─────────────────────────────────────────────────
|
|
|
|
describe("updateClaudeSettings — never creates entries", () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
|
});
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not create env block when none exists", () => {
|
|
const settings = path.join(tmpDir, "settings.json");
|
|
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
|
|
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
|
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
|
|
otherKey: 1,
|
|
});
|
|
});
|
|
|
|
it("does not create MEM0_API_KEY entry in existing env block", () => {
|
|
const settings = path.join(tmpDir, "settings.json");
|
|
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
|
|
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
|
});
|
|
|
|
it("updates existing entry and preserves siblings", () => {
|
|
const settings = path.join(tmpDir, "settings.json");
|
|
fs.writeFileSync(
|
|
settings,
|
|
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
|
|
);
|
|
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
|
|
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
|
|
expect(data.env.MEM0_API_KEY).toBe("fresh");
|
|
expect(data.env.OTHER).toBe("y");
|
|
});
|
|
|
|
it("is idempotent when value already matches", () => {
|
|
const settings = path.join(tmpDir, "settings.json");
|
|
fs.writeFileSync(
|
|
settings,
|
|
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
|
|
);
|
|
expect(updateClaudeSettings(settings, "same")).toBe(false);
|
|
});
|
|
|
|
it("is a no-op for malformed JSON", () => {
|
|
const settings = path.join(tmpDir, "settings.json");
|
|
fs.writeFileSync(settings, "{ this is not json");
|
|
expect(updateClaudeSettings(settings, "x")).toBe(false);
|
|
});
|
|
});
|