Skip to content

Commit c1f6dab

Browse files
author
A.R.
committed
test: regression tests for #5/#6 fixes + adjust existing tests
- Add is-main-module.test.js covering symlink resolution (issue #6.1) - Add vault.test.js cases for probeKeychainState caching and PERPLEXITY_DISABLE_KEYCHAIN=1 escape hatch (issue #6.3) - Add config-getSavedCookies.test.js for diagnostic logging (issue #5.3) - Update stdio-daemon-proxy.test.ts to expect empty env (issue #5.1) - Update auto-config.test.ts to expect empty env for OpenCode (issue #5.1) - Fix checks/vault.js to import probeKeychainState from vault.js - Harden tryKeytar() against vitest mock proxies: validate getPassword is a function inside the try-catch so proxy-access errors are caught. Refs issues #5, #6
1 parent c841124 commit c1f6dab

7 files changed

Lines changed: 159 additions & 11 deletions

File tree

packages/extension/tests/auto-config.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,7 @@ describe("2026-05 IDE expansion", () => {
191191
type: "local",
192192
command: ["C:/node.exe", "C:/bundle/server.mjs"],
193193
enabled: true,
194-
environment: {
195-
PERPLEXITY_HEADLESS_ONLY: "1",
196-
},
194+
environment: {},
197195
});
198196
expect(nextConfig.mcp?.Perplexity.args).toBeUndefined();
199197
expect(nextConfig.mcp?.Perplexity.env).toBeUndefined();

packages/extension/tests/transports/stdio-daemon-proxy.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("stdioDaemonProxyBuilder", () => {
3131
expect(result).toEqual({
3232
command: "node",
3333
args: ["/home/user/.perplexity-mcp/launcher.cjs"],
34-
env: { PERPLEXITY_HEADLESS_ONLY: "1" },
34+
env: {},
3535
});
3636
// Critical: proxy variant must NOT force the launcher into no-daemon mode.
3737
expect("env" in result ? result.env : {}).not.toHaveProperty("PERPLEXITY_NO_DAEMON");
@@ -67,7 +67,6 @@ describe("stdioDaemonProxyBuilder", () => {
6767
expect(env.PERPLEXITY_CHROME_PATH).toBe(
6868
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
6969
);
70-
expect(env.PERPLEXITY_HEADLESS_ONLY).toBe("1");
7170
expect(env).not.toHaveProperty("PERPLEXITY_NO_DAEMON");
7271
});
7372

@@ -108,7 +107,7 @@ describe("stdioDaemonProxyBuilder", () => {
108107
expect(result).toEqual({
109108
command: "node",
110109
args: ["/home/user/.perplexity-mcp/launcher.cjs"],
111-
env: { PERPLEXITY_HEADLESS_ONLY: "1" },
110+
env: {},
112111
});
113112

114113
const env = "env" in result ? result.env ?? {} : {};

packages/mcp-server/src/checks/vault.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync } from "node:fs";
22
import { join } from "node:path";
3+
import { probeKeychainState } from "../vault.js";
34

45
const CATEGORY = "vault";
56

packages/mcp-server/src/vault.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const scryptAsync = promisify(nodeScrypt);
7171
// sets the seam, so the floor remains enforced. Cleared by `__resetKeyCache`.
7272
let _kdfParamsOverride = null;
7373
let _kdfTestModeActive = false;
74-
_keytarModuleCache = null;
74+
let _keytarModuleCache = null;
7575

7676
/**
7777
* TEST SEAM — drop scrypt cost during tests by setting (logN, r, p).
@@ -321,9 +321,6 @@ const KEYTAR_ACCOUNT = "vault-master-key";
321321
let _keyCache = null;
322322
let _unsealMaterialCache = null;
323323

324-
/** Module-level keytar probe cache. `null` = not checked yet; `false` = unavailable. */
325-
let _keytarModuleCache = null;
326-
327324
/** Environment escape hatch to suppress all keychain access (issue #6 bug 3). */
328325
function isKeychainDisabled() {
329326
return process.env.PERPLEXITY_DISABLE_KEYCHAIN === "1";
@@ -348,6 +345,10 @@ async function tryKeytar() {
348345
try {
349346
const mod = await import("keytar");
350347
const keytar = mod.default ?? mod;
348+
if (!keytar || typeof keytar.getPassword !== "function") {
349+
_keytarModuleCache = false;
350+
return null;
351+
}
351352
_keytarModuleCache = keytar;
352353
return keytar;
353354
} catch {
@@ -390,7 +391,7 @@ export async function probeKeychainState() {
390391
return { available: false, hasKey: false };
391392
}
392393
const keytar = await tryKeytar();
393-
if (!keytar) {
394+
if (!keytar || typeof keytar.getPassword !== "function") {
394395
return { available: false, hasKey: false };
395396
}
396397
try {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2+
import { mkdtempSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
6+
describe("getSavedCookies diagnostic logging (issue #5.3)", () => {
7+
let TMP;
8+
let capturedErrors;
9+
let originalConsoleError;
10+
11+
beforeEach(() => {
12+
TMP = mkdtempSync(join(tmpdir(), "pplx-config-"));
13+
process.env.PERPLEXITY_CONFIG_DIR = TMP;
14+
process.env.PERPLEXITY_PROFILE = "default";
15+
delete process.env.PERPLEXITY_SESSION_TOKEN;
16+
delete process.env.PERPLEXITY_CSRF_TOKEN;
17+
capturedErrors = [];
18+
originalConsoleError = console.error;
19+
console.error = (...args) => {
20+
capturedErrors.push(args.join(" "));
21+
};
22+
});
23+
24+
afterEach(() => {
25+
rmSync(TMP, { recursive: true, force: true });
26+
delete process.env.PERPLEXITY_CONFIG_DIR;
27+
delete process.env.PERPLEXITY_PROFILE;
28+
delete process.env.PERPLEXITY_VAULT_PASSPHRASE;
29+
console.error = originalConsoleError;
30+
});
31+
32+
it("logs 'no vault.enc' when vault is missing", async () => {
33+
const { getSavedCookies } = await import("../src/config.js");
34+
const cookies = await getSavedCookies();
35+
expect(cookies).toEqual([]);
36+
expect(capturedErrors.some((c) => /no vault\.enc/.test(c))).toBe(true);
37+
expect(capturedErrors.some((c) => /run login first/.test(c))).toBe(true);
38+
});
39+
40+
it("logs 'cookies key absent' when vault exists but has no cookies", async () => {
41+
const { createProfile } = await import("../src/profiles.js");
42+
const { Vault, __resetKeyCache } = await import("../src/vault.js");
43+
createProfile("default");
44+
__resetKeyCache();
45+
process.env.PERPLEXITY_VAULT_PASSPHRASE = "test-passphrase";
46+
const v = new Vault();
47+
await v.set("default", "email", "test@example.com");
48+
49+
vi.resetModules();
50+
const { getSavedCookies } = await import("../src/config.js");
51+
const cookies = await getSavedCookies();
52+
expect(cookies).toEqual([]);
53+
expect(capturedErrors.some((c) => c.includes("'cookies' key is absent"))).toBe(true);
54+
});
55+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import { isMainModule } from "../src/is-main-module.js";
3+
import { writeFileSync, symlinkSync, realpathSync, mkdirSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
7+
const TMP = join(tmpdir(), "perplexity-is-main-module-test");
8+
9+
function cleanTmp() {
10+
try { rmSync(TMP, { recursive: true }); } catch { /* ignore */ }
11+
}
12+
13+
describe("isMainModule", () => {
14+
beforeEach(() => {
15+
cleanTmp();
16+
mkdirSync(TMP, { recursive: true });
17+
});
18+
afterEach(() => {
19+
cleanTmp();
20+
delete process.argv[1];
21+
});
22+
23+
it("returns true when argv[1] matches import.meta.url exactly", () => {
24+
const realFile = join(TMP, "real.mjs");
25+
writeFileSync(realFile, "");
26+
process.argv[1] = realFile;
27+
const metaUrl = "file://" + realFile.replace(/\\/g, "/");
28+
expect(isMainModule(metaUrl)).toBe(true);
29+
});
30+
31+
it("returns false when argv[1] points at a different file", () => {
32+
const realFile = join(TMP, "real.mjs");
33+
const otherFile = join(TMP, "other.mjs");
34+
writeFileSync(realFile, "");
35+
writeFileSync(otherFile, "");
36+
process.argv[1] = otherFile;
37+
const metaUrl = "file://" + realFile.replace(/\\/g, "/");
38+
expect(isMainModule(metaUrl)).toBe(false);
39+
});
40+
41+
it("returns true when argv[1] is a symlink to the real file (issue #6)", () => {
42+
const realFile = join(TMP, "real.mjs");
43+
const linkFile = join(TMP, "link.mjs");
44+
writeFileSync(realFile, "");
45+
symlinkSync(realFile, linkFile);
46+
process.argv[1] = linkFile;
47+
const metaUrl = "file://" + realFile.replace(/\\/g, "/");
48+
expect(isMainModule(metaUrl)).toBe(true);
49+
});
50+
51+
it("returns false when argv[1] is missing", () => {
52+
delete process.argv[1];
53+
expect(isMainModule("file:///any/path.mjs")).toBe(false);
54+
});
55+
});

packages/mcp-server/test/vault.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,3 +1340,42 @@ describe("v3 migration", () => {
13401340
}
13411341
});
13421342
});
1343+
1344+
describe("probeKeychainState — caching + escape hatch (issue #6.3)", () => {
1345+
beforeEach(() => {
1346+
__resetKeyCache();
1347+
delete process.env.PERPLEXITY_DISABLE_KEYCHAIN;
1348+
vi.doUnmock("keytar");
1349+
});
1350+
afterEach(() => {
1351+
vi.doUnmock("keytar");
1352+
delete process.env.PERPLEXITY_DISABLE_KEYCHAIN;
1353+
});
1354+
1355+
it("returns false immediately when PERPLEXITY_DISABLE_KEYCHAIN=1", async () => {
1356+
const { probeKeychainState } = await import("../src/vault.js");
1357+
process.env.PERPLEXITY_DISABLE_KEYCHAIN = "1";
1358+
const result = await probeKeychainState();
1359+
expect(result).toEqual({ available: false, hasKey: false });
1360+
});
1361+
1362+
it("caches keytar load result so repeated probes don't re-import", async () => {
1363+
let importCount = 0;
1364+
vi.doMock("keytar", () => {
1365+
importCount++;
1366+
return {
1367+
default: {
1368+
getPassword: async () => null,
1369+
setPassword: async () => undefined,
1370+
},
1371+
};
1372+
});
1373+
const { probeKeychainState } = await import("../src/vault.js");
1374+
__resetKeyCache(); // clear any prior cache
1375+
await probeKeychainState();
1376+
await probeKeychainState();
1377+
await probeKeychainState();
1378+
// keytar should only be imported once because the result is cached
1379+
expect(importCount).toBe(1);
1380+
});
1381+
});

0 commit comments

Comments
 (0)