Fix workflow continuation and provider setup gaps
This commit is contained in:
@@ -69,3 +69,31 @@ test("deepresearch workflow requires durable artifacts even when blocked", () =>
|
||||
assert.match(deepResearchPrompt, /Verification: BLOCKED/i);
|
||||
assert.match(deepResearchPrompt, /Never end with only an explanation in chat/i);
|
||||
});
|
||||
|
||||
test("workflow prompts do not introduce implicit confirmation gates", () => {
|
||||
const workflowPrompts = [
|
||||
"audit.md",
|
||||
"compare.md",
|
||||
"deepresearch.md",
|
||||
"draft.md",
|
||||
"lit.md",
|
||||
"review.md",
|
||||
"summarize.md",
|
||||
"watch.md",
|
||||
];
|
||||
const bannedConfirmationGates = [
|
||||
/Do you want to proceed/i,
|
||||
/Wait for confirmation/i,
|
||||
/wait for user confirmation/i,
|
||||
/give them a brief chance/i,
|
||||
/request changes before proceeding/i,
|
||||
];
|
||||
|
||||
for (const fileName of workflowPrompts) {
|
||||
const content = readFileSync(join(repoRoot, "prompts", fileName), "utf8");
|
||||
assert.match(content, /continue (immediately|automatically)/i, `${fileName} should keep running after planning`);
|
||||
for (const pattern of bannedConfirmationGates) {
|
||||
assert.doesNotMatch(content, pattern, `${fileName} contains confirmation gate ${pattern}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,6 +88,15 @@ test("resolveModelProviderForCommand supports LM Studio as a first-class local p
|
||||
assert.equal(resolved?.id, "lm-studio");
|
||||
});
|
||||
|
||||
test("resolveModelProviderForCommand supports LiteLLM as a first-class proxy provider", () => {
|
||||
const authPath = createAuthPath({});
|
||||
|
||||
const resolved = resolveModelProviderForCommand(authPath, "litellm");
|
||||
|
||||
assert.equal(resolved?.kind, "api-key");
|
||||
assert.equal(resolved?.id, "litellm");
|
||||
});
|
||||
|
||||
test("resolveModelProviderForCommand prefers OAuth when a provider supports both auth modes", () => {
|
||||
const authPath = createAuthPath({});
|
||||
|
||||
|
||||
@@ -30,3 +30,45 @@ test("upsertProviderConfig creates models.json and merges provider config", () =
|
||||
assert.equal(parsed.providers.custom.authHeader, true);
|
||||
assert.deepEqual(parsed.providers.custom.models, [{ id: "llama3.1:8b" }]);
|
||||
});
|
||||
|
||||
test("upsertProviderConfig writes LiteLLM proxy config with master key", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "feynman-litellm-"));
|
||||
const modelsPath = join(dir, "models.json");
|
||||
|
||||
const result = upsertProviderConfig(modelsPath, "litellm", {
|
||||
baseUrl: "http://localhost:4000/v1",
|
||||
apiKey: "LITELLM_MASTER_KEY",
|
||||
api: "openai-completions",
|
||||
authHeader: true,
|
||||
models: [{ id: "gpt-4o" }],
|
||||
});
|
||||
assert.deepEqual(result, { ok: true });
|
||||
|
||||
const parsed = JSON.parse(readFileSync(modelsPath, "utf8")) as any;
|
||||
assert.equal(parsed.providers.litellm.baseUrl, "http://localhost:4000/v1");
|
||||
assert.equal(parsed.providers.litellm.apiKey, "LITELLM_MASTER_KEY");
|
||||
assert.equal(parsed.providers.litellm.api, "openai-completions");
|
||||
assert.equal(parsed.providers.litellm.authHeader, true);
|
||||
assert.deepEqual(parsed.providers.litellm.models, [{ id: "gpt-4o" }]);
|
||||
});
|
||||
|
||||
test("upsertProviderConfig writes LiteLLM proxy config without master key", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "feynman-litellm-"));
|
||||
const modelsPath = join(dir, "models.json");
|
||||
|
||||
const result = upsertProviderConfig(modelsPath, "litellm", {
|
||||
baseUrl: "http://localhost:4000/v1",
|
||||
apiKey: "local",
|
||||
api: "openai-completions",
|
||||
authHeader: false,
|
||||
models: [{ id: "llama3" }],
|
||||
});
|
||||
assert.deepEqual(result, { ok: true });
|
||||
|
||||
const parsed = JSON.parse(readFileSync(modelsPath, "utf8")) as any;
|
||||
assert.equal(parsed.providers.litellm.baseUrl, "http://localhost:4000/v1");
|
||||
assert.equal(parsed.providers.litellm.apiKey, "local");
|
||||
assert.equal(parsed.providers.litellm.api, "openai-completions");
|
||||
assert.equal(parsed.providers.litellm.authHeader, false);
|
||||
assert.deepEqual(parsed.providers.litellm.models, [{ id: "llama3" }]);
|
||||
});
|
||||
|
||||
@@ -188,6 +188,46 @@ test("installPackageSources skips native packages on unsupported Node majors bef
|
||||
}
|
||||
});
|
||||
|
||||
test("installPackageSources disables inherited npm dry-run config for child installs", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-"));
|
||||
const workingDir = resolve(root, "project");
|
||||
const agentDir = resolve(root, "agent");
|
||||
const markerPath = resolve(root, "install-env-ok.txt");
|
||||
mkdirSync(workingDir, { recursive: true });
|
||||
|
||||
const scriptPath = writeFakeNpmScript(root, [
|
||||
`import { writeFileSync } from "node:fs";`,
|
||||
`if (process.env.npm_config_dry_run !== "false" || process.env.NPM_CONFIG_DRY_RUN !== "false") process.exit(42);`,
|
||||
`writeFileSync(${JSON.stringify(markerPath)}, "ok\\n", "utf8");`,
|
||||
"process.exit(0);",
|
||||
].join("\n"));
|
||||
|
||||
writeSettings(agentDir, {
|
||||
npmCommand: [process.execPath, scriptPath],
|
||||
});
|
||||
|
||||
const originalLower = process.env.npm_config_dry_run;
|
||||
const originalUpper = process.env.NPM_CONFIG_DRY_RUN;
|
||||
process.env.npm_config_dry_run = "true";
|
||||
process.env.NPM_CONFIG_DRY_RUN = "true";
|
||||
try {
|
||||
const result = await installPackageSources(workingDir, agentDir, ["npm:test-package"]);
|
||||
assert.deepEqual(result.installed, ["npm:test-package"]);
|
||||
assert.equal(existsSync(markerPath), true);
|
||||
} finally {
|
||||
if (originalLower === undefined) {
|
||||
delete process.env.npm_config_dry_run;
|
||||
} else {
|
||||
process.env.npm_config_dry_run = originalLower;
|
||||
}
|
||||
if (originalUpper === undefined) {
|
||||
delete process.env.NPM_CONFIG_DRY_RUN;
|
||||
} else {
|
||||
process.env.NPM_CONFIG_DRY_RUN = originalUpper;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("updateConfiguredPackages batches multiple npm updates into a single install per scope", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-"));
|
||||
const workingDir = resolve(root, "project");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { patchPiSubagentsSource } from "../scripts/lib/pi-subagents-patch.mjs";
|
||||
import { patchPiSubagentsSource, stripPiSubagentBuiltinModelSource } from "../scripts/lib/pi-subagents-patch.mjs";
|
||||
|
||||
const CASES = [
|
||||
{
|
||||
@@ -140,3 +140,22 @@ test("patchPiSubagentsSource rewrites modern agents.ts discovery paths", () => {
|
||||
assert.ok(!patched.includes('loadChainsFromDir(userDirNew, "user")'));
|
||||
assert.ok(!patched.includes('fs.existsSync(userDirNew) ? userDirNew : userDirOld'));
|
||||
});
|
||||
|
||||
test("stripPiSubagentBuiltinModelSource removes built-in model pins", () => {
|
||||
const input = [
|
||||
"---",
|
||||
"name: researcher",
|
||||
"description: Web researcher",
|
||||
"model: anthropic/claude-sonnet-4-6",
|
||||
"tools: read, web_search",
|
||||
"---",
|
||||
"",
|
||||
"Body",
|
||||
].join("\n");
|
||||
|
||||
const patched = stripPiSubagentBuiltinModelSource(input);
|
||||
|
||||
assert.ok(!patched.includes("model: anthropic/claude-sonnet-4-6"));
|
||||
assert.match(patched, /name: researcher/);
|
||||
assert.match(patched, /tools: read, web_search/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user