fix startup packaging and content guardrails
This commit is contained in:
110
tests/catalog-snapshot.test.ts
Normal file
110
tests/catalog-snapshot.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildModelStatusSnapshotFromRecords } from "../src/model/catalog.js";
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords returns empty guidance when model is set and valid", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[{ provider: "anthropic", id: "claude-opus-4-6" }],
|
||||
[{ provider: "anthropic", id: "claude-opus-4-6" }],
|
||||
"anthropic/claude-opus-4-6",
|
||||
);
|
||||
|
||||
assert.equal(snapshot.currentValid, true);
|
||||
assert.equal(snapshot.current, "anthropic/claude-opus-4-6");
|
||||
assert.equal(snapshot.guidance.length, 0);
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords emits guidance when no models are available", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords([], [], undefined);
|
||||
|
||||
assert.equal(snapshot.currentValid, false);
|
||||
assert.equal(snapshot.current, undefined);
|
||||
assert.equal(snapshot.recommended, undefined);
|
||||
assert.ok(snapshot.guidance.some((line) => line.includes("No authenticated Pi models")));
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords emits guidance when no default model is set", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[{ provider: "openai", id: "gpt-5.4" }],
|
||||
[{ provider: "openai", id: "gpt-5.4" }],
|
||||
undefined,
|
||||
);
|
||||
|
||||
assert.equal(snapshot.currentValid, false);
|
||||
assert.equal(snapshot.current, undefined);
|
||||
assert.ok(snapshot.guidance.some((line) => line.includes("No default research model")));
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords marks provider as configured only when it has available models", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
],
|
||||
[{ provider: "openai", id: "gpt-5.4" }],
|
||||
"openai/gpt-5.4",
|
||||
);
|
||||
|
||||
const anthropicProvider = snapshot.providers.find((provider) => provider.id === "anthropic");
|
||||
const openaiProvider = snapshot.providers.find((provider) => provider.id === "openai");
|
||||
|
||||
assert.ok(anthropicProvider);
|
||||
assert.equal(anthropicProvider!.configured, false);
|
||||
assert.equal(anthropicProvider!.supportedModels, 1);
|
||||
assert.equal(anthropicProvider!.availableModels, 0);
|
||||
|
||||
assert.ok(openaiProvider);
|
||||
assert.equal(openaiProvider!.configured, true);
|
||||
assert.equal(openaiProvider!.supportedModels, 1);
|
||||
assert.equal(openaiProvider!.availableModels, 1);
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords marks provider as current when selected model belongs to it", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
],
|
||||
[
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
],
|
||||
"anthropic/claude-opus-4-6",
|
||||
);
|
||||
|
||||
const anthropicProvider = snapshot.providers.find((provider) => provider.id === "anthropic");
|
||||
const openaiProvider = snapshot.providers.find((provider) => provider.id === "openai");
|
||||
|
||||
assert.equal(anthropicProvider!.current, true);
|
||||
assert.equal(openaiProvider!.current, false);
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords returns available models sorted by research preference", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
],
|
||||
[
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
],
|
||||
undefined,
|
||||
);
|
||||
|
||||
assert.equal(snapshot.availableModels[0], "anthropic/claude-opus-4-6");
|
||||
assert.equal(snapshot.availableModels[1], "openai/gpt-5.4");
|
||||
assert.equal(snapshot.recommended, "anthropic/claude-opus-4-6");
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords sets currentValid false when current model is not in available list", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[{ provider: "anthropic", id: "claude-opus-4-6" }],
|
||||
[],
|
||||
"anthropic/claude-opus-4-6",
|
||||
);
|
||||
|
||||
assert.equal(snapshot.currentValid, false);
|
||||
assert.equal(snapshot.current, "anthropic/claude-opus-4-6");
|
||||
});
|
||||
92
tests/config-paths.test.ts
Normal file
92
tests/config-paths.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import {
|
||||
ensureFeynmanHome,
|
||||
getBootstrapStatePath,
|
||||
getDefaultSessionDir,
|
||||
getFeynmanAgentDir,
|
||||
getFeynmanHome,
|
||||
getFeynmanMemoryDir,
|
||||
getFeynmanStateDir,
|
||||
} from "../src/config/paths.js";
|
||||
|
||||
test("getFeynmanHome uses FEYNMAN_HOME env var when set", () => {
|
||||
const previous = process.env.FEYNMAN_HOME;
|
||||
try {
|
||||
process.env.FEYNMAN_HOME = "/custom/home";
|
||||
assert.equal(getFeynmanHome(), resolve("/custom/home", ".feynman"));
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.FEYNMAN_HOME;
|
||||
} else {
|
||||
process.env.FEYNMAN_HOME = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("getFeynmanHome falls back to homedir when FEYNMAN_HOME is unset", () => {
|
||||
const previous = process.env.FEYNMAN_HOME;
|
||||
try {
|
||||
delete process.env.FEYNMAN_HOME;
|
||||
const home = getFeynmanHome();
|
||||
assert.ok(home.endsWith(".feynman"), `expected path ending in .feynman, got: ${home}`);
|
||||
assert.ok(!home.includes("undefined"), `expected no 'undefined' in path, got: ${home}`);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.FEYNMAN_HOME;
|
||||
} else {
|
||||
process.env.FEYNMAN_HOME = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("getFeynmanAgentDir resolves to <home>/agent", () => {
|
||||
assert.equal(getFeynmanAgentDir("/some/home"), resolve("/some/home", "agent"));
|
||||
});
|
||||
|
||||
test("getFeynmanMemoryDir resolves to <home>/memory", () => {
|
||||
assert.equal(getFeynmanMemoryDir("/some/home"), resolve("/some/home", "memory"));
|
||||
});
|
||||
|
||||
test("getFeynmanStateDir resolves to <home>/.state", () => {
|
||||
assert.equal(getFeynmanStateDir("/some/home"), resolve("/some/home", ".state"));
|
||||
});
|
||||
|
||||
test("getDefaultSessionDir resolves to <home>/sessions", () => {
|
||||
assert.equal(getDefaultSessionDir("/some/home"), resolve("/some/home", "sessions"));
|
||||
});
|
||||
|
||||
test("getBootstrapStatePath resolves to <home>/.state/bootstrap.json", () => {
|
||||
assert.equal(getBootstrapStatePath("/some/home"), resolve("/some/home", ".state", "bootstrap.json"));
|
||||
});
|
||||
|
||||
test("ensureFeynmanHome creates all required subdirectories", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-paths-"));
|
||||
try {
|
||||
const home = join(root, "home");
|
||||
ensureFeynmanHome(home);
|
||||
|
||||
assert.ok(existsSync(home), "home dir should exist");
|
||||
assert.ok(existsSync(join(home, "agent")), "agent dir should exist");
|
||||
assert.ok(existsSync(join(home, "memory")), "memory dir should exist");
|
||||
assert.ok(existsSync(join(home, ".state")), ".state dir should exist");
|
||||
assert.ok(existsSync(join(home, "sessions")), "sessions dir should exist");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("ensureFeynmanHome is idempotent when dirs already exist", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-paths-"));
|
||||
try {
|
||||
const home = join(root, "home");
|
||||
ensureFeynmanHome(home);
|
||||
assert.doesNotThrow(() => ensureFeynmanHome(home));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
32
tests/content-policy.test.ts
Normal file
32
tests/content-policy.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const bannedPatterns = [/ValiChord/i, /Harmony Record/i, /harmony_record_/i];
|
||||
|
||||
function collectMarkdownFiles(root: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
||||
const fullPath = join(root, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectMarkdownFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && fullPath.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
test("bundled prompts and skills do not contain blocked promotional product content", () => {
|
||||
for (const filePath of [...collectMarkdownFiles(join(repoRoot, "prompts")), ...collectMarkdownFiles(join(repoRoot, "skills"))]) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
for (const pattern of bannedPatterns) {
|
||||
assert.doesNotMatch(content, pattern, `${filePath} contains blocked promotional pattern ${pattern}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { applyFeynmanPackageManagerEnv, buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js";
|
||||
import { applyFeynmanPackageManagerEnv, buildPiArgs, buildPiEnv, resolvePiPaths, toNodeImportSpecifier } from "../src/pi/runtime.js";
|
||||
|
||||
test("buildPiArgs includes configured runtime paths and prompt", () => {
|
||||
const args = buildPiArgs({
|
||||
@@ -106,3 +107,11 @@ test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => {
|
||||
|
||||
assert.equal(paths.promisePolyfillPath, "/repo/feynman/dist/system/promise-polyfill.js");
|
||||
});
|
||||
|
||||
test("toNodeImportSpecifier converts absolute preload paths to file URLs", () => {
|
||||
assert.equal(
|
||||
toNodeImportSpecifier("/repo/feynman/dist/system/promise-polyfill.js"),
|
||||
pathToFileURL("/repo/feynman/dist/system/promise-polyfill.js").href,
|
||||
);
|
||||
assert.equal(toNodeImportSpecifier("tsx"), "tsx");
|
||||
});
|
||||
|
||||
48
tests/pi-web-access-patch.test.ts
Normal file
48
tests/pi-web-access-patch.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { patchPiWebAccessSource } from "../scripts/lib/pi-web-access-patch.mjs";
|
||||
|
||||
test("patchPiWebAccessSource rewrites legacy Pi web-search config paths", () => {
|
||||
const input = [
|
||||
'import { join } from "node:path";',
|
||||
'import { homedir } from "node:os";',
|
||||
'const CONFIG_PATH = join(homedir(), ".pi", "web-search.json");',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const patched = patchPiWebAccessSource("perplexity.ts", input);
|
||||
|
||||
assert.match(patched, /FEYNMAN_WEB_SEARCH_CONFIG/);
|
||||
assert.match(patched, /PI_WEB_SEARCH_CONFIG/);
|
||||
});
|
||||
|
||||
test("patchPiWebAccessSource updates index.ts directory handling", () => {
|
||||
const input = [
|
||||
'import { existsSync, mkdirSync } from "node:fs";',
|
||||
'import { join } from "node:path";',
|
||||
'import { homedir } from "node:os";',
|
||||
'const WEB_SEARCH_CONFIG_PATH = join(homedir(), ".pi", "web-search.json");',
|
||||
'const dir = join(homedir(), ".pi");',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const patched = patchPiWebAccessSource("index.ts", input);
|
||||
|
||||
assert.match(patched, /import \{ dirname, join \} from "node:path";/);
|
||||
assert.match(patched, /const dir = dirname\(WEB_SEARCH_CONFIG_PATH\);/);
|
||||
});
|
||||
|
||||
test("patchPiWebAccessSource is idempotent", () => {
|
||||
const input = [
|
||||
'import { join } from "node:path";',
|
||||
'import { homedir } from "node:os";',
|
||||
'const CONFIG_PATH = join(homedir(), ".pi", "web-search.json");',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const once = patchPiWebAccessSource("perplexity.ts", input);
|
||||
const twice = patchPiWebAccessSource("perplexity.ts", once);
|
||||
|
||||
assert.equal(twice, once);
|
||||
});
|
||||
@@ -67,6 +67,17 @@ test("getPiWebAccessStatus reads Gemini routes directly", () => {
|
||||
assert.equal(status.chromeProfile, "Profile 2");
|
||||
});
|
||||
|
||||
test("getPiWebAccessStatus supports the legacy route key", () => {
|
||||
const status = getPiWebAccessStatus({
|
||||
route: "perplexity",
|
||||
perplexityApiKey: "pplx_...",
|
||||
});
|
||||
|
||||
assert.equal(status.routeLabel, "Perplexity");
|
||||
assert.equal(status.requestProvider, "perplexity");
|
||||
assert.equal(status.perplexityConfigured, true);
|
||||
});
|
||||
|
||||
test("formatPiWebAccessDoctorLines reports Pi-managed web access", () => {
|
||||
const lines = formatPiWebAccessDoctorLines(
|
||||
getPiWebAccessStatus({
|
||||
|
||||
28
tests/skill-paths.test.ts
Normal file
28
tests/skill-paths.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const skillsRoot = join(repoRoot, "skills");
|
||||
const markdownPathPattern = /`((?:\.\.?\/)(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\.md)`/g;
|
||||
const simulatedInstallRoot = join(repoRoot, "__skill-install-root__");
|
||||
|
||||
test("all local markdown references in bundled skills resolve in the installed skill layout", () => {
|
||||
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const skillPath = join(skillsRoot, entry.name, "SKILL.md");
|
||||
if (!existsSync(skillPath)) continue;
|
||||
|
||||
const content = readFileSync(skillPath, "utf8");
|
||||
for (const match of content.matchAll(markdownPathPattern)) {
|
||||
const reference = match[1];
|
||||
const installedSkillDir = join(simulatedInstallRoot, entry.name);
|
||||
const installedTarget = resolve(installedSkillDir, reference);
|
||||
const repoTarget = installedTarget.replace(simulatedInstallRoot, repoRoot);
|
||||
assert.ok(existsSync(repoTarget), `${skillPath} references missing installed markdown file ${reference}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user