mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
Tests: align pnpm test expectations with main (#67001)
Merged via squash.
Prepared head SHA: 29c8068053
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/Matrix: split Matrix live QA into a source-linked `qa-matrix` runner and keep repo-private `qa-*` surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.
|
||||
- Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new `models.authStatus` gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.
|
||||
- GitHub Copilot/memory search: add a GitHub Copilot embedding provider for memory search, and expose a dedicated Copilot embedding host helper so plugins can reuse the transport while honoring remote overrides, token refresh, and safer payload validation. (#61718) Thanks @feiskyer and @vincentkoc.
|
||||
- Tests: align pnpm test expectations with main (#67001). Thanks @hxy91819
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
80
extensions/qa-channel/src/bus-client.test.ts
Normal file
80
extensions/qa-channel/src/bus-client.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { createServer } from "node:http";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getQaBusState, pollQaBus } from "./bus-client.js";
|
||||
|
||||
async function startJsonServer(
|
||||
handler: (req: { url?: string | undefined }) => { statusCode?: number; body: string },
|
||||
) {
|
||||
const server = createServer((req, res) => {
|
||||
const response = handler({ url: req.url });
|
||||
res.writeHead(response.statusCode ?? 200, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(response.body);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("test server failed to bind");
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
async stop() {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("qa-bus client", () => {
|
||||
const stops: Array<() => Promise<void>> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(stops.splice(0).map((stop) => stop()));
|
||||
});
|
||||
|
||||
it("rejects malformed JSON responses instead of throwing from the stream callback", async () => {
|
||||
const server = await startJsonServer(() => ({
|
||||
body: '{"cursor":1,"events":[',
|
||||
}));
|
||||
stops.push(server.stop);
|
||||
|
||||
await expect(
|
||||
pollQaBus({
|
||||
baseUrl: server.baseUrl,
|
||||
accountId: "acct-a",
|
||||
cursor: 0,
|
||||
timeoutMs: 0,
|
||||
}),
|
||||
).rejects.toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it("preserves baseUrl path prefixes when composing bus URLs", async () => {
|
||||
const server = await startJsonServer((req) => ({
|
||||
statusCode: req.url === "/qa-bus/v1/state" ? 200 : 404,
|
||||
body:
|
||||
req.url === "/qa-bus/v1/state"
|
||||
? JSON.stringify({
|
||||
cursor: 1,
|
||||
conversations: [],
|
||||
threads: [],
|
||||
messages: [],
|
||||
events: [],
|
||||
})
|
||||
: JSON.stringify({ error: `unexpected path: ${req.url}` }),
|
||||
}));
|
||||
stops.push(server.stop);
|
||||
|
||||
await expect(getQaBusState(`${server.baseUrl}/qa-bus`)).resolves.toMatchObject({
|
||||
cursor: 1,
|
||||
events: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import type {
|
||||
QaBusConversation,
|
||||
QaBusEvent,
|
||||
@@ -32,27 +34,78 @@ export type {
|
||||
|
||||
type JsonResult<T> = Promise<T>;
|
||||
|
||||
function buildQaBusUrl(baseUrl: string, path: string): URL {
|
||||
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
||||
return new URL(path.replace(/^\/+/, ""), normalizedBaseUrl);
|
||||
}
|
||||
|
||||
async function postJson<T>(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: unknown,
|
||||
signal?: AbortSignal,
|
||||
): JsonResult<T> {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
const url = buildQaBusUrl(baseUrl, path);
|
||||
const payload = JSON.stringify(body);
|
||||
const client = url.protocol === "https:" ? https : http;
|
||||
|
||||
return await new Promise<T>((resolve, reject) => {
|
||||
const abortError = () =>
|
||||
Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
|
||||
if (signal?.aborted) {
|
||||
reject(abortError());
|
||||
return;
|
||||
}
|
||||
|
||||
const request = client.request(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"content-length": Buffer.byteLength(payload),
|
||||
connection: "close",
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
response.on("data", (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
response.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
let parsed: T | { error?: string };
|
||||
try {
|
||||
parsed = text ? (JSON.parse(text) as T | { error?: string }) : ({} as T);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if ((response.statusCode ?? 500) < 200 || (response.statusCode ?? 500) >= 300) {
|
||||
const error =
|
||||
typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : undefined;
|
||||
reject(new Error(error || `qa-bus request failed: ${response.statusCode ?? 500}`));
|
||||
return;
|
||||
}
|
||||
resolve(parsed as T);
|
||||
});
|
||||
response.on("error", reject);
|
||||
},
|
||||
);
|
||||
|
||||
const onAbort = () => {
|
||||
request.destroy(abortError());
|
||||
};
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
request.on("error", (error) => {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
});
|
||||
request.on("close", () => {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
});
|
||||
request.end(payload);
|
||||
});
|
||||
const payload = (await response.json()) as T | { error?: string };
|
||||
if (!response.ok) {
|
||||
const error =
|
||||
typeof payload === "object" && payload && "error" in payload ? payload.error : undefined;
|
||||
throw new Error(error || `qa-bus request failed: ${response.status}`);
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export function normalizeQaTarget(raw: string): string | undefined {
|
||||
@@ -218,7 +271,7 @@ export async function injectQaBusInboundMessage(params: {
|
||||
}
|
||||
|
||||
export async function getQaBusState(baseUrl: string): Promise<QaBusStateSnapshot> {
|
||||
const response = await fetch(`${baseUrl}/v1/state`);
|
||||
const response = await fetch(buildQaBusUrl(baseUrl, "/v1/state"));
|
||||
if (!response.ok) {
|
||||
throw new Error(`qa-bus request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { extractToolPayload } from "../../../src/infra/outbound/tool-payload.js";
|
||||
import {
|
||||
resetPluginRuntimeStateForTest,
|
||||
setActivePluginRegistry,
|
||||
} from "../../../src/plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../../src/test-utils/channel-plugins.js";
|
||||
import { createStartAccountContext } from "../../../test/helpers/plugins/start-account-context.js";
|
||||
import { createQaBusState, startQaBusServer } from "../../qa-lab/api.js";
|
||||
import { qaChannelPlugin } from "../api.js";
|
||||
import { setQaChannelRuntime } from "../api.js";
|
||||
import { qaChannelPlugin, setQaChannelRuntime } from "../api.js";
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
function installQaChannelTestRegistry() {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "qa-channel", plugin: qaChannelPlugin, source: "test" }]),
|
||||
);
|
||||
}
|
||||
|
||||
function createMockQaRuntime(params?: {
|
||||
onDispatch?: (ctx: Record<string, unknown>) => void;
|
||||
@@ -71,6 +85,7 @@ function createMockQaRuntime(params?: {
|
||||
|
||||
describe("qa-channel plugin", () => {
|
||||
it("roundtrips inbound DM traffic through the qa bus", { timeout: 20_000 }, async () => {
|
||||
installQaChannelTestRegistry();
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
setQaChannelRuntime(createMockQaRuntime());
|
||||
@@ -120,6 +135,7 @@ describe("qa-channel plugin", () => {
|
||||
});
|
||||
|
||||
it("stages inbound image attachments into agent media payload", { timeout: 20_000 }, async () => {
|
||||
installQaChannelTestRegistry();
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
let dispatchedCtx: Record<string, unknown> | null = null;
|
||||
@@ -200,6 +216,7 @@ describe("qa-channel plugin", () => {
|
||||
});
|
||||
|
||||
it("exposes thread and message actions against the qa bus", async () => {
|
||||
installQaChannelTestRegistry();
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
|
||||
@@ -306,6 +323,7 @@ describe("qa-channel plugin", () => {
|
||||
});
|
||||
|
||||
it("routes the advertised send action to the qa bus", async () => {
|
||||
installQaChannelTestRegistry();
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeAccountId } from "./bus-queries.js";
|
||||
import type { QaBusState } from "./bus-state.js";
|
||||
import type {
|
||||
QaBusCreateThreadInput,
|
||||
@@ -134,16 +135,17 @@ export async function handleQaBusRequest(params: {
|
||||
case "/v1/poll": {
|
||||
const input = body as unknown as QaBusPollInput;
|
||||
const timeoutMs = Math.max(0, Math.min(input.timeoutMs ?? 0, 30_000));
|
||||
const accountId = normalizeAccountId(input.accountId);
|
||||
const initial = params.state.poll(input);
|
||||
if (initial.events.length > 0 || timeoutMs === 0) {
|
||||
writeJson(params.res, 200, initial);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await params.state.waitFor({
|
||||
kind: "event-kind",
|
||||
eventKind: "inbound-message",
|
||||
timeoutMs,
|
||||
await params.state.waitForCursorAdvance(input.cursor ?? 0, timeoutMs, (snapshot) => {
|
||||
return snapshot.events.some(
|
||||
(event) => event.accountId === accountId && event.cursor > (input.cursor ?? 0),
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
// timeout ok for long-poll
|
||||
|
||||
@@ -92,6 +92,50 @@ describe("qa-bus state", () => {
|
||||
).rejects.toThrow("qa-bus wait timeout");
|
||||
});
|
||||
|
||||
it("keeps account-scoped cursor waits blocked on unrelated account traffic", async () => {
|
||||
const state = createQaBusState();
|
||||
const pending = state.waitForCursorAdvance(0, 500, (snapshot) => {
|
||||
return snapshot.events.some((event) => event.accountId === "acct-a" && event.cursor > 0);
|
||||
});
|
||||
|
||||
state.addInboundMessage({
|
||||
accountId: "acct-b",
|
||||
conversation: { id: "other", kind: "direct" },
|
||||
senderId: "acct-b-user",
|
||||
text: "unrelated",
|
||||
});
|
||||
|
||||
const beforeMatch = await Promise.race([
|
||||
pending.then(() => "resolved"),
|
||||
new Promise((resolve) => setTimeout(() => resolve("still-waiting"), 20)),
|
||||
]);
|
||||
expect(beforeMatch).toBe("still-waiting");
|
||||
|
||||
state.addInboundMessage({
|
||||
accountId: "acct-a",
|
||||
conversation: { id: "target", kind: "direct" },
|
||||
senderId: "acct-a-user",
|
||||
text: "matched",
|
||||
});
|
||||
|
||||
await expect(pending).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("wakes default-account cursor waits when accountId is omitted", async () => {
|
||||
const state = createQaBusState();
|
||||
const pending = state.waitForCursorAdvance(0, 500, (snapshot) => {
|
||||
return snapshot.events.some((event) => event.accountId === "default" && event.cursor > 0);
|
||||
});
|
||||
|
||||
state.addInboundMessage({
|
||||
conversation: { id: "target", kind: "direct" },
|
||||
senderId: "default-user",
|
||||
text: "matched",
|
||||
});
|
||||
|
||||
await expect(pending).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves inline attachments and lets search match attachment metadata", () => {
|
||||
const state = createQaBusState();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
QaBusReadMessageInput,
|
||||
QaBusReactToMessageInput,
|
||||
QaBusSearchMessagesInput,
|
||||
QaBusStateSnapshot,
|
||||
QaBusThread,
|
||||
QaBusWaitForInput,
|
||||
} from "./runtime-api.js";
|
||||
@@ -282,6 +283,13 @@ export function createQaBusState() {
|
||||
async waitFor(input: QaBusWaitForInput) {
|
||||
return await waiters.waitFor(input);
|
||||
},
|
||||
async waitForCursorAdvance(
|
||||
afterCursor: number,
|
||||
timeoutMs: number,
|
||||
shouldResolve?: (snapshot: QaBusStateSnapshot) => boolean,
|
||||
) {
|
||||
return await waiters.waitForCursorAdvance(afterCursor, timeoutMs, shouldResolve);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@ type Waiter = {
|
||||
matcher: (snapshot: QaBusStateSnapshot) => QaBusWaitMatch | null;
|
||||
};
|
||||
|
||||
type CursorWaiter = {
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
timer: NodeJS.Timeout;
|
||||
afterCursor: number;
|
||||
shouldResolve?: (snapshot: QaBusStateSnapshot) => boolean;
|
||||
};
|
||||
|
||||
function createQaBusMatcher(
|
||||
input: QaBusWaitForInput,
|
||||
): (snapshot: QaBusStateSnapshot) => QaBusWaitMatch | null {
|
||||
@@ -39,6 +47,7 @@ function createQaBusMatcher(
|
||||
|
||||
export function createQaBusWaiterStore(getSnapshot: () => QaBusStateSnapshot) {
|
||||
const waiters = new Set<Waiter>();
|
||||
const cursorWaiters = new Set<CursorWaiter>();
|
||||
|
||||
return {
|
||||
reset(reason = "qa-bus reset") {
|
||||
@@ -47,9 +56,14 @@ export function createQaBusWaiterStore(getSnapshot: () => QaBusStateSnapshot) {
|
||||
waiter.reject(new Error(reason));
|
||||
}
|
||||
waiters.clear();
|
||||
for (const waiter of cursorWaiters) {
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.reject(new Error(reason));
|
||||
}
|
||||
cursorWaiters.clear();
|
||||
},
|
||||
settle() {
|
||||
if (waiters.size === 0) {
|
||||
if (waiters.size === 0 && cursorWaiters.size === 0) {
|
||||
return;
|
||||
}
|
||||
const snapshot = getSnapshot();
|
||||
@@ -62,6 +76,17 @@ export function createQaBusWaiterStore(getSnapshot: () => QaBusStateSnapshot) {
|
||||
waiters.delete(waiter);
|
||||
waiter.resolve(match);
|
||||
}
|
||||
for (const waiter of Array.from(cursorWaiters)) {
|
||||
if (snapshot.cursor <= waiter.afterCursor) {
|
||||
continue;
|
||||
}
|
||||
if (waiter.shouldResolve && !waiter.shouldResolve(snapshot)) {
|
||||
continue;
|
||||
}
|
||||
clearTimeout(waiter.timer);
|
||||
cursorWaiters.delete(waiter);
|
||||
waiter.resolve();
|
||||
}
|
||||
},
|
||||
async waitFor(input: QaBusWaitForInput) {
|
||||
const matcher = createQaBusMatcher(input);
|
||||
@@ -83,5 +108,28 @@ export function createQaBusWaiterStore(getSnapshot: () => QaBusStateSnapshot) {
|
||||
waiters.add(waiter);
|
||||
});
|
||||
},
|
||||
async waitForCursorAdvance(
|
||||
afterCursor: number,
|
||||
timeoutMs: number,
|
||||
shouldResolve?: (snapshot: QaBusStateSnapshot) => boolean,
|
||||
) {
|
||||
const snapshot = getSnapshot();
|
||||
if (snapshot.cursor > afterCursor && (!shouldResolve || shouldResolve(snapshot))) {
|
||||
return;
|
||||
}
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
const waiter: CursorWaiter = {
|
||||
resolve,
|
||||
reject,
|
||||
afterCursor,
|
||||
shouldResolve,
|
||||
timer: setTimeout(() => {
|
||||
cursorWaiters.delete(waiter);
|
||||
reject(new Error(`qa-bus wait timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs),
|
||||
};
|
||||
cursorWaiters.add(waiter);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ describe("qa multipass runtime", () => {
|
||||
const fakeCodexHome = path.join(fakeHome, ".codex");
|
||||
fs.mkdirSync(fakeCodexHome, { recursive: true });
|
||||
vi.stubEnv("HOME", "");
|
||||
vi.stubEnv("CODEX_HOME", "");
|
||||
vi.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||
|
||||
try {
|
||||
|
||||
@@ -280,8 +280,9 @@ function resolveQaLiveCliAuthEnv(baseEnv: NodeJS.ProcessEnv) {
|
||||
const codexHome = resolveUserPath(configuredCodexHome, baseEnv);
|
||||
return fs.existsSync(codexHome) ? { CODEX_HOME: codexHome } : {};
|
||||
}
|
||||
const hostHome = baseEnv.HOME?.trim() || os.homedir();
|
||||
const codexHome = path.join(hostHome, ".codex");
|
||||
const hostHome = baseEnv.HOME?.trim();
|
||||
const effectiveHome = hostHome || os.homedir();
|
||||
const codexHome = path.join(effectiveHome, ".codex");
|
||||
return fs.existsSync(codexHome) ? { CODEX_HOME: codexHome } : {};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ import "./test-helpers/fast-openclaw-tools.js";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
|
||||
describe("createOpenClawCodingTools", () => {
|
||||
const testConfig: OpenClawConfig = {};
|
||||
|
||||
it("preserves action enums in normalized schemas", () => {
|
||||
const defaultTools = createOpenClawCodingTools({ senderIsOwner: true });
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
const toolNames = ["canvas", "nodes", "cron", "gateway", "message"];
|
||||
const missingNames = toolNames.filter(
|
||||
(name) => !defaultTools.some((candidate) => candidate.name === name),
|
||||
@@ -56,18 +58,20 @@ describe("createOpenClawCodingTools", () => {
|
||||
}
|
||||
});
|
||||
it("enforces apply_patch availability and canonical names across model/provider constraints", () => {
|
||||
const defaultTools = createOpenClawCodingTools({ senderIsOwner: true });
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
expect(defaultTools.some((tool) => tool.name === "exec")).toBe(true);
|
||||
expect(defaultTools.some((tool) => tool.name === "process")).toBe(true);
|
||||
expect(defaultTools.some((tool) => tool.name === "apply_patch")).toBe(false);
|
||||
|
||||
const openAiTools = createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
});
|
||||
expect(openAiTools.some((tool) => tool.name === "apply_patch")).toBe(true);
|
||||
|
||||
const codexTools = createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
});
|
||||
@@ -116,6 +120,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(denied.some((tool) => tool.name === "apply_patch")).toBe(false);
|
||||
|
||||
const oauthTools = createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
modelProvider: "anthropic",
|
||||
modelAuthMode: "oauth",
|
||||
});
|
||||
@@ -127,7 +132,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(names.has("apply_patch")).toBe(false);
|
||||
});
|
||||
it("provides top-level object schemas for all tools", () => {
|
||||
const tools = createOpenClawCodingTools();
|
||||
const tools = createOpenClawCodingTools({ config: testConfig });
|
||||
const offenders = tools
|
||||
.map((tool) => {
|
||||
const schema =
|
||||
|
||||
@@ -120,6 +120,8 @@ describe("skills-install fallback edge cases", () => {
|
||||
});
|
||||
|
||||
it("handles sudo probe failures for go install without apt fallback", async () => {
|
||||
vi.spyOn(process, "getuid").mockReturnValue(1000);
|
||||
|
||||
for (const testCase of [
|
||||
{
|
||||
label: "sudo returns password required",
|
||||
@@ -130,8 +132,9 @@ describe("skills-install fallback edge cases", () => {
|
||||
stderr: "sudo: a password is required",
|
||||
}),
|
||||
assert: (result: { message: string; stderr: string }) => {
|
||||
expect(result.message).toContain("sudo");
|
||||
expect(result.message).toContain("sudo is not usable");
|
||||
expect(result.message).toContain("https://go.dev/doc/install");
|
||||
expect(result.stderr).toContain("sudo: a password is required");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -142,6 +145,7 @@ describe("skills-install fallback edge cases", () => {
|
||||
),
|
||||
assert: (result: { message: string; stderr: string }) => {
|
||||
expect(result.message).toContain("sudo is not usable");
|
||||
expect(result.message).toContain("https://go.dev/doc/install");
|
||||
expect(result.stderr).toContain("Executable not found");
|
||||
},
|
||||
},
|
||||
@@ -167,6 +171,7 @@ describe("skills-install fallback edge cases", () => {
|
||||
});
|
||||
|
||||
it("status-selected go installer fails gracefully when apt fallback needs sudo", async () => {
|
||||
vi.spyOn(process, "getuid").mockReturnValue(1000);
|
||||
mockAvailableBinaries(["apt-get", "sudo"]);
|
||||
|
||||
runCommandWithTimeoutMock.mockResolvedValueOnce({
|
||||
@@ -187,6 +192,7 @@ describe("skills-install fallback edge cases", () => {
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("sudo is not usable");
|
||||
expect(result.stderr).toContain("sudo: a password is required");
|
||||
});
|
||||
|
||||
it("uv not installed and no brew returns helpful error without curl auto-install", async () => {
|
||||
|
||||
@@ -339,7 +339,7 @@ describe("timestampOptsFromConfig", () => {
|
||||
{
|
||||
name: "falls back gracefully with empty config",
|
||||
cfg: {} as any,
|
||||
expected: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
expected: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
},
|
||||
])("$name", ({ cfg, expected }) => {
|
||||
expect(timestampOptsFromConfig(cfg).timezone).toBe(expected);
|
||||
|
||||
@@ -1355,8 +1355,8 @@ describe("installPluginFromArchive", () => {
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_FAILED);
|
||||
expect(result.error).toContain("manifest dependency scan could not read");
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain("plain-crypto-js");
|
||||
expect(result.error).toContain("vendor/sealed");
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
renderBundledRootHelpText,
|
||||
writeCliStartupMetadata,
|
||||
} from "../../scripts/write-cli-startup-metadata.ts";
|
||||
import { writeCliStartupMetadata } from "../../scripts/write-cli-startup-metadata.ts";
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
describe("write-cli-startup-metadata", () => {
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
it("captures bundled root help text from the CLI program", async () => {
|
||||
const rootHelpText = await renderBundledRootHelpText();
|
||||
|
||||
expect(rootHelpText).toContain("Usage:");
|
||||
expect(rootHelpText).toContain("openclaw");
|
||||
});
|
||||
|
||||
it("writes startup metadata with populated root help text", async () => {
|
||||
it("writes startup metadata with populated root help text when dist falls back to source rendering", async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-");
|
||||
const distDir = path.join(tempRoot, "dist");
|
||||
const extensionsDir = path.join(tempRoot, "extensions");
|
||||
|
||||
Reference in New Issue
Block a user