Fix Feynman onboarding and local install reliability

This commit is contained in:
Advait Paliwal
2026-04-15 13:46:12 -07:00
parent fa259f5cea
commit dd3c07633b
11 changed files with 512 additions and 123 deletions

View File

@@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveInitialPrompt } from "../src/cli.js";
import { resolveInitialPrompt, shouldRunInteractiveSetup } from "../src/cli.js";
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel } from "../src/model/catalog.js";
import { resolveModelProviderForCommand, setDefaultModelSpec } from "../src/model/commands.js";
@@ -118,10 +118,63 @@ test("chooseRecommendedModel prefers MiniMax M2.7 over highspeed when that is th
});
test("resolveInitialPrompt maps top-level research commands to Pi slash workflows", () => {
const workflows = new Set(["lit", "watch", "jobs", "deepresearch"]);
const workflows = new Set([
"lit",
"watch",
"jobs",
"deepresearch",
"review",
"audit",
"replicate",
"compare",
"draft",
"autoresearch",
"summarize",
"log",
]);
assert.equal(resolveInitialPrompt("lit", ["tool-using", "agents"], undefined, workflows), "/lit tool-using agents");
assert.equal(resolveInitialPrompt("watch", ["openai"], undefined, workflows), "/watch openai");
assert.equal(resolveInitialPrompt("jobs", [], undefined, workflows), "/jobs");
assert.equal(resolveInitialPrompt("deepresearch", ["scaling", "laws"], undefined, workflows), "/deepresearch scaling laws");
assert.equal(resolveInitialPrompt("review", ["paper.md"], undefined, workflows), "/review paper.md");
assert.equal(resolveInitialPrompt("audit", ["2401.12345"], undefined, workflows), "/audit 2401.12345");
assert.equal(resolveInitialPrompt("replicate", ["chain-of-thought"], undefined, workflows), "/replicate chain-of-thought");
assert.equal(resolveInitialPrompt("compare", ["tool", "use"], undefined, workflows), "/compare tool use");
assert.equal(resolveInitialPrompt("draft", ["mechanistic", "interp"], undefined, workflows), "/draft mechanistic interp");
assert.equal(resolveInitialPrompt("autoresearch", ["gsm8k"], undefined, workflows), "/autoresearch gsm8k");
assert.equal(resolveInitialPrompt("summarize", ["README.md"], undefined, workflows), "/summarize README.md");
assert.equal(resolveInitialPrompt("log", [], undefined, workflows), "/log");
assert.equal(resolveInitialPrompt("chat", ["hello"], undefined, workflows), "hello");
assert.equal(resolveInitialPrompt("unknown", ["topic"], undefined, workflows), "unknown topic");
});
test("shouldRunInteractiveSetup triggers on first run when no default model is configured", () => {
const authPath = createAuthPath({});
assert.equal(shouldRunInteractiveSetup(undefined, undefined, true, authPath), true);
});
test("shouldRunInteractiveSetup triggers when the configured default model is unavailable", () => {
const authPath = createAuthPath({
openai: { type: "api_key", key: "openai-test-key" },
});
assert.equal(shouldRunInteractiveSetup(undefined, "anthropic/claude-opus-4-6", true, authPath), true);
});
test("shouldRunInteractiveSetup skips onboarding when the configured default model is available", () => {
const authPath = createAuthPath({
openai: { type: "api_key", key: "openai-test-key" },
});
assert.equal(shouldRunInteractiveSetup(undefined, "openai/gpt-5.4", true, authPath), false);
});
test("shouldRunInteractiveSetup skips onboarding for explicit model overrides or non-interactive terminals", () => {
const authPath = createAuthPath({
openai: { type: "api_key", key: "openai-test-key" },
});
assert.equal(shouldRunInteractiveSetup("openai/gpt-5.4", undefined, true, authPath), false);
assert.equal(shouldRunInteractiveSetup(undefined, undefined, false, authPath), false);
});

View File

@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
MAX_NODE_MAJOR,
MIN_NODE_VERSION,
ensureSupportedNodeVersion,
getUnsupportedNodeVersionLines,
@@ -12,6 +13,8 @@ test("isSupportedNodeVersion enforces the exact minimum floor", () => {
assert.equal(isSupportedNodeVersion("20.19.0"), true);
assert.equal(isSupportedNodeVersion("20.19.0"), true);
assert.equal(isSupportedNodeVersion("21.0.0"), true);
assert.equal(isSupportedNodeVersion(`${MAX_NODE_MAJOR}.9.9`), true);
assert.equal(isSupportedNodeVersion(`${MAX_NODE_MAJOR + 1}.0.0`), false);
assert.equal(isSupportedNodeVersion("20.18.1"), false);
assert.equal(isSupportedNodeVersion("18.17.0"), false);
});
@@ -22,7 +25,7 @@ test("ensureSupportedNodeVersion throws a guided upgrade message", () => {
(error: unknown) =>
error instanceof Error &&
error.message.includes(`Node.js ${MIN_NODE_VERSION}`) &&
error.message.includes("nvm install 20 && nvm use 20") &&
error.message.includes("nvm install 22 && nvm use 22") &&
error.message.includes("https://feynman.is/install"),
);
});
@@ -30,6 +33,13 @@ test("ensureSupportedNodeVersion throws a guided upgrade message", () => {
test("unsupported version guidance reports the detected version", () => {
const lines = getUnsupportedNodeVersionLines("18.17.0");
assert.equal(lines[0], "feynman requires Node.js 20.19.0 or later (detected 18.17.0).");
assert.equal(lines[0], `feynman supports Node.js ${MIN_NODE_VERSION} through ${MAX_NODE_MAJOR}.x (detected 18.17.0).`);
assert.ok(lines.some((line) => line.includes("curl -fsSL https://feynman.is/install | bash")));
});
test("unsupported version guidance explains upper-bound failures", () => {
const lines = getUnsupportedNodeVersionLines("25.1.0");
assert.equal(lines[0], `feynman supports Node.js ${MIN_NODE_VERSION} through ${MAX_NODE_MAJOR}.x (detected 25.1.0).`);
assert.ok(lines.some((line) => line.includes("native Pi packages may fail to build")));
});