Skip to content
Closed
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
87 changes: 87 additions & 0 deletions src/gateway/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import fs from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { _resetGatewayConfigWarningsForTest, loadGatewayConfig } from "./config.js";

function makeTempDir(): string {
return fs.mkdtempSync(path.join(tmpdir(), "tdai-gateway-config-"));
}

describe("gateway config path resolution", () => {
let originalCwd: string;
let tempDir: string;

beforeEach(() => {
originalCwd = process.cwd();
tempDir = makeTempDir();
process.chdir(tempDir);
_resetGatewayConfigWarningsForTest();
vi.stubEnv("HOME", path.join(tempDir, "home"));
vi.stubEnv("USERPROFILE", "");
vi.stubEnv("MEMORY_TENCENTDB_ROOT", "");
vi.stubEnv("TDAI_DATA_DIR", "");
vi.stubEnv("TDAI_GATEWAY_CONFIG", "");
});

afterEach(() => {
process.chdir(originalCwd);
_resetGatewayConfigWarningsForTest();
});

it("loads Linux config from XDG_CONFIG_HOME/tencentdb-agent-memory", () => {
const xdgConfigHome = path.join(tempDir, "xdg-config");
const configDir = path.join(xdgConfigHome, "tencentdb-agent-memory");
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
path.join(configDir, "tdai-gateway.yaml"),
[
"server:",
" port: 9511",
" host: 0.0.0.0",
"data:",
" baseDir: /tmp/tdai-xdg-data",
"",
].join("\n"),
);

vi.stubEnv("XDG_CONFIG_HOME", xdgConfigHome);

const config = loadGatewayConfig();

expect(config.server.port).toBe(9511);
expect(config.server.host).toBe("0.0.0.0");
expect(config.data.baseDir).toBe("/tmp/tdai-xdg-data");
});

it("warns only once when TDAI_GATEWAY_CONFIG points to a missing file", () => {
const missingPath = path.join(tempDir, "missing", "tdai-gateway.yaml");
vi.stubEnv("TDAI_GATEWAY_CONFIG", missingPath);
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);

loadGatewayConfig();
loadGatewayConfig();

const messages = stderrWrite.mock.calls.map((call) => String(call[0]));
expect(messages).toHaveLength(1);
expect(messages[0]).toContain("TDAI_GATEWAY_CONFIG points to missing config");
expect(messages[0]).toContain(missingPath);
});

it("warns only once when falling back to the legacy data directory", () => {
const home = path.join(tempDir, "legacy-home");
const legacyDataDir = path.join(home, "memory-tdai");
fs.mkdirSync(legacyDataDir, { recursive: true });
vi.stubEnv("HOME", home);
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);

const first = loadGatewayConfig();
const second = loadGatewayConfig();

expect(first.data.baseDir).toBe(legacyDataDir);
expect(second.data.baseDir).toBe(legacyDataDir);
const messages = stderrWrite.mock.calls.map((call) => String(call[0]));
expect(messages).toHaveLength(1);
expect(messages[0]).toContain("DEPRECATED: using legacy data dir");
});
});
98 changes: 79 additions & 19 deletions src/gateway/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
*/

import fs from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
import YAML from "yaml";
import { getEnv } from "../utils/env.js";
import { parseConfig as parseMemoryConfig } from "../config.js";
import type { MemoryTdaiConfig } from "../config.js";
import type { StandaloneLLMConfig } from "../adapters/standalone/llm-runner.js";

const warnedMissingConfigPaths = new Set<string>();
const warnedLegacyDataDirs = new Set<string>();

// ============================
// Gateway config types
// ============================
Expand Down Expand Up @@ -165,26 +169,38 @@ export function loadGatewayConfig(overrides?: Partial<GatewayConfig>): GatewayCo
function resolveConfigPath(): string | null {
// 1. Explicit env var
const explicit = getEnv("TDAI_GATEWAY_CONFIG")?.trim();
if (explicit && fs.existsSync(explicit)) return explicit;
if (explicit) {
if (safeExists(explicit)) return explicit;
warnMissingConfigOnce(explicit, "TDAI_GATEWAY_CONFIG");
return null;
}

// 2. CWD
for (const name of ["tdai-gateway.yaml", "tdai-gateway.json"]) {
const p = path.join(process.cwd(), name);
if (fs.existsSync(p)) return p;
if (safeExists(p)) return p;
}

// 3. Platform config dir (Linux: $XDG_CONFIG_HOME/tencentdb-agent-memory)
for (const dir of resolvePlatformConfigDirs()) {
for (const name of ["tdai-gateway.yaml", "tdai-gateway.json"]) {
const p = path.join(dir, name);
if (safeExists(p)) return p;
}
}

// 3. Default data dir
// 4. Default data dir
const dataDir = resolveDefaultDataDir();
for (const name of ["tdai-gateway.yaml", "tdai-gateway.json"]) {
const p = path.join(dataDir, name);
if (fs.existsSync(p)) return p;
if (safeExists(p)) return p;
}

return null;
}

