fix(daemon): scope token drift warnings
This commit is contained in:
91
src/cli/daemon-cli/lifecycle-core.test.ts
Normal file
91
src/cli/daemon-cli/lifecycle-core.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadConfig = vi.fn(() => ({
|
||||
gateway: {
|
||||
auth: {
|
||||
token: "config-token",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const runtimeLogs: string[] = [];
|
||||
const defaultRuntime = {
|
||||
log: (message: string) => runtimeLogs.push(message),
|
||||
error: vi.fn(),
|
||||
exit: (code: number) => {
|
||||
throw new Error(`__exit__:${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
const service = {
|
||||
label: "TestService",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
install: vi.fn(),
|
||||
uninstall: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
isLoaded: vi.fn(),
|
||||
readCommand: vi.fn(),
|
||||
readRuntime: vi.fn(),
|
||||
restart: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
defaultRuntime,
|
||||
}));
|
||||
|
||||
describe("runServiceRestart token drift", () => {
|
||||
beforeEach(() => {
|
||||
runtimeLogs.length = 0;
|
||||
loadConfig.mockClear();
|
||||
service.isLoaded.mockClear();
|
||||
service.readCommand.mockClear();
|
||||
service.restart.mockClear();
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({
|
||||
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
|
||||
});
|
||||
service.restart.mockResolvedValue(undefined);
|
||||
vi.unstubAllEnvs();
|
||||
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
|
||||
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
|
||||
});
|
||||
|
||||
it("emits drift warning when enabled", async () => {
|
||||
const { runServiceRestart } = await import("./lifecycle-core.js");
|
||||
|
||||
await runServiceRestart({
|
||||
serviceNoun: "Gateway",
|
||||
service,
|
||||
renderStartHints: () => [],
|
||||
opts: { json: true },
|
||||
checkTokenDrift: true,
|
||||
});
|
||||
|
||||
expect(loadConfig).toHaveBeenCalledTimes(1);
|
||||
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
||||
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
|
||||
expect(payload.warnings?.[0]).toContain("gateway install --force");
|
||||
});
|
||||
|
||||
it("skips drift warning when disabled", async () => {
|
||||
const { runServiceRestart } = await import("./lifecycle-core.js");
|
||||
|
||||
await runServiceRestart({
|
||||
serviceNoun: "Node",
|
||||
service,
|
||||
renderStartHints: () => [],
|
||||
opts: { json: true },
|
||||
});
|
||||
|
||||
expect(loadConfig).not.toHaveBeenCalled();
|
||||
expect(service.readCommand).not.toHaveBeenCalled();
|
||||
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
||||
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
|
||||
expect(payload.warnings).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -236,6 +236,7 @@ export async function runServiceRestart(params: {
|
||||
service: GatewayService;
|
||||
renderStartHints: () => string[];
|
||||
opts?: DaemonLifecycleOptions;
|
||||
checkTokenDrift?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "restart", json });
|
||||
@@ -259,31 +260,33 @@ export async function runServiceRestart(params: {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for token drift before restart (service token vs config token)
|
||||
const warnings: string[] = [];
|
||||
try {
|
||||
const command = await params.service.readCommand(process.env);
|
||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN;
|
||||
const cfg = loadConfig();
|
||||
const configToken =
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const driftIssue = checkTokenDrift({ serviceToken, configToken });
|
||||
if (driftIssue) {
|
||||
const warning = driftIssue.detail
|
||||
? `${driftIssue.message} ${driftIssue.detail}`
|
||||
: driftIssue.message;
|
||||
warnings.push(warning);
|
||||
if (!json) {
|
||||
defaultRuntime.log(`\n⚠️ ${driftIssue.message}`);
|
||||
if (driftIssue.detail) {
|
||||
defaultRuntime.log(` ${driftIssue.detail}\n`);
|
||||
if (params.checkTokenDrift) {
|
||||
// Check for token drift before restart (service token vs config token)
|
||||
try {
|
||||
const command = await params.service.readCommand(process.env);
|
||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN;
|
||||
const cfg = loadConfig();
|
||||
const configToken =
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const driftIssue = checkTokenDrift({ serviceToken, configToken });
|
||||
if (driftIssue) {
|
||||
const warning = driftIssue.detail
|
||||
? `${driftIssue.message} ${driftIssue.detail}`
|
||||
: driftIssue.message;
|
||||
warnings.push(warning);
|
||||
if (!json) {
|
||||
defaultRuntime.log(`\n⚠️ ${driftIssue.message}`);
|
||||
if (driftIssue.detail) {
|
||||
defaultRuntime.log(` ${driftIssue.detail}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: token drift check is best-effort
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: token drift check is best-effort
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -46,5 +46,6 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
service: resolveGatewayService(),
|
||||
renderStartHints: renderGatewayServiceStartHints,
|
||||
opts,
|
||||
checkTokenDrift: true,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user