diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49f75503bab8..aeb30f567514 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 00b4a65aff35..64f9a0c1353b 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/src/agents/sessions/agent-session-loop-correctness.test.ts b/src/agents/sessions/agent-session-loop-correctness.test.ts index 7b47de81052d..946cc5874e73 100644 --- a/src/agents/sessions/agent-session-loop-correctness.test.ts +++ b/src/agents/sessions/agent-session-loop-correctness.test.ts @@ -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 }, diff --git a/src/agents/sessions/agent-session.context-usage.test.ts b/src/agents/sessions/agent-session.context-usage.test.ts index 19391d35a013..ee76b81e7602 100644 --- a/src/agents/sessions/agent-session.context-usage.test.ts +++ b/src/agents/sessions/agent-session.context-usage.test.ts @@ -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; + } + ).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 = [ { diff --git a/src/commands/doctor-config-flow.gateway-bind-persistence.test.ts b/src/commands/doctor-config-flow.gateway-bind-persistence.test.ts index 394c0ec3a59c..e0509e59a425 100644 --- a/src/commands/doctor-config-flow.gateway-bind-persistence.test.ts +++ b/src/commands/doctor-config-flow.gateway-bind-persistence.test.ts @@ -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); }); }); }); diff --git a/src/plugin-sdk/api-baseline.test.ts b/src/plugin-sdk/api-baseline.test.ts index 353f3560a681..4cd329f65f45 100644 --- a/src/plugin-sdk/api-baseline.test.ts +++ b/src/plugin-sdk/api-baseline.test.ts @@ -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 =", ); + 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":'); }); diff --git a/src/plugins/session-catalog.ts b/src/plugins/session-catalog.ts index daf656a07253..07a02668d60c 100644 --- a/src/plugins/session-catalog.ts +++ b/src/plugins/session-catalog.ts @@ -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 = { diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index fc256f986b28..76221487f08a 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -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', diff --git a/ui/src/e2e/chat-flow.media-files.e2e.test.ts b/ui/src/e2e/chat-flow.media-files.e2e.test.ts index 20ed3d505e7d..3de62bb2d75d 100644 --- a/ui/src/e2e/chat-flow.media-files.e2e.test.ts +++ b/ui/src/e2e/chat-flow.media-files.e2e.test.ts @@ -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( diff --git a/ui/src/e2e/session-management.sidebar.e2e.test.ts b/ui/src/e2e/session-management.sidebar.e2e.test.ts index 537dea2a0d01..cb74a34adca7 100644 --- a/ui/src/e2e/session-management.sidebar.e2e.test.ts +++ b/ui/src/e2e/session-management.sidebar.e2e.test.ts @@ -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");