import { describe, expect, it } from "vitest"; import { auditGatewayServiceConfig, checkTokenDrift, SERVICE_AUDIT_CODES, } from "./service-audit.js"; import { buildMinimalServicePath } from "./service-env.js"; describe("auditGatewayServiceConfig", () => { it("flags bun runtime", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "darwin", command: { programArguments: ["/opt/homebrew/bin/bun", "gateway"], environment: { PATH: "/usr/bin:/bin" }, }, }); expect(audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun)).toBe( true, ); }); it("flags version-managed node paths", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "darwin", command: { programArguments: ["/Users/test/.nvm/versions/node/v22.0.0/bin/node", "gateway"], environment: { PATH: "/usr/bin:/bin:/Users/test/.nvm/versions/node/v22.0.0/bin", }, }, }); expect( audit.issues.some( (issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager, ), ).toBe(true); expect( audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathNonMinimal), ).toBe(true); expect( audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs), ).toBe(true); }); it("accepts Linux minimal PATH with user directories", async () => { const env = { HOME: "/home/testuser", PNPM_HOME: "/opt/pnpm" }; const minimalPath = buildMinimalServicePath({ platform: "linux", env }); const audit = await auditGatewayServiceConfig({ env, platform: "linux", command: { programArguments: ["/usr/bin/node", "gateway"], environment: { PATH: minimalPath }, }, }); expect( audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathNonMinimal), ).toBe(false); expect( audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs), ).toBe(false); }); it("flags gateway token mismatch when service token is stale", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "linux", expectedGatewayToken: "new-token", command: { programArguments: ["/usr/bin/node", "gateway"], environment: { PATH: "/usr/local/bin:/usr/bin:/bin", OPENCLAW_GATEWAY_TOKEN: "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); }); it("flags embedded service token even when it matches config token", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "linux", expectedGatewayToken: "new-token", command: { programArguments: ["/usr/bin/node", "gateway"], environment: { PATH: "/usr/local/bin:/usr/bin:/bin", OPENCLAW_GATEWAY_TOKEN: "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); }); it("does not flag token issues when service token is not embedded", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "linux", 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); }); it("does not treat EnvironmentFile-backed tokens as embedded", async () => { const audit = await auditGatewayServiceConfig({ env: { HOME: "/tmp" }, platform: "linux", 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", }, }, }); 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); }); }); describe("checkTokenDrift", () => { it("returns null when both tokens are undefined", () => { const result = checkTokenDrift({ serviceToken: undefined, configToken: undefined }); expect(result).toBeNull(); }); it("returns null when both tokens are empty strings", () => { const result = checkTokenDrift({ serviceToken: "", configToken: "" }); expect(result).toBeNull(); }); it("returns null when tokens match", () => { const result = checkTokenDrift({ serviceToken: "same-token", configToken: "same-token" }); expect(result).toBeNull(); }); it("returns null when tokens match but service token has trailing newline", () => { const result = checkTokenDrift({ serviceToken: "same-token\n", configToken: "same-token" }); expect(result).toBeNull(); }); it("returns null when tokens match but have surrounding whitespace", () => { const result = checkTokenDrift({ serviceToken: " same-token ", configToken: "same-token" }); expect(result).toBeNull(); }); it("returns null when both tokens have different whitespace padding", () => { const result = checkTokenDrift({ serviceToken: "same-token\r\n", configToken: " same-token ", }); expect(result).toBeNull(); }); it("detects drift when config has token but service has different token", () => { const result = checkTokenDrift({ serviceToken: "old-token", configToken: "new-token" }); expect(result).not.toBeNull(); expect(result?.code).toBe(SERVICE_AUDIT_CODES.gatewayTokenDrift); expect(result?.message).toContain("differs from service token"); }); it("returns null when config has token but service has no token", () => { const result = checkTokenDrift({ serviceToken: undefined, configToken: "new-token" }); expect(result).toBeNull(); }); it("returns null when service has token but config does not", () => { // This is not really drift - service will work, just config is incomplete const result = checkTokenDrift({ serviceToken: "service-token", configToken: undefined }); expect(result).toBeNull(); }); });