mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
16 Commits
codex/pr-8
...
v2026.4.20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd05f4e89 | ||
|
|
bfde3c98a4 | ||
|
|
835de92b7a | ||
|
|
2020e63bd2 | ||
|
|
b835337cd6 | ||
|
|
7e4a5f8a6e | ||
|
|
8b3ddb28cd | ||
|
|
ca245b8621 | ||
|
|
2db45c7892 | ||
|
|
8ce7c4f08b | ||
|
|
87b81fa66f | ||
|
|
e57e54e591 | ||
|
|
adef75c1e1 | ||
|
|
ed6ccc9923 | ||
|
|
c4ddaf63fd | ||
|
|
c127812bba |
@@ -1,4 +1,4 @@
|
||||
e3a16ceb9e933c5b707b717c18a1d9d50f98e687a98e6c35f4f3a290f7036a62 config-baseline.json
|
||||
ae1ab87635e7bf613c84fee04425af901ceeb67fb5dbcf1c74095aa00a59ee88 config-baseline.core.json
|
||||
e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json
|
||||
8fb3a1cf5fe56ab8fc2cb46341c3403aed32b0d1f0aaeac0e96cd3599db4f06e config-baseline.plugin.json
|
||||
aa12edd01845f5cabac04befcd258371b2c3b4c95203a5fe540fe871af5334ab config-baseline.json
|
||||
7956c319e82d288d496a51cb2ff4485ab72ef4900cb089f99e1df8b9ef3bfb73 config-baseline.core.json
|
||||
702f21ae56b489422dd9a0ea64a982822bfce0145c3a53315d15a2f8f91baf92 config-baseline.channel.json
|
||||
17a73724e5082b3aa846c220d38115916fb6003887439e6794510a99fc73f7de config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
f135ddc1802b7f8b2d29bf495fd0ac1f497a89bab8164ca8c7c8f18efc010e6e plugin-sdk-api-baseline.json
|
||||
a47d06095ec5c3701a94888a11e89700d8a8511db46fa3122fb9407e160707b6 plugin-sdk-api-baseline.jsonl
|
||||
c923c90f11cc188755b341778fb8975ff6ff8714ebf305189babd2953fcd21fa plugin-sdk-api-baseline.json
|
||||
6f43b0998f301dad7a68803f2863bca581c24edcd4e917cd5afac79accb46472 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -26,6 +26,14 @@ describe("qa model-switch evaluation", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts concise handed-off phrasing from live models", () => {
|
||||
expect(
|
||||
hasModelSwitchContinuityEvidence(
|
||||
"The harness has handed off to the alternate model for this turn, and the read tool confirms continued access to the QA scenario pack mission.",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts concise paraphrases of the kickoff task after a handoff", () => {
|
||||
expect(
|
||||
hasModelSwitchContinuityEvidence(
|
||||
|
||||
@@ -3,7 +3,11 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
|
||||
export function hasModelSwitchContinuityEvidence(text: string) {
|
||||
const lower = normalizeLowercaseStringOrEmpty(text);
|
||||
const mentionsHandoff =
|
||||
lower.includes("handoff") || lower.includes("model switch") || lower.includes("switched");
|
||||
lower.includes("handoff") ||
|
||||
lower.includes("handed off") ||
|
||||
lower.includes("handed-off") ||
|
||||
lower.includes("model switch") ||
|
||||
lower.includes("switched");
|
||||
const mentionsKickoffTask =
|
||||
lower.includes("qa_kickoff_task") ||
|
||||
lower.includes("qa/scenarios/index.md") ||
|
||||
|
||||
@@ -127,8 +127,8 @@ describe("qa scenario catalog", () => {
|
||||
const scenario = readQaScenarioById("gpt54-thinking-visibility-switch");
|
||||
const config = readQaScenarioExecutionConfig("gpt54-thinking-visibility-switch") as
|
||||
| {
|
||||
requiredLiveProvider?: string;
|
||||
requiredLiveModel?: string;
|
||||
requiredProvider?: string;
|
||||
requiredModel?: string;
|
||||
offDirective?: string;
|
||||
maxDirective?: string;
|
||||
reasoningDirective?: string;
|
||||
@@ -136,8 +136,8 @@ describe("qa scenario catalog", () => {
|
||||
| undefined;
|
||||
|
||||
expect(scenario.sourcePath).toBe("qa/scenarios/models/gpt54-thinking-visibility-switch.md");
|
||||
expect(config?.requiredLiveProvider).toBe("openai");
|
||||
expect(config?.requiredLiveModel).toBe("gpt-5.4");
|
||||
expect(config?.requiredProvider).toBe("openai");
|
||||
expect(config?.requiredModel).toBe("gpt-5.4");
|
||||
expect(config?.offDirective).toBe("/think off");
|
||||
expect(config?.maxDirective).toBe("/think max");
|
||||
expect(config?.reasoningDirective).toBe("/reasoning on");
|
||||
|
||||
@@ -250,4 +250,38 @@ describe("qa suite planning helpers", () => {
|
||||
}).map((scenario) => scenario.id),
|
||||
).toEqual(["generic", "claude-subscription"]);
|
||||
});
|
||||
|
||||
it("filters env-gated scenarios from an implicit live lane", () => {
|
||||
const previous = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
|
||||
delete process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
|
||||
try {
|
||||
const scenarios = [
|
||||
makeQaSuiteTestScenario("generic"),
|
||||
makeQaSuiteTestScenario("anthropic-api-key", {
|
||||
config: { requiredProvider: "anthropic", requiredModel: "claude-opus-4-6" },
|
||||
}),
|
||||
makeQaSuiteTestScenario("anthropic-setup-token", {
|
||||
config: {
|
||||
requiredProvider: "anthropic",
|
||||
requiredModel: "claude-opus-4-6",
|
||||
requiredEnv: "OPENCLAW_LIVE_SETUP_TOKEN_VALUE",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
selectQaSuiteScenarios({
|
||||
scenarios,
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "anthropic/claude-opus-4-6",
|
||||
}).map((scenario) => scenario.id),
|
||||
).toEqual(["generic", "anthropic-api-key"]);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
|
||||
} else {
|
||||
process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,10 +32,12 @@ function scenarioMatchesLiveLane(params: {
|
||||
primaryModel: string;
|
||||
providerMode: QaProviderMode;
|
||||
claudeCliAuthMode?: QaCliBackendAuthMode;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
if (getQaProvider(params.providerMode).kind !== "live") {
|
||||
return true;
|
||||
}
|
||||
const env = params.env ?? process.env;
|
||||
const selected = splitModelRef(params.primaryModel);
|
||||
const config = params.scenario.execution.config ?? {};
|
||||
const requiredProvider = normalizeQaConfigString(config.requiredProvider);
|
||||
@@ -50,6 +52,10 @@ function scenarioMatchesLiveLane(params: {
|
||||
if (requiredAuthMode && params.claudeCliAuthMode !== requiredAuthMode) {
|
||||
return false;
|
||||
}
|
||||
const requiredEnv = normalizeQaConfigString(config.requiredEnv);
|
||||
if (requiredEnv && !env[requiredEnv]?.trim()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.4.20",
|
||||
"version": "2026.4.20-beta.1",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
|
||||
@@ -49,8 +49,14 @@ execution:
|
||||
Evidence path: AGENT.md -> SOUL.md -> FOLLOWTHROUGH_INPUT.md -> repo-contract-summary.txt
|
||||
prompt: |-
|
||||
Repo contract followthrough check. Read AGENT.md, SOUL.md, and FOLLOWTHROUGH_INPUT.md first.
|
||||
Then follow the repo contract exactly, write ./repo-contract-summary.txt, and reply with
|
||||
three labeled lines: Read, Wrote, Status.
|
||||
Then use the write tool to create ./repo-contract-summary.txt with this exact body:
|
||||
|
||||
Repo contract
|
||||
Evidence path: AGENT.md -> SOUL.md -> FOLLOWTHROUGH_INPUT.md -> repo-contract-summary.txt
|
||||
Status: complete
|
||||
|
||||
Do not send the final reply until ./repo-contract-summary.txt exists. After writing it, reply with
|
||||
three labeled lines only: Read, Wrote, Status.
|
||||
Do not stop after planning and do not ask for permission before the first feasible action.
|
||||
expectedReplyAll:
|
||||
- "read:"
|
||||
|
||||
@@ -195,7 +195,7 @@ steps:
|
||||
- lambda:
|
||||
async: true
|
||||
expr: "await (async () => { const entries = (await fs.readdir(transcriptRoot).catch(() => [])).filter((entry) => entry.endsWith('.jsonl')).toSorted(); return entries.length > 0 ? path.join(transcriptRoot, entries.at(-1)) : undefined; })()"
|
||||
- 10000
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- call: fs.readFile
|
||||
saveAs: transcriptText
|
||||
args:
|
||||
@@ -207,13 +207,12 @@ steps:
|
||||
- assert:
|
||||
expr: "transcriptText.includes('memory_get')"
|
||||
message: active memory transcript missing memory_get
|
||||
- call: waitForCondition
|
||||
saveAs: activeSessionEntry
|
||||
args:
|
||||
- lambda:
|
||||
async: true
|
||||
expr: "await (async () => { const store = await readRawQaSessionStore(env); const entry = store[activeSessionKey]; if (!entry || !Array.isArray(entry.pluginDebugEntries)) return undefined; return entry.pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory' && Array.isArray(pluginEntry.lines) && pluginEntry.lines.some((line) => line.includes('Active Memory: status=ok'))) ? entry : undefined; })()"
|
||||
- 10000
|
||||
- set: activeSessionStore
|
||||
value:
|
||||
expr: "await readRawQaSessionStore(env)"
|
||||
- assert:
|
||||
expr: "Array.isArray(activeSessionStore[activeSessionKey]?.pluginDebugEntries) && activeSessionStore[activeSessionKey].pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory')"
|
||||
message: active session missing active-memory plugin debug entry
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
|
||||
@@ -33,6 +33,9 @@ execution:
|
||||
- can’t confirm
|
||||
- cannot confirm
|
||||
- i can confirm there is a hidden fact
|
||||
- hidden fact does exist
|
||||
- hidden fact **is** present
|
||||
- not to reveal
|
||||
- will not guess
|
||||
- won't guess
|
||||
- won’t guess
|
||||
|
||||
@@ -151,7 +151,7 @@ steps:
|
||||
expr: "normalizeLowercaseStringOrEmpty(outbound.text)"
|
||||
- set: staleLeak
|
||||
value:
|
||||
expr: "outbound.text.includes(staleFact) && !lower.includes('stale') && !lower.includes('older') && !lower.includes('previous')"
|
||||
expr: "outbound.text.includes(staleFact) && !lower.includes('stale') && !lower.includes('older') && !lower.includes('previous') && !lower.includes('newer')"
|
||||
- assert:
|
||||
expr: "!staleLeak"
|
||||
message:
|
||||
|
||||
@@ -28,6 +28,7 @@ execution:
|
||||
config:
|
||||
requiredProvider: anthropic
|
||||
requiredModel: claude-opus-4-6
|
||||
requiredEnv: OPENCLAW_LIVE_SETUP_TOKEN_VALUE
|
||||
profileId: "anthropic:qa-setup-token"
|
||||
chatPrompt: "Anthropic Opus setup-token smoke. Reply exactly: ANTHROPIC-OPUS-SETUP-TOKEN-OK"
|
||||
chatExpected: ANTHROPIC-OPUS-SETUP-TOKEN-OK
|
||||
|
||||
@@ -29,8 +29,8 @@ execution:
|
||||
kind: flow
|
||||
summary: Toggle reasoning display and GPT-5.4 thinking between off/none and max/high, then verify visible reasoning only on the max turn.
|
||||
config:
|
||||
requiredLiveProvider: openai
|
||||
requiredLiveModel: gpt-5.4
|
||||
requiredProvider: openai
|
||||
requiredModel: gpt-5.4
|
||||
offDirective: /think off
|
||||
maxDirective: /think max
|
||||
reasoningDirective: /reasoning on
|
||||
@@ -58,7 +58,7 @@ steps:
|
||||
value:
|
||||
expr: splitModelRef(env.primaryModel)
|
||||
- assert:
|
||||
expr: "env.providerMode !== 'live-frontier' || (selected?.provider === config.requiredLiveProvider && selected?.model === config.requiredLiveModel)"
|
||||
expr: "env.providerMode !== 'live-frontier' || (selected?.provider === config.requiredProvider && selected?.model === config.requiredModel)"
|
||||
message:
|
||||
expr: "`expected live GPT-5.4, got ${env.primaryModel}`"
|
||||
- call: state.addInboundMessage
|
||||
@@ -153,7 +153,7 @@ steps:
|
||||
saveAs: maxAck
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.conversationId && /Thinking level set to high/i.test(candidate.text)).at(-1)"
|
||||
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.conversationId && /Thinking level set to (?:high|xhigh)/i.test(candidate.text)).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 20000)
|
||||
detailsExpr: "`max ack=${maxAck.text}`"
|
||||
- name: verifies max thinking emits visible reasoning
|
||||
|
||||
@@ -230,6 +230,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
payload = json.loads(os.environ["PRL_VM_JSON"])
|
||||
requested = os.environ["REQUESTED_VM_NAME"].strip()
|
||||
@@ -237,7 +238,7 @@ requested_lower = requested.lower()
|
||||
explicit = os.environ["VM_NAME_EXPLICIT"] == "1"
|
||||
names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()]
|
||||
|
||||
def parse_ubuntu_version(name: str) -> tuple[int, ...] | None:
|
||||
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
|
||||
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
@@ -594,12 +595,12 @@ start_server() {
|
||||
}
|
||||
|
||||
install_latest_release() {
|
||||
local version_args=()
|
||||
if [[ -n "$INSTALL_VERSION" ]]; then
|
||||
version_args=(--version "$INSTALL_VERSION")
|
||||
fi
|
||||
guest_exec curl -fsSL "$INSTALL_URL" -o /tmp/openclaw-install.sh
|
||||
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh "${version_args[@]}" --no-onboard
|
||||
if [[ -n "$INSTALL_VERSION" ]]; then
|
||||
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh --version "$INSTALL_VERSION" --no-onboard
|
||||
else
|
||||
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh --no-onboard
|
||||
fi
|
||||
guest_exec openclaw --version
|
||||
}
|
||||
|
||||
|
||||
@@ -196,13 +196,14 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
payload = json.loads(os.environ["PRL_VM_JSON"])
|
||||
requested = os.environ["REQUESTED_VM_NAME"].strip()
|
||||
requested_lower = requested.lower()
|
||||
names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()]
|
||||
|
||||
def parse_ubuntu_version(name: str) -> tuple[int, ...] | None:
|
||||
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
|
||||
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
@@ -937,10 +937,11 @@ EOF
|
||||
}
|
||||
|
||||
ensure_mingit_zip() {
|
||||
local mingit_name mingit_url
|
||||
mapfile -t mingit_meta < <(resolve_mingit_download)
|
||||
mingit_name="${mingit_meta[0]}"
|
||||
mingit_url="${mingit_meta[1]}"
|
||||
local mingit_name mingit_url mingit_meta
|
||||
mingit_meta="$(resolve_mingit_download)"
|
||||
mingit_name="${mingit_meta%%$'\n'*}"
|
||||
mingit_url="${mingit_meta#*$'\n'}"
|
||||
[[ "$mingit_name" != "$mingit_url" ]] || die "failed to resolve MinGit download metadata"
|
||||
MINGIT_ZIP_NAME="$mingit_name"
|
||||
MINGIT_ZIP_PATH="$MAIN_TGZ_DIR/$mingit_name"
|
||||
if [[ ! -f "$MINGIT_ZIP_PATH" ]]; then
|
||||
|
||||
@@ -807,6 +807,20 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
export function isDirectPostinstallInvocation(params = {}) {
|
||||
const entryPath = params.entryPath ?? process.argv[1];
|
||||
if (!entryPath) {
|
||||
return false;
|
||||
}
|
||||
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
||||
const resolveRealPath = params.realpathSync ?? realpathSync;
|
||||
try {
|
||||
return resolveRealPath(entryPath) === resolveRealPath(modulePath);
|
||||
} catch {
|
||||
return pathToFileURL(entryPath).href === pathToFileURL(modulePath).href;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDirectPostinstallInvocation()) {
|
||||
runBundledPluginPostinstall();
|
||||
}
|
||||
|
||||
@@ -204,6 +204,24 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string {
|
||||
}).trim();
|
||||
}
|
||||
|
||||
export function createPackedBundledPluginPostinstallEnv(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
|
||||
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
|
||||
};
|
||||
}
|
||||
|
||||
function runPackedBundledPluginPostinstall(packageRoot: string): void {
|
||||
execFileSync(process.execPath, [join(packageRoot, "scripts/postinstall-bundled-plugins.mjs")], {
|
||||
cwd: packageRoot,
|
||||
stdio: "inherit",
|
||||
env: createPackedBundledPluginPostinstallEnv(),
|
||||
});
|
||||
}
|
||||
|
||||
function runPackedBundledChannelEntrySmoke(): void {
|
||||
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
|
||||
try {
|
||||
@@ -216,6 +234,7 @@ function runPackedBundledChannelEntrySmoke(): void {
|
||||
installPackedTarball(prefixDir, tarballPath, tmpRoot);
|
||||
|
||||
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
|
||||
runPackedBundledPluginPostinstall(packageRoot);
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
|
||||
@@ -331,7 +331,7 @@ else
|
||||
echo "==> Run installer smoke test (root): $FRESH_TAG_URL"
|
||||
docker run --rm -t \
|
||||
--platform "$SMOKE_PLATFORM" \
|
||||
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
|
||||
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
|
||||
-v "${LATEST_DIR}:/out" \
|
||||
-e OPENCLAW_INSTALL_URL="$INSTALL_URL" \
|
||||
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
|
||||
@@ -352,7 +352,7 @@ else
|
||||
echo "==> Run update smoke (${UPDATE_BASELINE_VERSION} -> ${UPDATE_EXPECT_VERSION})"
|
||||
docker run --rm -t \
|
||||
--platform "$SMOKE_PLATFORM" \
|
||||
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
|
||||
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
|
||||
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
|
||||
-e OPENCLAW_INSTALL_SMOKE_MODE=update \
|
||||
-e OPENCLAW_INSTALL_UPDATE_BASELINE="$UPDATE_BASELINE_VERSION" \
|
||||
@@ -370,7 +370,7 @@ else
|
||||
echo "==> Run direct npm global smoke (${UPDATE_BASELINE_VERSION} -> ${UPDATE_EXPECT_VERSION})"
|
||||
docker run --rm -t \
|
||||
--platform "$SMOKE_PLATFORM" \
|
||||
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
|
||||
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
|
||||
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
|
||||
-e OPENCLAW_INSTALL_SMOKE_MODE=npm-global \
|
||||
-e OPENCLAW_INSTALL_UPDATE_BASELINE="$UPDATE_BASELINE_VERSION" \
|
||||
|
||||
@@ -428,23 +428,34 @@ describe("runDaemonInstall", () => {
|
||||
},
|
||||
} as never);
|
||||
|
||||
await runDaemonInstall({ json: true, force: true });
|
||||
const previous = process.env.OPENAI_API_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
try {
|
||||
await runDaemonInstall({ json: true, force: true });
|
||||
|
||||
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
OPENAI_API_KEY: "service-openai-key",
|
||||
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
OPENAI_API_KEY: "service-openai-key",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const [firstArg] =
|
||||
(buildGatewayInstallPlanMock.mock.calls.at(0) as [Record<string, unknown>] | undefined) ?? [];
|
||||
const env = firstArg?.env as Record<string, string | undefined>;
|
||||
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
|
||||
expect(env.NODE_OPTIONS).toBeUndefined();
|
||||
expect(env.PATH).not.toContain("/tmp/doctor-bin");
|
||||
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
|
||||
);
|
||||
const [firstArg] =
|
||||
(buildGatewayInstallPlanMock.mock.calls.at(0) as [Record<string, unknown>] | undefined) ??
|
||||
[];
|
||||
const env = firstArg?.env as Record<string, string | undefined>;
|
||||
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
|
||||
expect(env.NODE_OPTIONS).toBeUndefined();
|
||||
expect(env.PATH).not.toContain("/tmp/doctor-bin");
|
||||
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,6 +311,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -622,6 +625,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -13069,6 +13075,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
pollingStallThresholdMs: {
|
||||
type: "integer",
|
||||
minimum: 30000,
|
||||
maximum: 600000,
|
||||
},
|
||||
retry: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -14102,6 +14113,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
pollingStallThresholdMs: {
|
||||
type: "integer",
|
||||
minimum: 30000,
|
||||
maximum: 600000,
|
||||
},
|
||||
retry: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -14482,6 +14498,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Telegram API Timeout (seconds)",
|
||||
help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
},
|
||||
pollingStallThresholdMs: {
|
||||
label: "Telegram Polling Stall Threshold (ms)",
|
||||
help: "Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000.",
|
||||
},
|
||||
silentErrorReplies: {
|
||||
label: "Telegram Silent Error Replies",
|
||||
help: "When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.",
|
||||
|
||||
@@ -27646,6 +27646,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
},
|
||||
version: "2026.4.20",
|
||||
version: "2026.4.20-beta.1",
|
||||
generatedAt: "2026-03-22T21:17:33.302Z",
|
||||
};
|
||||
|
||||
@@ -75,6 +75,19 @@ describe("gateway codex harness live helpers", () => {
|
||||
expect(isExpectedCodexModelsCommandText(text)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the sandboxed CLI failure active-model summary", () => {
|
||||
const text = [
|
||||
"I couldn’t inspect the CLI model list because sandboxed `codex --help` failed on a namespace restriction, and the escalated retry was rejected.",
|
||||
"",
|
||||
"What I can confirm from the current session is:",
|
||||
"- Active model: `codex/gpt-5.4`",
|
||||
].join("\n");
|
||||
|
||||
expect(
|
||||
EXPECTED_CODEX_MODELS_COMMAND_TEXT.some((expectedText) => text.includes(expectedText)),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unrelated codex command output", () => {
|
||||
expect(isExpectedCodexModelsCommandText("Codex is healthy.")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export const EXPECTED_CODEX_MODELS_COMMAND_TEXT = [
|
||||
"This harness is configured with a single Codex model: `codex/",
|
||||
"Primary model: `codex/",
|
||||
"Registered models: `codex/",
|
||||
"Active model: `codex/",
|
||||
"Current active model is `codex/",
|
||||
"Current OpenClaw session status reports the active model as:",
|
||||
] as const;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
createPackedBundledPluginPostinstallEnv,
|
||||
packageNameFromSpecifier,
|
||||
} from "../scripts/release-check.ts";
|
||||
import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts";
|
||||
@@ -463,3 +464,13 @@ describe("collectPackUnpackedSizeErrors", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPackedBundledPluginPostinstallEnv", () => {
|
||||
it("enables eager bundled dependency repair for packed channel entry smoke", () => {
|
||||
expect(createPackedBundledPluginPostinstallEnv({ PATH: "/usr/bin" })).toEqual({
|
||||
PATH: "/usr/bin",
|
||||
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
|
||||
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createBundledRuntimeDependencyInstallArgs,
|
||||
createBundledRuntimeDependencyInstallEnv,
|
||||
createNestedNpmInstallEnv,
|
||||
isDirectPostinstallInvocation,
|
||||
pruneInstalledPackageDist,
|
||||
discoverBundledPluginRuntimeDeps,
|
||||
pruneBundledPluginSourceNodeModules,
|
||||
@@ -82,6 +83,20 @@ describe("bundled plugin postinstall", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("recognizes direct invocation through symlinked temp prefixes", () => {
|
||||
const realpathSync = vi.fn((value: string) =>
|
||||
value.replace(/^\/var\/folders\//u, "/private/var/folders/"),
|
||||
);
|
||||
|
||||
expect(
|
||||
isDirectPostinstallInvocation({
|
||||
entryPath: "/var/folders/tmp/openclaw/scripts/postinstall-bundled-plugins.mjs",
|
||||
modulePath: "/private/var/folders/tmp/openclaw/scripts/postinstall-bundled-plugins.mjs",
|
||||
realpathSync,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
async function writeDiscordDaveyOptionalDependencyFixture(
|
||||
extensionsDir: string,
|
||||
packageRoot: string,
|
||||
|
||||
Reference in New Issue
Block a user