fix: improve executor model discovery
This commit is contained in:
@@ -183,7 +183,7 @@ agent-workflow web [--port <n>]
|
|||||||
- **`--port <n>`** — specify a custom port to listen on (default: `4317`).
|
- **`--port <n>`** — specify a custom port to listen on (default: `4317`).
|
||||||
- **Security & Scope** — The server binds exclusively to the loopback interface (`127.0.0.1`) for local-only access. It refuses any external or remote listener configurations. Since executor logs can contain sensitive code, configuration details, or tokens/credentials, binding to `127.0.0.1` ensures that the console remains local. Mutation endpoints require same-origin localhost requests, JSON content type, bounded bodies, and validated model values. Credentials are never returned to the browser.
|
- **Security & Scope** — The server binds exclusively to the loopback interface (`127.0.0.1`) for local-only access. It refuses any external or remote listener configurations. Since executor logs can contain sensitive code, configuration details, or tokens/credentials, binding to `127.0.0.1` ensures that the console remains local. Mutation endpoints require same-origin localhost requests, JSON content type, bounded bodies, and validated model values. Credentials are never returned to the browser.
|
||||||
- **Workflow monitoring remains read-only** — The console provides no endpoints to start, stop, decide, or otherwise control workflow runs. It parses and displays execution progress, cycles, timeline, and stdout/stderr output. It cannot recover or display private reasoning tokens/processes of underlying models that are not output to the console.
|
- **Workflow monitoring remains read-only** — The console provides no endpoints to start, stop, decide, or otherwise control workflow runs. It parses and displays execution progress, cycles, timeline, and stdout/stderr output. It cannot recover or display private reasoning tokens/processes of underlying models that are not output to the console.
|
||||||
- **Writable global model settings** — The Chinese “全局执行器模型配置” view can independently set Claude, Agy, and Reasonix models. Discovery uses the current CLI environment: Claude via Anthropic `/v1/models` (with configured-default fallback), Agy via `agy models`, and Reasonix via `reasonix doctor --json` provider names. Discovery failures are isolated per executor, and manual entry always remains available. Saving atomically updates only `executors.<kind>.model` in `~/.agent-workflow/config.json`, preserves unrelated fields, and clears an override when the value is emptied. Active executors are not interrupted; saved models take effect on later cycles, scope remediation, extensions, and executor retries through the existing reload path. Project configuration continues to override global values.
|
- **Writable global model settings** — The Chinese “全局执行器模型配置” view can independently set Claude, Agy, and Reasonix models. Discovery uses the current CLI environment: Claude via Anthropic `/v1/models` (with configured-default fallback), Agy via `agy models`, and Reasonix via `reasonix doctor --json` provider/model entries. Reasonix candidates use the CLI-compatible `provider/model` form, with provider-only fallback when no concrete model is listed. CLI discovery allows up to 15 seconds for executor startup; HTTP model discovery remains bounded separately. Discovery failures are isolated per executor, and manual entry always remains available. Saving atomically updates only `executors.<kind>.model` in `~/.agent-workflow/config.json`, preserves unrelated fields, and clears an override when the value is emptied. Active executors are not interrupted; saved models take effect on later cycles, scope remediation, extensions, and executor retries through the existing reload path. Project configuration continues to override global values.
|
||||||
- **Interaction with `logs --follow`** — The web server directly reads the persisted run files without taking any logs follow locks or interfering with the main executor loop, allowing you to use both the CLI `logs --follow` and the web console concurrently.
|
- **Interaction with `logs --follow`** — The web server directly reads the persisted run files without taking any logs follow locks or interfering with the main executor loop, allowing you to use both the CLI `logs --follow` and the web console concurrently.
|
||||||
|
|
||||||
### `agent-workflow abort`
|
### `agent-workflow abort`
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { extractModelIds, discoverExecutorModels } from "./workflow-model-discovery.js";
|
import { extractModelIds, extractReasonixModels, discoverExecutorModels } from "./workflow-model-discovery.js";
|
||||||
import { saveGlobalExecutorModels, loadGlobalWorkflowConfig } from "./workflow-config.js";
|
import { saveGlobalExecutorModels, loadGlobalWorkflowConfig } from "./workflow-config.js";
|
||||||
|
|
||||||
describe("workflow-model-discovery helpers", () => {
|
describe("workflow-model-discovery helpers", () => {
|
||||||
@@ -24,6 +24,31 @@ describe("workflow-model-discovery helpers", () => {
|
|||||||
],
|
],
|
||||||
})).toEqual(["model-a", "model-b"]);
|
})).toEqual(["model-a", "model-b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts concrete Reasonix provider/model ids", () => {
|
||||||
|
expect(extractReasonixModels({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
name: "custom-opencode-ai",
|
||||||
|
models: ["deepseek-v4-flash", "deepseek-v4-pro"],
|
||||||
|
},
|
||||||
|
{ name: "anthropic", models: [] },
|
||||||
|
],
|
||||||
|
})).toEqual([
|
||||||
|
"custom-opencode-ai/deepseek-v4-flash",
|
||||||
|
"custom-opencode-ai/deepseek-v4-pro",
|
||||||
|
"anthropic",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports object model entries and legacy top-level models", () => {
|
||||||
|
expect(extractReasonixModels({
|
||||||
|
providers: [{ name: "gateway", models: [{ id: "model-a" }, { name: "model-b" }] }],
|
||||||
|
})).toEqual(["gateway/model-a", "gateway/model-b"]);
|
||||||
|
expect(extractReasonixModels({
|
||||||
|
models: [{ provider: "gateway", id: "model-c" }, "provider-only"],
|
||||||
|
})).toEqual(["gateway/model-c", "provider-only"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("saveGlobalExecutorModels", () => {
|
describe("saveGlobalExecutorModels", () => {
|
||||||
@@ -115,6 +140,25 @@ describe("discoverExecutorModels isolation", () => {
|
|||||||
savedVars[key] = process.env[key];
|
savedVars[key] = process.env[key];
|
||||||
delete process.env[key];
|
delete process.env[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const binDir = path.join(tmpHome, "bin");
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const agyBin = path.join(binDir, "agy");
|
||||||
|
const reasonixBin = path.join(binDir, "reasonix");
|
||||||
|
fs.writeFileSync(agyBin, "#!/bin/sh\nprintf '%s\\n' 'Test Agy Model'\n", { mode: 0o755 });
|
||||||
|
fs.writeFileSync(reasonixBin, `#!/bin/sh
|
||||||
|
printf '%s\\n' '{"providers":[{"name":"test-provider","models":["test-model"]}]}'
|
||||||
|
`, { mode: 0o755 });
|
||||||
|
|
||||||
|
const configDir = path.join(tmpHome, ".agent-workflow");
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
executors: {
|
||||||
|
agy: { binary: agyBin },
|
||||||
|
reasonix: { binary: reasonixBin },
|
||||||
|
},
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -148,8 +192,11 @@ describe("discoverExecutorModels isolation", () => {
|
|||||||
expect(result.claude.error).toBeTruthy();
|
expect(result.claude.error).toBeTruthy();
|
||||||
expect(JSON.stringify(result)).not.toContain("secret-key-value");
|
expect(JSON.stringify(result)).not.toContain("secret-key-value");
|
||||||
expect(JSON.stringify(result)).not.toContain("user:pass");
|
expect(JSON.stringify(result)).not.toContain("user:pass");
|
||||||
expect(result.agy.status === "ok" || result.agy.status === "error").toBe(true);
|
expect(result.agy).toMatchObject({ status: "ok", models: ["Test Agy Model"] });
|
||||||
expect(result.reasonix.status === "ok" || result.reasonix.status === "error").toBe(true);
|
expect(result.reasonix).toMatchObject({
|
||||||
|
status: "ok",
|
||||||
|
models: ["test-provider/test-model"],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves Claude settings from ~/.claude/settings.json when process env is unset", async () => {
|
it("resolves Claude settings from ~/.claude/settings.json when process env is unset", async () => {
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import type { ExecutorConfig, ExecutorKind } from "./workflow-types.js";
|
|||||||
import { EXECUTOR_KINDS } from "./workflow-types.js";
|
import { EXECUTOR_KINDS } from "./workflow-types.js";
|
||||||
import { loadGlobalWorkflowConfig } from "./workflow-config.js";
|
import { loadGlobalWorkflowConfig } from "./workflow-config.js";
|
||||||
|
|
||||||
/** Bounded discovery timeout so web requests cannot hang indefinitely. */
|
/** Bounded HTTP discovery timeout so web requests cannot hang indefinitely. */
|
||||||
const DISCOVERY_TIMEOUT_MS = 3_000;
|
const HTTP_DISCOVERY_TIMEOUT_MS = 3_000;
|
||||||
|
/** CLI discovery can take longer while the executor loads its configuration. */
|
||||||
|
const CLI_DISCOVERY_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
export interface ExecutorModelDiscovery {
|
export interface ExecutorModelDiscovery {
|
||||||
status: "ok" | "error";
|
status: "ok" | "error";
|
||||||
@@ -141,7 +143,7 @@ function cleanError(err: unknown, secrets: string[] = []): string {
|
|||||||
function runCommand(
|
function runCommand(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
timeoutMs = DISCOVERY_TIMEOUT_MS,
|
timeoutMs = CLI_DISCOVERY_TIMEOUT_MS,
|
||||||
): Promise<{ status: number | null; stdout: string; stderr: string }> {
|
): Promise<{ status: number | null; stdout: string; stderr: string }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(command, args, {
|
const child = spawn(command, args, {
|
||||||
@@ -227,30 +229,56 @@ export function extractModelIds(data: unknown): string[] {
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractReasonixProviders(data: unknown): string[] {
|
export function extractReasonixModels(data: unknown): string[] {
|
||||||
if (!data || typeof data !== "object") return [];
|
if (!data || typeof data !== "object") return [];
|
||||||
const obj = data as Record<string, unknown>;
|
const obj = data as Record<string, unknown>;
|
||||||
const providers: string[] = [];
|
const models: string[] = [];
|
||||||
|
|
||||||
if (Array.isArray(obj.providers)) {
|
if (Array.isArray(obj.providers)) {
|
||||||
for (const provider of obj.providers) {
|
for (const provider of obj.providers) {
|
||||||
if (provider && typeof provider === "object" && typeof (provider as Record<string, unknown>).name === "string") {
|
if (!provider || typeof provider !== "object") continue;
|
||||||
providers.push((provider as Record<string, unknown>).name as string);
|
const providerObj = provider as Record<string, unknown>;
|
||||||
|
const name = typeof providerObj.name === "string" ? providerObj.name.trim() : "";
|
||||||
|
if (!name) continue;
|
||||||
|
|
||||||
|
if (Array.isArray(providerObj.models)) {
|
||||||
|
for (const model of providerObj.models) {
|
||||||
|
if (typeof model === "string" && model.trim()) {
|
||||||
|
models.push(`${name}/${model.trim()}`);
|
||||||
|
} else if (model && typeof model === "object") {
|
||||||
|
const modelObj = model as Record<string, unknown>;
|
||||||
|
const id = typeof modelObj.id === "string"
|
||||||
|
? modelObj.id.trim()
|
||||||
|
: typeof modelObj.name === "string" ? modelObj.name.trim() : "";
|
||||||
|
if (id) models.push(`${name}/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providers.length === 0 && Array.isArray(obj.models)) {
|
// Some providers only expose a provider name. Keep it selectable as a
|
||||||
|
// fallback because Reasonix accepts provider names for these configs.
|
||||||
|
if (!Array.isArray(providerObj.models) || providerObj.models.length === 0) {
|
||||||
|
models.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (models.length === 0 && Array.isArray(obj.models)) {
|
||||||
for (const item of obj.models) {
|
for (const item of obj.models) {
|
||||||
if (typeof item === "string") {
|
if (typeof item === "string") {
|
||||||
providers.push(item);
|
models.push(item);
|
||||||
} else if (item && typeof item === "object" && typeof (item as Record<string, unknown>).provider === "string") {
|
} else if (item && typeof item === "object" && typeof (item as Record<string, unknown>).provider === "string") {
|
||||||
providers.push((item as Record<string, unknown>).provider as string);
|
const modelObj = item as Record<string, unknown>;
|
||||||
|
const provider = (modelObj.provider as string).trim();
|
||||||
|
const model = typeof modelObj.id === "string"
|
||||||
|
? modelObj.id.trim()
|
||||||
|
: typeof modelObj.name === "string" ? modelObj.name.trim() : "";
|
||||||
|
models.push(model ? `${provider}/${model}` : provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...new Set(providers)];
|
return [...new Set(models)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLAUDE_DEFAULTS = [
|
const CLAUDE_DEFAULTS = [
|
||||||
@@ -275,7 +303,7 @@ async function discoverClaudeModels(config: ExecutorConfig): Promise<ExecutorMod
|
|||||||
try {
|
try {
|
||||||
const url = new URL("/v1/models", settings.baseUrl);
|
const url = new URL("/v1/models", settings.baseUrl);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
|
const timer = setTimeout(() => controller.abort(), HTTP_DISCOVERY_TIMEOUT_MS);
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": "2023-06-01",
|
||||||
@@ -354,8 +382,8 @@ async function discoverReasonixModels(config: ExecutorConfig): Promise<ExecutorM
|
|||||||
throw new Error(`Failed to parse reasonix doctor output: ${cleanError(err)}`);
|
throw new Error(`Failed to parse reasonix doctor output: ${cleanError(err)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = extractReasonixProviders(data);
|
const models = extractReasonixModels(data);
|
||||||
return { status: "ok", models: providers.length > 0 ? providers : defaults, defaults };
|
return { status: "ok", models: models.length > 0 ? models : defaults, defaults };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { status: "error", error: cleanError(err), defaults };
|
return { status: "error", error: cleanError(err), defaults };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -975,7 +975,7 @@ export function getAssetsHtml(): string {
|
|||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label for="model-input-reasonix">手动输入模型</label>
|
<label for="model-input-reasonix">手动输入模型</label>
|
||||||
<input id="model-input-reasonix" class="search-input" type="text" placeholder="例如 anthropic 或留空使用默认">
|
<input id="model-input-reasonix" class="search-input" type="text" placeholder="例如 anthropic 或留空使用默认">
|
||||||
<div class="settings-help">Reasonix 使用 provider 名称作为 --model;通过 reasonix doctor --json 发现。</div>
|
<div class="settings-help">通过 reasonix doctor --json 发现 provider/model;没有具体模型时使用 provider 名称。</div>
|
||||||
<div class="settings-error" id="error-reasonix"></div>
|
<div class="settings-error" id="error-reasonix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+26
-19
@@ -36,6 +36,8 @@ function makeRequest(
|
|||||||
describe("workflow-web", () => {
|
describe("workflow-web", () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
|
let originalHome: string | undefined;
|
||||||
|
let testHome: string;
|
||||||
let port = 14319;
|
let port = 14319;
|
||||||
let baseUrl = `http://127.0.0.1:${port}`;
|
let baseUrl = `http://127.0.0.1:${port}`;
|
||||||
let runId = "test-run-123";
|
let runId = "test-run-123";
|
||||||
@@ -46,6 +48,27 @@ describe("workflow-web", () => {
|
|||||||
// Setup temp workflows root directory
|
// Setup temp workflows root directory
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-web-test-"));
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-web-test-"));
|
||||||
process.env.AGENT_WORKFLOW_DIR = tmpDir;
|
process.env.AGENT_WORKFLOW_DIR = tmpDir;
|
||||||
|
originalHome = process.env.HOME;
|
||||||
|
testHome = path.join(tmpDir, "home");
|
||||||
|
process.env.HOME = testHome;
|
||||||
|
|
||||||
|
const binDir = path.join(testHome, "bin");
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const agyBin = path.join(binDir, "agy");
|
||||||
|
const reasonixBin = path.join(binDir, "reasonix");
|
||||||
|
fs.writeFileSync(agyBin, "#!/bin/sh\nprintf '%s\\n' 'Test Agy Model'\n", { mode: 0o755 });
|
||||||
|
fs.writeFileSync(reasonixBin, `#!/bin/sh
|
||||||
|
printf '%s\\n' '{"providers":[{"name":"test-provider","models":["test-model"]}]}'
|
||||||
|
`, { mode: 0o755 });
|
||||||
|
const configDir = path.join(testHome, ".agent-workflow");
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
executors: {
|
||||||
|
agy: { binary: agyBin },
|
||||||
|
reasonix: { binary: reasonixBin },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const runDir = path.join(tmpDir, hash, runId);
|
const runDir = path.join(tmpDir, hash, runId);
|
||||||
fs.mkdirSync(runDir, { recursive: true });
|
fs.mkdirSync(runDir, { recursive: true });
|
||||||
@@ -92,6 +115,8 @@ describe("workflow-web", () => {
|
|||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
|
if (originalHome === undefined) delete process.env.HOME;
|
||||||
|
else process.env.HOME = originalHome;
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@@ -267,10 +292,6 @@ describe("workflow-web", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("accepts legitimate model values with spaces and punctuation", async () => {
|
it("accepts legitimate model values with spaces and punctuation", async () => {
|
||||||
const previousHome = process.env.HOME;
|
|
||||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-web-model-ok-"));
|
|
||||||
process.env.HOME = tmpHome;
|
|
||||||
try {
|
|
||||||
const res = await makeRequest(`${baseUrl}/api/config/models`, "POST", {
|
const res = await makeRequest(`${baseUrl}/api/config/models`, "POST", {
|
||||||
"Host": "127.0.0.1:14319",
|
"Host": "127.0.0.1:14319",
|
||||||
"Origin": "http://127.0.0.1:14319",
|
"Origin": "http://127.0.0.1:14319",
|
||||||
@@ -279,11 +300,6 @@ describe("workflow-web", () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
const saved = JSON.parse(res.body);
|
const saved = JSON.parse(res.body);
|
||||||
expect(saved.current.agy).toBe("Claude Sonnet 4.6 (Thinking)");
|
expect(saved.current.agy).toBe("Claude Sonnet 4.6 (Thinking)");
|
||||||
} finally {
|
|
||||||
if (previousHome === undefined) delete process.env.HOME;
|
|
||||||
else process.env.HOME = previousHome;
|
|
||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
it("returns an observable 413 for oversized bodies instead of resetting the socket", async () => {
|
it("returns an observable 413 for oversized bodies instead of resetting the socket", async () => {
|
||||||
@@ -297,10 +313,6 @@ describe("workflow-web", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("saves and clears global model overrides through the API", async () => {
|
it("saves and clears global model overrides through the API", async () => {
|
||||||
const previousHome = process.env.HOME;
|
|
||||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-web-config-"));
|
|
||||||
process.env.HOME = tmpHome;
|
|
||||||
try {
|
|
||||||
const saveRes = await makeRequest(`${baseUrl}/api/config/models`, "POST", {
|
const saveRes = await makeRequest(`${baseUrl}/api/config/models`, "POST", {
|
||||||
"Host": "127.0.0.1:14319",
|
"Host": "127.0.0.1:14319",
|
||||||
"Origin": "http://127.0.0.1:14319",
|
"Origin": "http://127.0.0.1:14319",
|
||||||
@@ -311,7 +323,7 @@ describe("workflow-web", () => {
|
|||||||
expect(saved.current.claude).toBe("claude-sonnet-4");
|
expect(saved.current.claude).toBe("claude-sonnet-4");
|
||||||
expect(saved.current.reasonix).toBe("anthropic");
|
expect(saved.current.reasonix).toBe("anthropic");
|
||||||
|
|
||||||
const configPath = path.join(tmpHome, ".agent-workflow", "config.json");
|
const configPath = path.join(testHome, ".agent-workflow", "config.json");
|
||||||
expect(fs.existsSync(configPath)).toBe(true);
|
expect(fs.existsSync(configPath)).toBe(true);
|
||||||
const onDisk = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
const onDisk = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
expect(onDisk.executors.claude.model).toBe("claude-sonnet-4");
|
expect(onDisk.executors.claude.model).toBe("claude-sonnet-4");
|
||||||
@@ -326,11 +338,6 @@ describe("workflow-web", () => {
|
|||||||
const cleared = JSON.parse(clearRes.body);
|
const cleared = JSON.parse(clearRes.body);
|
||||||
expect(cleared.current.claude).toBeNull();
|
expect(cleared.current.claude).toBeNull();
|
||||||
expect(cleared.current.reasonix).toBeNull();
|
expect(cleared.current.reasonix).toBeNull();
|
||||||
} finally {
|
|
||||||
if (previousHome === undefined) delete process.env.HOME;
|
|
||||||
else process.env.HOME = previousHome;
|
|
||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
it("supports HEAD request", async () => {
|
it("supports HEAD request", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user