Polish Feynman harness and stabilize Pi web runtime
This commit is contained in:
51
tests/bootstrap-sync.test.ts
Normal file
51
tests/bootstrap-sync.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { syncBundledAssets } from "../src/bootstrap/sync.js";
|
||||
|
||||
function createAppRoot(): string {
|
||||
const appRoot = mkdtempSync(join(tmpdir(), "feynman-app-"));
|
||||
mkdirSync(join(appRoot, ".pi", "themes"), { recursive: true });
|
||||
mkdirSync(join(appRoot, ".pi", "agents"), { recursive: true });
|
||||
writeFileSync(join(appRoot, ".pi", "themes", "feynman.json"), '{"theme":"v1"}\n', "utf8");
|
||||
writeFileSync(join(appRoot, ".pi", "agents", "researcher.md"), "# v1\n", "utf8");
|
||||
return appRoot;
|
||||
}
|
||||
|
||||
test("syncBundledAssets copies missing bundled files", () => {
|
||||
const appRoot = createAppRoot();
|
||||
const home = mkdtempSync(join(tmpdir(), "feynman-home-"));
|
||||
process.env.FEYNMAN_HOME = home;
|
||||
const agentDir = join(home, "agent");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
const result = syncBundledAssets(appRoot, agentDir);
|
||||
|
||||
assert.deepEqual(result.copied.sort(), ["feynman.json", "researcher.md"]);
|
||||
assert.equal(readFileSync(join(agentDir, "themes", "feynman.json"), "utf8"), '{"theme":"v1"}\n');
|
||||
assert.equal(readFileSync(join(agentDir, "agents", "researcher.md"), "utf8"), "# v1\n");
|
||||
});
|
||||
|
||||
test("syncBundledAssets preserves user-modified files and updates managed files", () => {
|
||||
const appRoot = createAppRoot();
|
||||
const home = mkdtempSync(join(tmpdir(), "feynman-home-"));
|
||||
process.env.FEYNMAN_HOME = home;
|
||||
const agentDir = join(home, "agent");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
syncBundledAssets(appRoot, agentDir);
|
||||
|
||||
writeFileSync(join(appRoot, ".pi", "themes", "feynman.json"), '{"theme":"v2"}\n', "utf8");
|
||||
writeFileSync(join(appRoot, ".pi", "agents", "researcher.md"), "# v2\n", "utf8");
|
||||
writeFileSync(join(agentDir, "agents", "researcher.md"), "# user-custom\n", "utf8");
|
||||
|
||||
const result = syncBundledAssets(appRoot, agentDir);
|
||||
|
||||
assert.deepEqual(result.updated, ["feynman.json"]);
|
||||
assert.deepEqual(result.skipped, ["researcher.md"]);
|
||||
assert.equal(readFileSync(join(agentDir, "themes", "feynman.json"), "utf8"), '{"theme":"v2"}\n');
|
||||
assert.equal(readFileSync(join(agentDir, "agents", "researcher.md"), "utf8"), "# user-custom\n");
|
||||
});
|
||||
60
tests/feynman-config.test.ts
Normal file
60
tests/feynman-config.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
configureWebSearchProvider,
|
||||
getConfiguredWebSearchProvider,
|
||||
loadFeynmanConfig,
|
||||
saveFeynmanConfig,
|
||||
} from "../src/config/feynman-config.js";
|
||||
|
||||
test("loadFeynmanConfig falls back to legacy web-search config", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-config-"));
|
||||
const configPath = join(root, "config.json");
|
||||
const legacyDir = join(process.env.HOME ?? root, ".pi");
|
||||
const legacyPath = join(legacyDir, "web-search.json");
|
||||
mkdirSync(legacyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
legacyPath,
|
||||
JSON.stringify({
|
||||
feynmanWebProvider: "perplexity",
|
||||
perplexityApiKey: "legacy-key",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = loadFeynmanConfig(configPath);
|
||||
assert.equal(config.version, 1);
|
||||
assert.equal(config.webSearch?.feynmanWebProvider, "perplexity");
|
||||
assert.equal(config.webSearch?.perplexityApiKey, "legacy-key");
|
||||
});
|
||||
|
||||
test("saveFeynmanConfig persists sessionDir and webSearch", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-config-"));
|
||||
const configPath = join(root, "config.json");
|
||||
const webSearch = configureWebSearchProvider({}, "gemini-browser", { chromeProfile: "Profile 2" });
|
||||
|
||||
saveFeynmanConfig(
|
||||
{
|
||||
version: 1,
|
||||
sessionDir: "/tmp/feynman-sessions",
|
||||
webSearch,
|
||||
},
|
||||
configPath,
|
||||
);
|
||||
|
||||
const config = loadFeynmanConfig(configPath);
|
||||
assert.equal(config.sessionDir, "/tmp/feynman-sessions");
|
||||
assert.equal(config.webSearch?.feynmanWebProvider, "gemini-browser");
|
||||
assert.equal(config.webSearch?.chromeProfile, "Profile 2");
|
||||
});
|
||||
|
||||
test("default web provider falls back to Pi web via gemini-browser", () => {
|
||||
const provider = getConfiguredWebSearchProvider({});
|
||||
|
||||
assert.equal(provider.id, "gemini-browser");
|
||||
assert.equal(provider.runtimeProvider, "gemini");
|
||||
});
|
||||
67
tests/model-harness.test.ts
Normal file
67
tests/model-harness.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { resolveInitialPrompt } from "../src/cli.js";
|
||||
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel } from "../src/model/catalog.js";
|
||||
import { setDefaultModelSpec } from "../src/model/commands.js";
|
||||
|
||||
function createAuthPath(contents: Record<string, unknown>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-auth-"));
|
||||
const authPath = join(root, "auth.json");
|
||||
writeFileSync(authPath, JSON.stringify(contents, null, 2) + "\n", "utf8");
|
||||
return authPath;
|
||||
}
|
||||
|
||||
test("chooseRecommendedModel prefers the strongest authenticated research model", () => {
|
||||
const authPath = createAuthPath({
|
||||
openai: { type: "api_key", key: "openai-test-key" },
|
||||
anthropic: { type: "api_key", key: "anthropic-test-key" },
|
||||
});
|
||||
|
||||
const recommendation = chooseRecommendedModel(authPath);
|
||||
|
||||
assert.equal(recommendation?.spec, "anthropic/claude-opus-4-6");
|
||||
});
|
||||
|
||||
test("setDefaultModelSpec accepts a unique bare model id from authenticated models", () => {
|
||||
const authPath = createAuthPath({
|
||||
openai: { type: "api_key", key: "openai-test-key" },
|
||||
});
|
||||
const settingsPath = join(mkdtempSync(join(tmpdir(), "feynman-settings-")), "settings.json");
|
||||
|
||||
setDefaultModelSpec(settingsPath, authPath, "gpt-5.4");
|
||||
|
||||
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as {
|
||||
defaultProvider?: string;
|
||||
defaultModel?: string;
|
||||
};
|
||||
assert.equal(settings.defaultProvider, "openai");
|
||||
assert.equal(settings.defaultModel, "gpt-5.4");
|
||||
});
|
||||
|
||||
test("buildModelStatusSnapshotFromRecords flags an invalid current model and suggests a replacement", () => {
|
||||
const snapshot = buildModelStatusSnapshotFromRecords(
|
||||
[
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "openai", id: "gpt-5.4" },
|
||||
],
|
||||
[{ provider: "openai", id: "gpt-5.4" }],
|
||||
"anthropic/claude-opus-4-6",
|
||||
);
|
||||
|
||||
assert.equal(snapshot.currentValid, false);
|
||||
assert.equal(snapshot.recommended, "openai/gpt-5.4");
|
||||
assert.ok(snapshot.guidance.some((line) => line.includes("Configured default model is unavailable")));
|
||||
});
|
||||
|
||||
test("resolveInitialPrompt maps top-level research commands to Pi slash workflows", () => {
|
||||
assert.equal(resolveInitialPrompt("lit", ["tool-using", "agents"], undefined), "/lit tool-using agents");
|
||||
assert.equal(resolveInitialPrompt("watch", ["openai"], undefined), "/watch openai");
|
||||
assert.equal(resolveInitialPrompt("jobs", [], undefined), "/jobs");
|
||||
assert.equal(resolveInitialPrompt("chat", ["hello"], undefined), "hello");
|
||||
assert.equal(resolveInitialPrompt("unknown", ["topic"], undefined), "unknown topic");
|
||||
});
|
||||
|
||||
58
tests/pi-runtime.test.ts
Normal file
58
tests/pi-runtime.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js";
|
||||
|
||||
test("buildPiArgs includes configured runtime paths and prompt", () => {
|
||||
const args = buildPiArgs({
|
||||
appRoot: "/repo/feynman",
|
||||
workingDir: "/workspace",
|
||||
sessionDir: "/sessions",
|
||||
feynmanAgentDir: "/home/.feynman/agent",
|
||||
systemPrompt: "system",
|
||||
initialPrompt: "hello",
|
||||
explicitModelSpec: "openai:gpt-5.4",
|
||||
thinkingLevel: "medium",
|
||||
});
|
||||
|
||||
assert.deepEqual(args, [
|
||||
"--session-dir",
|
||||
"/sessions",
|
||||
"--extension",
|
||||
"/repo/feynman/extensions/research-tools.ts",
|
||||
"--skill",
|
||||
"/repo/feynman/skills",
|
||||
"--prompt-template",
|
||||
"/repo/feynman/prompts",
|
||||
"--system-prompt",
|
||||
"system",
|
||||
"--model",
|
||||
"openai:gpt-5.4",
|
||||
"--thinking",
|
||||
"medium",
|
||||
"hello",
|
||||
]);
|
||||
});
|
||||
|
||||
test("buildPiEnv wires Feynman paths into the Pi environment", () => {
|
||||
const env = buildPiEnv({
|
||||
appRoot: "/repo/feynman",
|
||||
workingDir: "/workspace",
|
||||
sessionDir: "/sessions",
|
||||
feynmanAgentDir: "/home/.feynman/agent",
|
||||
systemPrompt: "system",
|
||||
feynmanVersion: "0.1.5",
|
||||
});
|
||||
|
||||
assert.equal(env.PI_CODING_AGENT_DIR, "/home/.feynman/agent");
|
||||
assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions");
|
||||
assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js");
|
||||
assert.equal(env.FEYNMAN_PI_NPM_ROOT, "/repo/feynman/.pi/npm/node_modules");
|
||||
assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory");
|
||||
});
|
||||
|
||||
test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => {
|
||||
const paths = resolvePiPaths("/repo/feynman");
|
||||
|
||||
assert.equal(paths.promisePolyfillPath, "/repo/feynman/dist/system/promise-polyfill.js");
|
||||
});
|
||||
Reference in New Issue
Block a user