function resolveDefaultDataDir(): string {
const home = getEnv("HOME") ?? getEnv("USERPROFILE") ?? "/tmp";
const home = resolveHomeDir();

// New canonical location: everything related to standalone/Hermes-mode TDAI
// is collected under ~/.memory-tencentdb/ to avoid scattering top-level dirs
Expand All @@ -195,32 +211,76 @@ function resolveDefaultDataDir(): string {
// Note: this only governs the standalone/Hermes fallback. Under the openclaw
// host the plugin data dir is decided by `resolveStateDir() + "memory-tdai"`
// (typically ~/.openclaw/memory-tdai/) which is intentionally NOT changed.
const root = getEnv("MEMORY_TENCENTDB_ROOT") ?? path.join(home, ".memory-tencentdb");
const root = getEnv("MEMORY_TENCENTDB_ROOT")?.trim() || path.join(home, ".memory-tencentdb");
const newDefault = path.join(root, "memory-tdai");

// Backward compatibility: if the new location does not yet exist but the
// legacy ~/memory-tdai still has data, keep using the legacy dir so existing
// users don't silently lose their memory store. The install script
// (install_hermes_memory_tencentdb.sh, Step 0) will migrate it on next run.
try {
if (!fs.existsSync(newDefault)) {
const legacy = path.join(home, "memory-tdai");
if (fs.existsSync(legacy)) {
// Stderr-only deprecation hint; doesn't pollute structured logs.
process.stderr.write(
`[tdai-gateway] DEPRECATED: using legacy data dir ${legacy}; ` +
`move it to ${newDefault} (or set TDAI_DATA_DIR / MEMORY_TENCENTDB_ROOT) to silence this warning.\n`,
);
return legacy;
}
if (!safeExists(newDefault)) {
const legacy = path.join(home, "memory-tdai");
if (safeExists(legacy)) {
// Stderr-only deprecation hint; doesn't pollute structured logs.
warnLegacyDataDirOnce(legacy, newDefault);
return legacy;
}
} catch {
// existsSync should not throw, but guard anyway.
}

return newDefault;
}

function resolvePlatformConfigDirs(): string[] {
const home = resolveHomeDir();

if (process.platform === "win32") {
const appData = getEnv("APPDATA")?.trim() || path.join(home, "AppData", "Roaming");
return [path.join(appData, "tencentdb-agent-memory")];
}

if (process.platform === "darwin") {
return [path.join(home, "Library", "Application Support", "tencentdb-agent-memory")];
}

const xdgConfigHome = getEnv("XDG_CONFIG_HOME")?.trim() || path.join(home, ".config");
return [path.join(xdgConfigHome, "tencentdb-agent-memory")];
}

function resolveHomeDir(): string {
return getEnv("HOME")?.trim() || getEnv("USERPROFILE")?.trim() || homedir() || "/tmp";
}

function safeExists(p: string): boolean {
try {
return fs.existsSync(p);
} catch {
return false;
}
}

function warnMissingConfigOnce(configPath: string, source: string): void {
const key = `${source}:${configPath}`;
if (warnedMissingConfigPaths.has(key)) return;
warnedMissingConfigPaths.add(key);
process.stderr.write(
`[tdai-gateway] WARN: ${source} points to missing config ${configPath}; using defaults.\n`,
);
}

function warnLegacyDataDirOnce(legacy: string, replacement: string): void {
if (warnedLegacyDataDirs.has(legacy)) return;
warnedLegacyDataDirs.add(legacy);
process.stderr.write(
`[tdai-gateway] DEPRECATED: using legacy data dir ${legacy}; ` +
`move it to ${replacement} (or set TDAI_DATA_DIR / MEMORY_TENCENTDB_ROOT) to silence this warning.\n`,
);
}

export function _resetGatewayConfigWarningsForTest(): void {
warnedMissingConfigPaths.clear();
warnedLegacyDataDirs.clear();
}

function env(key: string): string | undefined {
const v = getEnv(key)?.trim();
return v || undefined;
Expand Down
Loading