test: share daemon cli service helpers

This commit is contained in:
Peter Steinberger
2026-03-14 00:10:44 +00:00
parent 68a507ab31
commit c90b10b02f
2 changed files with 84 additions and 93 deletions

View File

@@ -29,6 +29,21 @@ let runServiceRestart: typeof import("./lifecycle-core.js").runServiceRestart;
let runServiceStart: typeof import("./lifecycle-core.js").runServiceStart;
let runServiceStop: typeof import("./lifecycle-core.js").runServiceStop;
function readJsonLog<T extends object>() {
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
return JSON.parse(jsonLine ?? "{}") as T;
}
function createServiceRunArgs(checkTokenDrift?: boolean) {
return {
serviceNoun: "Gateway",
service,
renderStartHints: () => [],
opts: { json: true as const },
...(checkTokenDrift ? { checkTokenDrift } : {}),
};
}
describe("runServiceRestart token drift", () => {
beforeAll(async () => {
({ runServiceRestart, runServiceStart, runServiceStop } = await import("./lifecycle-core.js"));
@@ -53,17 +68,10 @@ describe("runServiceRestart token drift", () => {
});
it("emits drift warning when enabled", async () => {
await runServiceRestart({
serviceNoun: "Gateway",
service,
renderStartHints: () => [],
opts: { json: true },
checkTokenDrift: true,
});
await runServiceRestart(createServiceRunArgs(true));
expect(loadConfig).toHaveBeenCalledTimes(1);
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
const payload = readJsonLog<{ warnings?: string[] }>();
expect(payload.warnings).toEqual(
expect.arrayContaining([expect.stringContaining("gateway install --force")]),
);
@@ -83,16 +91,9 @@ describe("runServiceRestart token drift", () => {
});
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "env-token");
await runServiceRestart({
serviceNoun: "Gateway",
service,
renderStartHints: () => [],
opts: { json: true },
checkTokenDrift: true,
});
await runServiceRestart(createServiceRunArgs(true));
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
const payload = readJsonLog<{ warnings?: string[] }>();
expect(payload.warnings).toEqual(
expect.arrayContaining([expect.stringContaining("gateway install --force")]),
);
@@ -108,8 +109,7 @@ describe("runServiceRestart token drift", () => {
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[] };
const payload = readJsonLog<{ warnings?: string[] }>();
expect(payload.warnings).toBeUndefined();
});
@@ -126,8 +126,7 @@ describe("runServiceRestart token drift", () => {
}),
});
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
const payload = readJsonLog<{ result?: string; message?: string }>();
expect(payload.result).toBe("stopped");
expect(payload.message).toContain("unmanaged process");
expect(service.stop).not.toHaveBeenCalled();
@@ -152,8 +151,7 @@ describe("runServiceRestart token drift", () => {
expect(postRestartCheck).toHaveBeenCalledTimes(1);
expect(service.restart).not.toHaveBeenCalled();
expect(service.readCommand).not.toHaveBeenCalled();
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
const payload = readJsonLog<{ result?: string; message?: string }>();
expect(payload.result).toBe("restarted");
expect(payload.message).toContain("unmanaged process");
});
@@ -172,8 +170,7 @@ describe("runServiceRestart token drift", () => {
expect(result).toBe(true);
expect(postRestartCheck).not.toHaveBeenCalled();
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
const payload = readJsonLog<{ result?: string; message?: string }>();
expect(payload.result).toBe("scheduled");
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
});
@@ -189,8 +186,7 @@ describe("runServiceRestart token drift", () => {
});
expect(service.isLoaded).toHaveBeenCalledTimes(1);
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
const payload = readJsonLog<{ result?: string; message?: string }>();
expect(payload.result).toBe("scheduled");
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
});

View File

@@ -6,6 +6,53 @@ import {
} from "./service-audit.js";
import { buildMinimalServicePath } from "./service-env.js";
function hasIssue(
audit: Awaited<ReturnType<typeof auditGatewayServiceConfig>>,
code: (typeof SERVICE_AUDIT_CODES)[keyof typeof SERVICE_AUDIT_CODES],
) {
return audit.issues.some((issue) => issue.code === code);
}
function createGatewayAudit({
expectedGatewayToken,
path = "/usr/local/bin:/usr/bin:/bin",
serviceToken,
environmentValueSources,
}: {
expectedGatewayToken?: string;
path?: string;
serviceToken?: string;
environmentValueSources?: Record<string, string>;
} = {}) {
return auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
expectedGatewayToken,
command: {
programArguments: ["/usr/bin/node", "gateway"],
environment: {
PATH: path,
...(serviceToken ? { OPENCLAW_GATEWAY_TOKEN: serviceToken } : {}),
},
...(environmentValueSources ? { environmentValueSources } : {}),
},
});
}
function expectTokenAudit(
audit: Awaited<ReturnType<typeof auditGatewayServiceConfig>>,
{
embedded,
mismatch,
}: {
embedded: boolean;
mismatch: boolean;
},
) {
expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayTokenEmbedded)).toBe(embedded);
expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayTokenMismatch)).toBe(mismatch);
}
describe("auditGatewayServiceConfig", () => {
it("flags bun runtime", async () => {
const audit = await auditGatewayServiceConfig({
@@ -66,89 +113,37 @@ describe("auditGatewayServiceConfig", () => {
});
it("flags gateway token mismatch when service token is stale", async () => {
const audit = await auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
const audit = await createGatewayAudit({
expectedGatewayToken: "new-token",
command: {
programArguments: ["/usr/bin/node", "gateway"],
environment: {
PATH: "/usr/local/bin:/usr/bin:/bin",
OPENCLAW_GATEWAY_TOKEN: "old-token",
},
},
serviceToken: "old-token",
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(true);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(true);
expectTokenAudit(audit, { embedded: true, mismatch: true });
});
it("flags embedded service token even when it matches config token", async () => {
const audit = await auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
const audit = await createGatewayAudit({
expectedGatewayToken: "new-token",
command: {
programArguments: ["/usr/bin/node", "gateway"],
environment: {
PATH: "/usr/local/bin:/usr/bin:/bin",
OPENCLAW_GATEWAY_TOKEN: "new-token",
},
},
serviceToken: "new-token",
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(true);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(false);
expectTokenAudit(audit, { embedded: true, mismatch: false });
});
it("does not flag token issues when service token is not embedded", async () => {
const audit = await auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
const audit = await createGatewayAudit({
expectedGatewayToken: "new-token",
command: {
programArguments: ["/usr/bin/node", "gateway"],
environment: {
PATH: "/usr/local/bin:/usr/bin:/bin",
},
},
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(false);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(false);
expectTokenAudit(audit, { embedded: false, mismatch: false });
});
it("does not treat EnvironmentFile-backed tokens as embedded", async () => {
const audit = await auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
const audit = await createGatewayAudit({
expectedGatewayToken: "new-token",
command: {
programArguments: ["/usr/bin/node", "gateway"],
environment: {
PATH: "/usr/local/bin:/usr/bin:/bin",
OPENCLAW_GATEWAY_TOKEN: "old-token",
},
environmentValueSources: {
OPENCLAW_GATEWAY_TOKEN: "file",
},
serviceToken: "old-token",
environmentValueSources: {
OPENCLAW_GATEWAY_TOKEN: "file",
},
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(false);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(false);
expectTokenAudit(audit, { embedded: false, mismatch: false });
});
});