fix: restore tsc build and plugin install tests

This commit is contained in:
Peter Steinberger
2026-01-31 07:51:26 +00:00
parent c4feb7a457
commit a42e1c82d9
26 changed files with 90 additions and 86 deletions

View File

@@ -1083,10 +1083,11 @@ export function createExecTool(
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
},
);
decision =
const decisionValue =
decisionResult && typeof decisionResult === "object"
? (decisionResult.decision ?? null)
: null;
? (decisionResult as { decision?: unknown }).decision
: undefined;
decision = typeof decisionValue === "string" ? decisionValue : null;
} catch {
emitExecSystemEvent(
`Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${commandText}`,
@@ -1177,28 +1178,32 @@ export function createExecTool(
}
const startedAt = Date.now();
const raw = await callGatewayTool<{
payload: {
exitCode: number;
success?: string;
stdout?: string;
stderr?: string;
error?: string;
};
}>("node.invoke", { timeoutMs: invokeTimeoutMs }, buildInvokeParams(false, null));
const payload = raw?.payload ?? {};
const raw = await callGatewayTool(
"node.invoke",
{ timeoutMs: invokeTimeoutMs },
buildInvokeParams(false, null),
);
const payload =
raw && typeof raw === "object" ? (raw as { payload?: unknown }).payload : undefined;
const payloadObj =
payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
const stdout = typeof payloadObj.stdout === "string" ? payloadObj.stdout : "";
const stderr = typeof payloadObj.stderr === "string" ? payloadObj.stderr : "";
const errorText = typeof payloadObj.error === "string" ? payloadObj.error : "";
const success = typeof payloadObj.success === "boolean" ? payloadObj.success : false;
const exitCode = typeof payloadObj.exitCode === "number" ? payloadObj.exitCode : null;
return {
content: [
{
type: "text",
text: payload.stdout || payload.stderr || payload.error || "",
text: stdout || stderr || errorText || "",
},
],
details: {
status: payload.success ? "completed" : "failed",
exitCode: payload.exitCode ?? null,
status: success ? "completed" : "failed",
exitCode,
durationMs: Date.now() - startedAt,
aggregated: [payload.stdout, payload.stderr, payload.error].filter(Boolean).join("\n"),
aggregated: [stdout, stderr, errorText].filter(Boolean).join("\n"),
cwd: workdir,
} satisfies ExecToolDetails,
};
@@ -1261,10 +1266,11 @@ export function createExecTool(
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
},
);
decision =
const decisionValue =
decisionResult && typeof decisionResult === "object"
? (decisionResult.decision ?? null)
: null;
? (decisionResult as { decision?: unknown }).decision
: undefined;
decision = typeof decisionValue === "string" ? decisionValue : null;
} catch {
emitExecSystemEvent(
`Exec denied (gateway id=${approvalId}, approval-request-failed): ${commandText}`,

View File

@@ -361,7 +361,7 @@ async function mapWithConcurrency<T, R>(
opts?: { onProgress?: (completed: number, total: number) => void },
): Promise<R[]> {
const limit = Math.max(1, Math.floor(concurrency));
const results: R[] = Array.from({ length: items.length });
const results: R[] = Array.from({ length: items.length }, () => undefined as R);
let nextIndex = 0;
let completed = 0;

View File

@@ -869,7 +869,7 @@ export async function runEmbeddedAttempt(
const lastAssistant = messagesSnapshot
.slice()
.toReversed()
.find((m) => m?.role === "assistant");
.find((m) => m.role === "assistant");
const toolMetasNormalized = toolMetas
.filter(

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { SkillsInstallPreferences } from "./skills/types.js";
import type { SkillsInstallPreferences } from "./skills/types.js";
export {
hasBinary,
@@ -38,7 +38,7 @@ export function resolveSkillsInstallPreferences(config?: OpenClawConfig): Skills
const preferBrew = raw?.preferBrew ?? true;
const managerRaw = typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : "";
const manager = managerRaw.toLowerCase();
const nodeManager =
const nodeManager: SkillsInstallPreferences["nodeManager"] =
manager === "pnpm" || manager === "yarn" || manager === "bun" || manager === "npm"
? manager
: "npm";

View File

@@ -382,10 +382,11 @@ export async function runSubagentAnnounceFlow(params: {
},
timeoutMs: waitMs + 2000,
});
const waitError = typeof wait?.error === "string" ? wait.error : undefined;
if (wait?.status === "timeout") {
outcome = { status: "timeout" };
} else if (wait?.status === "error") {
outcome = { status: "error", error: wait.error };
outcome = { status: "error", error: waitError };
} else if (wait?.status === "ok") {
outcome = { status: "ok" };
}

View File

@@ -355,8 +355,9 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
entry.endedAt = Date.now();
mutated = true;
}
const waitError = typeof wait.error === "string" ? wait.error : undefined;
entry.outcome =
wait.status === "error" ? { status: "error", error: wait.error } : { status: "ok" };
wait.status === "error" ? { status: "error", error: waitError } : { status: "ok" };
mutated = true;
if (mutated) {
persistSubagentRuns();

View File

@@ -149,10 +149,10 @@ async function callBrowserProxy(params: {
(typeof payload?.payloadJSON === "string" && payload.payloadJSON
? (JSON.parse(payload.payloadJSON) as BrowserProxyResult)
: null);
if (!parsed || typeof parsed !== "object") {
if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
throw new Error("browser proxy failed");
}
return parsed;
return parsed as BrowserProxyResult;
}
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {

View File

@@ -26,7 +26,7 @@ export function resolveGatewayOptions(opts?: GatewayCallOptions) {
return { url, token, timeoutMs };
}
export async function callGatewayTool<T = unknown>(
export async function callGatewayTool<T = Record<string, unknown>>(
method: string,
opts: GatewayCallOptions,
params?: unknown,

View File

@@ -383,9 +383,8 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
},
timeoutMs: 10_000,
});
if (response?.runId) {
runId = response.runId;
}
const responseRunId = typeof response?.runId === "string" ? response.runId : undefined;
if (responseRunId) runId = responseRunId;
} catch (err) {
const messageText =
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
@@ -405,10 +404,11 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
};
}
if (wait?.status === "error") {
const waitError = typeof wait.error === "string" ? wait.error : "unknown error";
return {
shouldContinue: false,
reply: {
text: `⚠️ Subagent error: ${wait.error ?? "unknown error"} (run ${runId.slice(0, 8)}).`,
text: `⚠️ Subagent error: ${waitError} (run ${runId.slice(0, 8)}).`,
},
};
}

View File

@@ -254,8 +254,7 @@ export function registerGatewayCli(program: Command) {
return;
}
const rich = isRich();
const obj =
result && typeof result === "object" ? (result as Record<string, unknown>) : {};
const obj: Record<string, unknown> = result && typeof result === "object" ? result : {};
const durationMs = typeof obj.durationMs === "number" ? obj.durationMs : null;
defaultRuntime.log(colorize(rich, theme.heading, "Gateway Health"));
defaultRuntime.log(

View File

@@ -107,10 +107,8 @@ export function registerNodesStatusCommands(nodes: Command) {
const connectedOnly = Boolean(opts.connected);
const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected");
const result = await callGatewayCli("node.list", opts, {});
const obj =
typeof result === "object" && result !== null
? (result as Record<string, unknown>)
: {};
const obj: Record<string, unknown> =
typeof result === "object" && result !== null ? result : {};
const { ok, warn, muted } = getNodesTheme();
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now();
@@ -227,10 +225,8 @@ export function registerNodesStatusCommands(nodes: Command) {
return;
}
const obj =
typeof result === "object" && result !== null
? (result as Record<string, unknown>)
: {};
const obj: Record<string, unknown> =
typeof result === "object" && result !== null ? result : {};
const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId;
const connected = Boolean(obj.connected);
const paired = Boolean(obj.paired);

View File

@@ -268,7 +268,7 @@ export async function channelsStatusCommand(
runtime.log(JSON.stringify(payload, null, 2));
return;
}
runtime.log(formatGatewayChannelsStatusLines(payload as Record<string, unknown>).join("\n"));
runtime.log(formatGatewayChannelsStatusLines(payload).join("\n"));
} catch (err) {
runtime.error(`Gateway not reachable: ${String(err)}`);
const cfg = await requireValidConfig(runtime);

View File

@@ -30,7 +30,7 @@ export async function checkGatewayHealth(params: {
if (healthOk) {
try {
const status = await callGateway<Record<string, unknown>>({
const status = await callGateway({
method: "channels.status",
params: { probe: true, timeoutMs: 5000 },
timeoutMs: 6000,

View File

@@ -230,7 +230,7 @@ export async function statusAllCommand(
: { error: gatewayProbe?.error ?? "gateway unreachable" };
const channelsStatus = gatewayReachable
? await callGateway<Record<string, unknown>>({
? await callGateway({
method: "channels.status",
params: { probe: false, timeoutMs: opts?.timeoutMs ?? 10_000 },
timeoutMs: Math.min(8000, opts?.timeoutMs ?? 10_000),

View File

@@ -127,7 +127,7 @@ export async function scanStatus(
progress.setLabel("Querying channel status…");
const channelsStatus = gatewayReachable
? await callGateway<Record<string, unknown>>({
? await callGateway({
method: "channels.status",
params: {
probe: false,

View File

@@ -16,6 +16,14 @@ type DiscordChannelSummary = {
archived?: boolean;
};
type DiscordChannelPayload = {
id?: string;
name?: string;
type?: number;
guild_id?: string;
thread_metadata?: { archived?: boolean };
};
export type DiscordChannelResolution = {
input: string;
resolved: boolean;
@@ -83,27 +91,23 @@ async function listGuildChannels(
fetcher: typeof fetch,
guildId: string,
): Promise<DiscordChannelSummary[]> {
const raw = await fetchDiscord<Array<DiscordChannelSummary>>(
const raw = await fetchDiscord<DiscordChannelPayload[]>(
`/guilds/${guildId}/channels`,
token,
fetcher,
);
return raw
.filter((channel) => Boolean(channel.id) && "name" in channel)
.map((channel) => {
const archived =
"thread_metadata" in channel
? (channel as { thread_metadata?: { archived?: boolean } }).thread_metadata?.archived
: undefined;
const archived = channel.thread_metadata?.archived;
return {
id: channel.id,
name: "name" in channel ? (channel.name ?? "") : "",
id: typeof channel.id === "string" ? channel.id : "",
name: typeof channel.name === "string" ? channel.name : "",
guildId,
type: channel.type,
archived,
};
})
.filter((channel) => Boolean(channel.name));
.filter((channel) => Boolean(channel.id) && Boolean(channel.name));
}
async function fetchChannel(
@@ -111,18 +115,12 @@ async function fetchChannel(
fetcher: typeof fetch,
channelId: string,
): Promise<DiscordChannelSummary | null> {
const raw = await fetchDiscord<DiscordChannelSummary & { guild_id: string }>(
`/channels/${channelId}`,
token,
fetcher,
);
if (!raw || !("guild_id" in raw)) {
return null;
}
const raw = await fetchDiscord<DiscordChannelPayload>(`/channels/${channelId}`, token, fetcher);
if (!raw || typeof raw.guild_id !== "string" || typeof raw.id !== "string") return null;
return {
id: raw.id,
name: "name" in raw ? (raw.name ?? "") : "",
guildId: raw.guild_id ?? "",
name: typeof raw.name === "string" ? raw.name : "",
guildId: raw.guild_id,
type: raw.type,
};
}

View File

@@ -109,7 +109,9 @@ export function buildGatewayConnectionDetails(
};
}
export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promise<T> {
export async function callGateway<T = Record<string, unknown>>(
opts: CallGatewayOptions,
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const config = opts.config ?? loadConfig();
const isRemoteMode = config.gateway?.mode === "remote";

View File

@@ -411,7 +411,7 @@ export class GatewayClient {
return null;
}
async request<T = unknown>(
async request<T = Record<string, unknown>>(
method: string,
params?: unknown,
opts?: { expectFinal?: boolean },

View File

@@ -329,7 +329,7 @@ describeLive("gateway live (cli backend)", () => {
providerId === "codex-cli"
? `Please include the token CLI-BACKEND-${nonce} in your reply.`
: `Reply with exactly: CLI backend OK ${nonce}.`;
const payload = await client.request<Record<string, unknown>>(
const payload = await client.request(
"agent",
{
sessionKey,
@@ -356,7 +356,7 @@ describeLive("gateway live (cli backend)", () => {
providerId === "codex-cli"
? `Please include the token CLI-RESUME-${resumeNonce} in your reply.`
: `Reply with exactly: CLI backend RESUME OK ${resumeNonce}.`;
const resumePayload = await client.request<Record<string, unknown>>(
const resumePayload = await client.request(
"agent",
{
sessionKey,
@@ -383,7 +383,7 @@ describeLive("gateway live (cli backend)", () => {
const imageBase64 = renderCatNoncePngBase64(imageCode);
const runIdImage = randomUUID();
const imageProbe = await client.request<Record<string, unknown>>(
const imageProbe = await client.request(
"agent",
{
sessionKey,

View File

@@ -603,10 +603,10 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
// Ensure session exists + override model for this run.
// Reset between models: avoids cross-provider transcript incompatibilities
// (notably OpenAI Responses requiring reasoning replay for function_call items).
await client.request<Record<string, unknown>>("sessions.reset", {
await client.request("sessions.reset", {
key: sessionKey,
});
await client.request<Record<string, unknown>>("sessions.patch", {
await client.request("sessions.patch", {
key: sessionKey,
model: modelKey,
});
@@ -1164,11 +1164,11 @@ describeLive("gateway live (dev agent, profile keys)", () => {
try {
const sessionKey = `agent:${agentId}:live-zai-fallback`;
await client.request<Record<string, unknown>>("sessions.patch", {
await client.request("sessions.patch", {
key: sessionKey,
model: "anthropic/claude-opus-4-5",
});
await client.request<Record<string, unknown>>("sessions.reset", {
await client.request("sessions.reset", {
key: sessionKey,
});
@@ -1200,7 +1200,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
throw new Error(`anthropic tool probe missing nonce: ${toolText}`);
}
await client.request<Record<string, unknown>>("sessions.patch", {
await client.request("sessions.patch", {
key: sessionKey,
model: "zai/glm-4.7",
});

View File

@@ -109,7 +109,7 @@ describe("gateway e2e", () => {
try {
const sessionKey = "agent:dev:mock-openai";
await client.request<Record<string, unknown>>("sessions.patch", {
await client.request("sessions.patch", {
key: sessionKey,
model: "openai/gpt-5.2",
});

View File

@@ -125,7 +125,7 @@ export async function sendMessageIMessage(
const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
const shouldClose = !opts.client;
try {
const result = await client.request<Record<string, unknown>>("send", params, {
const result = await client.request("send", params, {
timeoutMs: opts.timeoutMs,
});
const resolvedId = resolveMessageId(result);

View File

@@ -106,7 +106,7 @@ describe("installPluginFromArchive", () => {
}),
"utf-8",
);
fs.writeFileSync(path.join(pkgDir, "dist", "index.js"), "export {};", "utf-8");
fs.writeFileSync(path.join(pkgDir, "dist", "index.mjs"), "export {};", "utf-8");
const archivePath = packToArchive({
pkgDir,
@@ -127,7 +127,7 @@ describe("installPluginFromArchive", () => {
expect(result.pluginId).toBe("voice-call");
expect(result.targetDir).toBe(path.join(stateDir, "extensions", "voice-call"));
expect(fs.existsSync(path.join(result.targetDir, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(result.targetDir, "dist", "index.js"))).toBe(true);
expect(fs.existsSync(path.join(result.targetDir, "dist", "index.mjs"))).toBe(true);
});
it("rejects installing when plugin already exists", async () => {
@@ -203,7 +203,7 @@ describe("installPluginFromArchive", () => {
expect(result.pluginId).toBe("zipper");
expect(result.targetDir).toBe(path.join(stateDir, "extensions", "zipper"));
expect(fs.existsSync(path.join(result.targetDir, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(result.targetDir, "dist", "index.js"))).toBe(true);
expect(fs.existsSync(path.join(result.targetDir, "dist", "index.mjs"))).toBe(true);
});
it("allows updates when mode is update", async () => {

View File

@@ -45,10 +45,10 @@ export async function configureGatewayForOnboarding(
10,
);
let bind =
let bind: GatewayWizardSettings["bind"] =
flow === "quickstart"
? quickstartGateway.bind
: await prompter.select({
: await prompter.select<GatewayWizardSettings["bind"]>({
message: "Gateway bind",
options: [
{ value: "loopback", label: "Loopback (127.0.0.1)" },
@@ -107,10 +107,10 @@ export async function configureGatewayForOnboarding(
initialValue: "token",
})) as GatewayAuthChoice);
const tailscaleMode =
const tailscaleMode: GatewayWizardSettings["tailscaleMode"] =
flow === "quickstart"
? quickstartGateway.tailscaleMode
: await prompter.select({
: await prompter.select<GatewayWizardSettings["tailscaleMode"]>({
message: "Tailscale exposure",
options: [
{ value: "off", label: "Off", hint: "No Tailscale exposure" },

View File

@@ -240,7 +240,7 @@ describe("provider timeouts (e2e)", () => {
try {
const sessionKey = "agent:dev:timeout-fallback";
await client.request<Record<string, unknown>>("sessions.patch", {
await client.request("sessions.patch", {
key: sessionKey,
model: "primary/gpt-5.2",
});

View File

@@ -5,6 +5,7 @@
"forceConsistentCasingInFileNames": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["DOM", "DOM.Iterable", "ES2023", "ScriptHost"],
"noEmit": true,
"noEmitOnError": true,
"outDir": "dist",