mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ci): restore deterministic main checks (#120501)
* fix(plugin-sdk): stabilize session catalog baseline * test(doctor): isolate bind persistence inventory * fix(ci): prepare max-lines base during checkout * test(sessions): align freshness regression fixtures * style(tests): format context usage fixture * fix(ci): preserve token provenance and isolate fork credentials Finish the canonical token-provenance fixtures while preserving the latest maintainer-owned unavailable-usage ordering. Limit the diff-base GitHub token to manual non-release comparisons and guard that fork isolation invariant in the existing workflow regression test. Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com> * test(ui): identify actual managed image eviction * fix(ci): reuse the validated protocol comparison base Consume the immutable diff base already resolved and validated by preflight for every CI event. Remove the dead duplicate manual GitHub lookup and unreachable fallbacks, and guard the single authoritative path without changing fork credential isolation. * test(sessions): preserve stale total expectation * test(ui): arm reconnect deferral before disconnect --------- Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
This commit is contained in:
committed by
GitHub
parent
6ae7a8684a
commit
95ea82b1e5
+38
-88
@@ -252,7 +252,9 @@ jobs:
|
||||
- name: Resolve exact diff base
|
||||
id: diff_base
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
EVENT_BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha || '' }}
|
||||
GH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && github.token || '' }}
|
||||
PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
|
||||
RELEASE_GATE: ${{ inputs.release_gate }}
|
||||
run: |
|
||||
@@ -265,6 +267,18 @@ jobs:
|
||||
base_sha="$(node scripts/lib/merge-head-diff-base.mjs \
|
||||
--base "$base_sha" --head HEAD --prefer-first-parent)"
|
||||
fi
|
||||
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] && [ "$RELEASE_GATE" != "true" ]; then
|
||||
default_sha="$(git ls-remote origin "refs/heads/${DEFAULT_BRANCH}" | awk 'NR == 1 { print $1}')"
|
||||
if [[ ! "$default_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve the default branch head for the manual target." >&2
|
||||
exit 1
|
||||
fi
|
||||
base_sha="$(
|
||||
gh api --method GET \
|
||||
"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${head_sha}" \
|
||||
--jq '.merge_base_commit.sha'
|
||||
)"
|
||||
fi
|
||||
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] && [ "$RELEASE_GATE" = "true" ]; then
|
||||
merge_ref="refs/remotes/origin/release-gate-merge"
|
||||
timeout --signal=TERM --kill-after=10s 120s git fetch \
|
||||
@@ -279,6 +293,10 @@ jobs:
|
||||
base_sha="$(git rev-parse "${merge_ref}^1")"
|
||||
head_sha="$(git rev-parse "$merge_ref")"
|
||||
fi
|
||||
if [[ ! "$base_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve an exact diff base for ${GITHUB_EVENT_NAME}." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "sha=$base_sha" >> "$GITHUB_OUTPUT"
|
||||
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -1030,10 +1048,15 @@ jobs:
|
||||
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
git -C "$workdir" config gc.auto 0
|
||||
|
||||
fetch_refs=("+${CHECKOUT_SHA}:refs/remotes/origin/ci-target")
|
||||
if [ -n "${CHECKOUT_BASE_SHA:-}" ]; then
|
||||
fetch_refs+=("+${CHECKOUT_BASE_SHA}:refs/remotes/origin/ci-max-lines-base")
|
||||
fi
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
||||
"${fetch_refs[@]}" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
||||
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
||||
@@ -1533,6 +1556,8 @@ jobs:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
env:
|
||||
CHECKOUT_BASE_SHA: ${{ matrix.task == 'max-lines-ratchet' && needs.preflight.outputs.diff_base_revision || '' }}
|
||||
steps:
|
||||
- *linux_node_checkout_step
|
||||
- name: Prepare release-gate max-lines merge tree
|
||||
@@ -1577,7 +1602,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
git checkout --detach "$merge_sha"
|
||||
echo "RATCHET_RELEASE_BASE_SHA=${frozen_base_sha}" >> "$GITHUB_ENV"
|
||||
echo "RATCHET_BASE_REF=${frozen_base_sha}" >> "$GITHUB_ENV"
|
||||
echo "RATCHET_RELEASE_MERGE_TREE=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -1590,41 +1615,10 @@ jobs:
|
||||
sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }}
|
||||
|
||||
- name: Resolve manual protocol base
|
||||
id: protocol_manual_base
|
||||
if: matrix.task == 'bundled-protocol' && github.event_name == 'workflow_dispatch' && !inputs.release_gate
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ needs.preflight.outputs.checkout_revision }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
default_sha="$(git ls-remote origin "refs/heads/${DEFAULT_BRANCH}" | awk 'NR == 1 { print $1 }')"
|
||||
if [[ ! "$default_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve the default branch head for the manual protocol target." >&2
|
||||
exit 1
|
||||
fi
|
||||
merge_base_sha="$(
|
||||
gh api --method GET \
|
||||
"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${TARGET_SHA}" \
|
||||
--jq '.merge_base_commit.sha'
|
||||
)"
|
||||
if [[ ! "$merge_base_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve the manual protocol target merge base." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "sha=${merge_base_sha}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
||||
env:
|
||||
GH_TOKEN: ${{ matrix.task == 'max-lines-ratchet' && github.token || '' }}
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: 3
|
||||
PROTOCOL_MANUAL_BASE_SHA: ${{ steps.protocol_manual_base.outputs.sha }}
|
||||
PROTOCOL_SINCE_BASE_SHA: ${{ needs.preflight.outputs.diff_base_revision }}
|
||||
RATCHET_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
RATCHET_EVENT_BASE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }}
|
||||
RATCHET_MANUAL_TARGET_SHA: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && needs.preflight.outputs.checkout_revision || '' }}
|
||||
RATCHET_PR_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||
TASK: ${{ matrix.task }}
|
||||
shell: bash
|
||||
@@ -1636,41 +1630,11 @@ jobs:
|
||||
process.exit(Object.hasOwn(scripts, process.argv[1]) ? 0 : 1);
|
||||
' "$1"
|
||||
}
|
||||
resolve_manual_merge_base() {
|
||||
local default_branch="${RATCHET_DEFAULT_BRANCH:-main}"
|
||||
local default_sha
|
||||
local merge_base_sha
|
||||
default_sha="$(git ls-remote origin "refs/heads/${default_branch}" | awk 'NR == 1 { print $1 }')"
|
||||
if [[ ! "$default_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve the default branch head for the manual target." >&2
|
||||
return 1
|
||||
fi
|
||||
merge_base_sha="$(
|
||||
gh api --method GET \
|
||||
"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${RATCHET_MANUAL_TARGET_SHA}" \
|
||||
--jq '.merge_base_commit.sha'
|
||||
)"
|
||||
if [[ ! "$merge_base_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Could not resolve the manual target merge base." >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$merge_base_sha"
|
||||
}
|
||||
case "$TASK" in
|
||||
bundled-protocol)
|
||||
pnpm test:bundled
|
||||
if [[ -n "${PROTOCOL_SINCE_BASE_SHA:-}" ]]; then
|
||||
git fetch --no-tags --no-recurse-submodules --depth=1 origin \
|
||||
"+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/protocol-since-base"
|
||||
elif [[ -n "${PROTOCOL_MANUAL_BASE_SHA:-}" ]]; then
|
||||
PROTOCOL_SINCE_BASE_SHA="$PROTOCOL_MANUAL_BASE_SHA"
|
||||
export PROTOCOL_SINCE_BASE_SHA
|
||||
git fetch --no-tags --no-recurse-submodules --depth=1 origin \
|
||||
"+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/protocol-since-base"
|
||||
else
|
||||
git fetch --no-tags --no-recurse-submodules --depth=1 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
fi
|
||||
git fetch --no-tags --no-recurse-submodules --depth=1 origin \
|
||||
"+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/protocol-since-base"
|
||||
pnpm protocol:check
|
||||
;;
|
||||
contracts-plugins-ci-routing)
|
||||
@@ -1685,37 +1649,23 @@ jobs:
|
||||
echo "Current CI targets must provide check:max-lines-ratchet." >&2
|
||||
exit 1
|
||||
fi
|
||||
base_sha="${RATCHET_EVENT_BASE_SHA:-${RATCHET_RELEASE_BASE_SHA:-}}"
|
||||
if [[ "$base_sha" == "0000000000000000000000000000000000000000" ]]; then
|
||||
base_sha=""
|
||||
base_ref="${RATCHET_BASE_REF:-refs/remotes/origin/ci-max-lines-base}"
|
||||
if ! git cat-file -e "${base_ref}^{commit}" 2>/dev/null; then
|
||||
echo "Prepared max-lines base ${base_ref} is unavailable." >&2
|
||||
exit 1
|
||||
fi
|
||||
base_ref=""
|
||||
if [[ -n "${RATCHET_PR_HEAD_SHA:-}" ]]; then
|
||||
mapfile -t merge_parents < <(git cat-file -p HEAD | sed -n 's/^parent //p')
|
||||
if [[ "${#merge_parents[@]}" != "2" || "${merge_parents[1]:-}" != "$RATCHET_PR_HEAD_SHA" ]]; then
|
||||
echo "Pull request checkout is not the expected two-parent merge tree." >&2
|
||||
exit 1
|
||||
fi
|
||||
merge_base="${merge_parents[0]}"
|
||||
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin \
|
||||
"+${merge_base}:refs/remotes/origin/ci-max-lines-base"
|
||||
base_ref="refs/remotes/origin/ci-max-lines-base"
|
||||
elif [[ -n "$base_sha" ]]; then
|
||||
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin \
|
||||
"+${base_sha}:refs/remotes/origin/ci-max-lines-base"
|
||||
base_ref="refs/remotes/origin/ci-max-lines-base"
|
||||
elif [[ -n "${RATCHET_MANUAL_TARGET_SHA:-}" ]]; then
|
||||
merge_base_sha="$(resolve_manual_merge_base)"
|
||||
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin \
|
||||
"+${merge_base_sha}:refs/remotes/origin/ci-max-lines-base"
|
||||
base_ref="refs/remotes/origin/ci-max-lines-base"
|
||||
else
|
||||
default_branch="${RATCHET_DEFAULT_BRANCH:-main}"
|
||||
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${default_branch}:refs/remotes/origin/ci-max-lines-base"
|
||||
base_ref="refs/remotes/origin/ci-max-lines-base"
|
||||
prepared_base="$(git rev-parse "$base_ref")"
|
||||
if [[ "${merge_parents[0]}" != "$prepared_base" ]]; then
|
||||
echo "Pull request merge base does not match the prepared preflight base." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
unset GH_TOKEN
|
||||
pnpm check:max-lines-ratchet --base "$base_ref"
|
||||
if [[ "${RATCHET_RELEASE_MERGE_TREE:-}" == "true" ]]; then
|
||||
node scripts/run-oxlint.mjs src ui/src packages extensions
|
||||
|
||||
@@ -122,7 +122,7 @@ aa8a411ad37c1d1143b67376bf2d20255b9eedff61d80815f42e4f8ed7bd8e58 module/secret-
|
||||
57dcb1462d4c4f9a98d934c4ca975b163d704758af9821a64001ff3ac05637c3 module/secret-input-runtime
|
||||
dc0ee07d392a85c218939000b28c0138f139215da00f5592b34a68ba8e29a25d module/secret-ref-runtime
|
||||
f97549081955e412d8eb64070c64bb5921bb1324744d824db05767d3adcce403 module/security-runtime
|
||||
8f57eaf07620598f0a62cf603d450d9a9da2d4ea7a867f13aadc1212019e5eb9 module/session-catalog
|
||||
b18d65a8abbed79bcb77e34679b7aeb03f2e9101271f48298b47eb839c33a379 module/session-catalog
|
||||
50f5e344f98c27570b7a30e32a906b612e2383d21f102e88cd93e1d5425a6de9 module/session-discussion
|
||||
f112bdabc51ba8659b37d0a6f6a32a2b1d471e5b49b56e108bf750ec55a7ea71 module/session-store-runtime
|
||||
36affbe151431a6141664b6838e20f2d121ff210d57a3c1b4b41a8818b5c81d8 module/setup
|
||||
|
||||
@@ -158,60 +158,6 @@ describe("AgentSession loop correctness", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pre-prompt compact from usage before a zero unavailable marker", async () => {
|
||||
const model = { ...testModel, contextWindow: 1_000 };
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
appendHistory(
|
||||
sessionManager,
|
||||
createAssistant(model, [{ type: "text", text: "old cumulative turn" }], "stop", 950),
|
||||
);
|
||||
sessionManager.appendMessage({ role: "user", content: "CLI prompt", timestamp: Date.now() });
|
||||
sessionManager.appendMessage({
|
||||
...createAssistant(model, [{ type: "text", text: "usage unavailable" }]),
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
contextUsage: { state: "unavailable" },
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
});
|
||||
const settingsManager = SettingsManager.inMemory({
|
||||
compaction: { enabled: true, reserveTokens: 100, keepRecentTokens: 20 },
|
||||
retry: { enabled: false },
|
||||
});
|
||||
const compactionEvents: AgentSessionEvent[] = [];
|
||||
streamMocks.streamSimple.mockImplementation((activeModel: Model) =>
|
||||
createAssistantResultStream(
|
||||
createAssistant(activeModel, [{ type: "text", text: "complete answer" }], "stop", 20),
|
||||
),
|
||||
);
|
||||
const { session } = await createTestSession({
|
||||
model,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
resourceLoader: createResourceLoader(createCompactionHandlers()),
|
||||
});
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "compaction_start" || event.type === "compaction_end") {
|
||||
compactionEvents.push(event);
|
||||
}
|
||||
});
|
||||
|
||||
expect(session.messages.at(-1)).toMatchObject({
|
||||
role: "assistant",
|
||||
usage: { contextUsage: { state: "unavailable" } },
|
||||
});
|
||||
expect(session.getContextUsage()?.tokens).toBeLessThan(900);
|
||||
await session.prompt("continue after CLI turn");
|
||||
|
||||
expect(streamMocks.streamSimple).toHaveBeenCalledOnce();
|
||||
expect(compactionEvents).toEqual([]);
|
||||
expect(session.getLastAssistantText()).toBe("complete answer");
|
||||
});
|
||||
|
||||
it("skips threshold maintenance when embedded auto-compaction is disabled", async () => {
|
||||
const settingsManager = SettingsManager.inMemory({
|
||||
compaction: { enabled: false, reserveTokens: 0, keepRecentTokens: 1 },
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AgentMessage } from "../runtime/index.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
|
||||
describe("AgentSession context usage", () => {
|
||||
it("does not compact an unavailable marker when current context is below threshold", async () => {
|
||||
const runAutoCompaction = vi.fn();
|
||||
const checkCompaction = (
|
||||
AgentSession.prototype as unknown as {
|
||||
checkCompaction: (message: AgentMessage, skipAbortedCheck: boolean) => Promise<boolean>;
|
||||
}
|
||||
).checkCompaction;
|
||||
|
||||
const compacted = await checkCompaction.call(
|
||||
{
|
||||
settingsManager: {
|
||||
getCompactionSettings: () => ({
|
||||
enabled: true,
|
||||
reserveTokens: 100,
|
||||
keepRecentTokens: 20,
|
||||
}),
|
||||
},
|
||||
model: { provider: "test", id: "model", contextWindow: 1_000 },
|
||||
sessionManager: { getBranch: () => [] },
|
||||
getContextUsage: () => ({ tokens: 100 }),
|
||||
runAutoCompaction,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "test",
|
||||
model: "model",
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
usage: { contextUsage: { state: "unavailable" } },
|
||||
} as AgentMessage,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(compacted).toBe(false);
|
||||
expect(runAutoCompaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves an earlier exact snapshot when unavailable usage precedes any compaction", () => {
|
||||
const messages = [
|
||||
{
|
||||
|
||||
@@ -1,50 +1,68 @@
|
||||
// Verifies Doctor persists legacy gateway bind repairs through the real config writer.
|
||||
import fs from "node:fs/promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome, writeOpenClawConfig } from "../config/test-helpers.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { readConfigFileSnapshot } from "../config/config.js";
|
||||
import { withEnvOverride, withTempHome, writeOpenClawConfig } from "../config/test-helpers.js";
|
||||
import { runInitialConfigWriteHealth } from "../flows/doctor-health-contribution-runners.config.js";
|
||||
import type { DoctorHealthFlowContext } from "../flows/doctor-health-contribution-types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
|
||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||
import { migrateLegacyConfig } from "./doctor/shared/legacy-config-migrate.js";
|
||||
|
||||
describe("Doctor gateway bind persistence", () => {
|
||||
afterEach(() => {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["localhost", "loopback"],
|
||||
["0.0.0.0", "lan"],
|
||||
] as const)("persists gateway bind %s as %s", async (legacyBind, canonicalBind) => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = await writeOpenClawConfig(home, {
|
||||
gateway: { mode: "local", bind: legacyBind },
|
||||
await withEnvOverride({ OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }, async () => {
|
||||
// This core writer regression needs the authoritative empty bundled-plugin inventory.
|
||||
const configPath = await writeOpenClawConfig(home, {
|
||||
gateway: { mode: "local", bind: legacyBind },
|
||||
});
|
||||
const runtime: RuntimeEnv = {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
const options: DoctorOptions = { nonInteractive: true, repair: true };
|
||||
const prompter = createDoctorPrompter({ runtime, options });
|
||||
const configResult = await loadAndMaybeMigrateDoctorConfig({
|
||||
options,
|
||||
confirm: (params) => prompter.confirm(params),
|
||||
runtime,
|
||||
prompter,
|
||||
});
|
||||
const ctx: DoctorHealthFlowContext = {
|
||||
runtime,
|
||||
options,
|
||||
prompter,
|
||||
configResult,
|
||||
cfg: configResult.cfg,
|
||||
cfgForPersistence: structuredClone(configResult.cfg),
|
||||
sourceConfigValid: configResult.sourceConfigValid ?? true,
|
||||
configPath,
|
||||
stateDirExistedAtStart: true,
|
||||
...(configResult.runWithPluginMetadataSnapshot
|
||||
? { runWithPluginMetadataSnapshot: configResult.runWithPluginMetadataSnapshot }
|
||||
: {}),
|
||||
...(configResult.invalidatePluginMetadataSnapshot
|
||||
? { invalidatePluginMetadataSnapshot: configResult.invalidatePluginMetadataSnapshot }
|
||||
: {}),
|
||||
};
|
||||
|
||||
await runInitialConfigWriteHealth(ctx);
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
expect(snapshot.valid).toBe(true);
|
||||
expect(snapshot.config.gateway?.bind).toBe(canonicalBind);
|
||||
expect(await fs.readFile(configPath, "utf-8")).not.toContain(`"bind": "${legacyBind}"`);
|
||||
});
|
||||
const runtime: RuntimeEnv = {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
const options: DoctorOptions = { nonInteractive: true, repair: true };
|
||||
const prompter = createDoctorPrompter({ runtime, options });
|
||||
const migration = migrateLegacyConfig({ gateway: { mode: "local", bind: legacyBind } });
|
||||
expect(migration.config).not.toBeNull();
|
||||
const cfg = migration.config!;
|
||||
const configResult = { cfg, shouldWriteConfig: true };
|
||||
const ctx: DoctorHealthFlowContext = {
|
||||
runtime,
|
||||
options,
|
||||
prompter,
|
||||
configResult,
|
||||
cfg,
|
||||
cfgForPersistence: structuredClone(cfg),
|
||||
sourceConfigValid: true,
|
||||
configPath,
|
||||
stateDirExistedAtStart: true,
|
||||
};
|
||||
|
||||
await runInitialConfigWriteHealth(ctx);
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
||||
expect(persisted.gateway?.bind).toBe(canonicalBind);
|
||||
expect(persisted.gateway?.bind).not.toBe(legacyBind);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ const TEST_ENTRYPOINTS = [
|
||||
"provider-selection-runtime",
|
||||
"provider-web-search-config-contract",
|
||||
"realtime-voice",
|
||||
"session-catalog",
|
||||
"sqlite-runtime-testing",
|
||||
] as const;
|
||||
|
||||
@@ -194,6 +195,10 @@ describe("Plugin SDK API baseline", () => {
|
||||
expect(findDeclaration("ProviderSelection")).toContain(
|
||||
"export type ProviderSelection<TProvider> =",
|
||||
);
|
||||
expect(findDeclaration("SessionCatalogEntrySummary")).toContain(
|
||||
"export interface SessionCatalogEntrySummary",
|
||||
);
|
||||
expect(findDeclaration("SessionCatalogEntrySummary")).toContain("entry: SessionEntry;");
|
||||
expect(rendered.json).not.toContain('"line":');
|
||||
expect(rendered.jsonl).not.toContain('"sourceLine":');
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
SessionsCatalogReadResult,
|
||||
} from "../../packages/gateway-protocol/src/schema/sessions-catalog.js";
|
||||
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
|
||||
@@ -57,9 +58,10 @@ export type SessionCatalogCreateTarget = {
|
||||
agentRuntime: string;
|
||||
};
|
||||
|
||||
export type SessionCatalogEntrySummary = ReturnType<
|
||||
PluginRuntime["agent"]["session"]["listSessionEntries"]
|
||||
>[number];
|
||||
export interface SessionCatalogEntrySummary {
|
||||
sessionKey: string;
|
||||
entry: SessionEntry;
|
||||
}
|
||||
|
||||
/** Shared, logically frozen store state for one request; copy locally before mutating. */
|
||||
export type SessionCatalogEntrySnapshot = {
|
||||
|
||||
@@ -4307,11 +4307,18 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(parsedWorkflow.jobs.preflight.outputs.diff_base_revision).toBe(
|
||||
"${{ steps.diff_base.outputs.sha }}",
|
||||
);
|
||||
expect(
|
||||
parsedWorkflow.jobs.preflight.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Resolve exact diff base",
|
||||
).run,
|
||||
).toContain("--prefer-first-parent");
|
||||
const diffBaseStep = parsedWorkflow.jobs.preflight.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Resolve exact diff base",
|
||||
);
|
||||
expect(diffBaseStep.run).toContain("--prefer-first-parent");
|
||||
expect(diffBaseStep.env.DEFAULT_BRANCH).toBe("${{ github.event.repository.default_branch }}");
|
||||
expect(diffBaseStep.env.GH_TOKEN).toBe(
|
||||
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && github.token || '' }}",
|
||||
);
|
||||
expect(diffBaseStep.run).toContain(
|
||||
'"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${head_sha}"',
|
||||
);
|
||||
expect(diffBaseStep.run).toContain("Could not resolve an exact diff base");
|
||||
const securityDiffBase = parsedWorkflow.jobs["security-fast"].steps.find(
|
||||
(step: WorkflowStep) => step.name === "Resolve security diff base",
|
||||
).run;
|
||||
@@ -4422,48 +4429,44 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
|
||||
it("runs the suppression-baseline max-lines ratchet against the exact tested tree", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const checksFastSteps = workflow.jobs["checks-fast-core"].steps;
|
||||
const checksFastJob = workflow.jobs["checks-fast-core"];
|
||||
const checksFastSteps = checksFastJob.steps;
|
||||
const checkout = checksFastSteps.find((step: WorkflowStep) => step.name === "Checkout");
|
||||
const checksFastRun = checksFastSteps.find(
|
||||
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
|
||||
);
|
||||
const releaseGateMerge = checksFastSteps.find(
|
||||
(step: WorkflowStep) => step.name === "Prepare release-gate max-lines merge tree",
|
||||
);
|
||||
const protocolManualBase = checksFastSteps.find(
|
||||
(step: WorkflowStep) => step.name === "Resolve manual protocol base",
|
||||
);
|
||||
expect(
|
||||
checksFastSteps.some((step: WorkflowStep) => step.name === "Resolve manual protocol base"),
|
||||
).toBe(false);
|
||||
|
||||
expect(workflow.jobs["checks-fast-core"].permissions).toEqual({
|
||||
contents: "read",
|
||||
"pull-requests": "read",
|
||||
});
|
||||
expect(checksFastJob.env.CHECKOUT_BASE_SHA).toBe(
|
||||
"${{ matrix.task == 'max-lines-ratchet' && needs.preflight.outputs.diff_base_revision || '' }}",
|
||||
);
|
||||
expect(checkout.run).toContain(
|
||||
'fetch_refs+=("+${CHECKOUT_BASE_SHA}:refs/remotes/origin/ci-max-lines-base")',
|
||||
);
|
||||
expect(checkout.run).toContain('"${fetch_refs[@]}" || return 1');
|
||||
expect(releaseGateMerge.if).toBe(
|
||||
"matrix.task == 'max-lines-ratchet' && github.event_name == 'workflow_dispatch' && inputs.release_gate",
|
||||
);
|
||||
expect(checksFastRun.run).toContain("max-lines-ratchet)");
|
||||
expect(checksFastRun.run).toContain('has_package_script "check:max-lines-ratchet"');
|
||||
expect(checksFastRun.env.RATCHET_EVENT_BASE_SHA).toBe(
|
||||
"${{ github.event_name == 'push' && github.event.before || '' }}",
|
||||
);
|
||||
expect(checksFastRun.env.RATCHET_PR_HEAD_SHA).toBe(
|
||||
"${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}",
|
||||
);
|
||||
expect(checksFastRun.env.RATCHET_MANUAL_TARGET_SHA).toBe(
|
||||
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && needs.preflight.outputs.checkout_revision || '' }}",
|
||||
);
|
||||
expect(checksFastRun.env.GH_TOKEN).toBe(
|
||||
"${{ matrix.task == 'max-lines-ratchet' && github.token || '' }}",
|
||||
);
|
||||
expect(protocolManualBase.if).toBe(
|
||||
"matrix.task == 'bundled-protocol' && github.event_name == 'workflow_dispatch' && !inputs.release_gate",
|
||||
);
|
||||
expect(protocolManualBase.env.GH_TOKEN).toBe("${{ github.token }}");
|
||||
expect(protocolManualBase.run).toContain(
|
||||
'"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${TARGET_SHA}"',
|
||||
);
|
||||
expect(protocolManualBase.run).toContain('echo "sha=${merge_base_sha}" >> "$GITHUB_OUTPUT"');
|
||||
expect(checksFastRun.env.PROTOCOL_MANUAL_BASE_SHA).toBe(
|
||||
"${{ steps.protocol_manual_base.outputs.sha }}",
|
||||
expect(checksFastRun.env).not.toHaveProperty("RATCHET_EVENT_BASE_SHA");
|
||||
expect(checksFastRun.env).not.toHaveProperty("RATCHET_MANUAL_TARGET_SHA");
|
||||
expect(checksFastRun.env).not.toHaveProperty("GH_TOKEN");
|
||||
expect(checksFastRun.env).not.toHaveProperty("PROTOCOL_MANUAL_BASE_SHA");
|
||||
expect(checksFastRun.env.PROTOCOL_SINCE_BASE_SHA).toBe(
|
||||
"${{ needs.preflight.outputs.diff_base_revision }}",
|
||||
);
|
||||
expect(releaseGateMerge.run).toContain(
|
||||
'gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PULL_REQUEST_NUMBER}"',
|
||||
@@ -4492,39 +4495,29 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(releaseGateMerge.run).not.toContain(".base.sha");
|
||||
expect(releaseGateMerge.run).toContain('git checkout --detach "$merge_sha"');
|
||||
expect(releaseGateMerge.run).toContain(
|
||||
'echo "RATCHET_RELEASE_BASE_SHA=${frozen_base_sha}" >> "$GITHUB_ENV"',
|
||||
'echo "RATCHET_BASE_REF=${frozen_base_sha}" >> "$GITHUB_ENV"',
|
||||
);
|
||||
expect(releaseGateMerge.run).toContain(
|
||||
'echo "RATCHET_RELEASE_MERGE_TREE=true" >> "$GITHUB_ENV"',
|
||||
);
|
||||
expect(
|
||||
checksFastRun.run.match(
|
||||
/timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin \\/gu,
|
||||
),
|
||||
).toHaveLength(4);
|
||||
expect(checksFastRun.run).toContain('git ls-remote origin "refs/heads/${default_branch}"');
|
||||
expect(checksFastRun.run).toContain(
|
||||
'"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${RATCHET_MANUAL_TARGET_SHA}"',
|
||||
);
|
||||
expect(checksFastRun.run).toContain('PROTOCOL_SINCE_BASE_SHA="$PROTOCOL_MANUAL_BASE_SHA"');
|
||||
expect(checksFastRun.run).not.toContain("PROTOCOL_MANUAL_BASE_SHA");
|
||||
expect(checksFastRun.run).toContain(
|
||||
'"+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/protocol-since-base"',
|
||||
);
|
||||
expect(checksFastRun.run).toContain("--jq '.merge_base_commit.sha'");
|
||||
expect(checksFastRun.run).toContain(
|
||||
'"+${merge_base_sha}:refs/remotes/origin/ci-max-lines-base"',
|
||||
);
|
||||
expect(checksFastRun.run).toContain(
|
||||
'if [[ "$base_sha" == "0000000000000000000000000000000000000000" ]]',
|
||||
'base_ref="${RATCHET_BASE_REF:-refs/remotes/origin/ci-max-lines-base}"',
|
||||
);
|
||||
expect(checksFastRun.run).toContain('git cat-file -e "${base_ref}^{commit}"');
|
||||
expect(checksFastRun.run).toContain(
|
||||
"mapfile -t merge_parents < <(git cat-file -p HEAD | sed -n 's/^parent //p')",
|
||||
);
|
||||
expect(checksFastRun.run).toContain('"${#merge_parents[@]}" != "2"');
|
||||
expect(checksFastRun.run).toContain('"${merge_parents[1]:-}" != "$RATCHET_PR_HEAD_SHA"');
|
||||
expect(checksFastRun.run).toContain('"+${merge_base}:refs/remotes/origin/ci-max-lines-base"');
|
||||
expect(checksFastRun.run).toContain('prepared_base="$(git rev-parse "$base_ref")"');
|
||||
expect(checksFastRun.run).toContain('"${merge_parents[0]}" != "$prepared_base"');
|
||||
expect(checksFastRun.run).not.toContain("ci-max-lines-target^");
|
||||
expect(checksFastRun.run).toContain("unset GH_TOKEN");
|
||||
expect(checksFastRun.run).not.toContain("resolve_manual_merge_base");
|
||||
expect(checksFastRun.run).not.toContain("+${merge_base}:refs/remotes/origin/ci-max-lines-base");
|
||||
expect(checksFastRun.run).toContain('pnpm check:max-lines-ratchet --base "$base_ref"');
|
||||
expect(checksFastRun.run).toContain(
|
||||
'if [[ "${RATCHET_RELEASE_MERGE_TREE:-}" == "true" ]]; then',
|
||||
|
||||
@@ -645,13 +645,14 @@ suite.define(() => {
|
||||
const overflowProof = await readBlobProof();
|
||||
// Concurrent image fetches can resolve in any order. Find the real LRU
|
||||
// rather than assuming that creation order matches transcript order.
|
||||
expect(overflowProof.revoked).toHaveLength(1);
|
||||
const evictedBlobUrl = expectDefined(
|
||||
overflowProof.created.find((blobUrl) => blobUrl !== retainedRecentBlobUrl),
|
||||
overflowProof.revoked.find((blobUrl) => blobUrl !== retainedRecentBlobUrl),
|
||||
"evicted managed image Blob URL",
|
||||
);
|
||||
expect(overflowProof.created).toContain(evictedBlobUrl);
|
||||
const evictedImageIndex = initialBlobUrls.indexOf(evictedBlobUrl);
|
||||
expect(evictedImageIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(overflowProof.revoked).toContain(evictedBlobUrl);
|
||||
expect(overflowProof.revoked).not.toContain(retainedRecentBlobUrl);
|
||||
|
||||
const evictedPath = new URL(
|
||||
|
||||
@@ -434,8 +434,8 @@ suite.define(() => {
|
||||
await expect.poll(() => sidebarRows.count()).toBe(3);
|
||||
const initialListCount = (await gateway.getRequests("sessions.list")).length;
|
||||
|
||||
await gateway.closeLatest(1006, "disconnect proof");
|
||||
await gateway.deferNext("sessions.list");
|
||||
await gateway.closeLatest(1006, "disconnect proof");
|
||||
await sidebarRow.waitFor({ state: "visible" });
|
||||
await captureUiProof(page, "sidebar-sessions-during-reconnect.png");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user