Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ci-paths.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ tests = [
"tests/**", "integrations/**", "examples/**", "scripts/generate_js_operations.py",
"scripts/sync_dsh_plugin_tree.py", "scripts/generate_integration_manifest_docs.py",
"scripts/run_atomic_memory_clean_migration.py", "scripts/scan_e2e_evidence.sh",
"scripts/check_atomic_memory_migration_shards.py",
"scripts/ci_role_smoke.py",
# These evaluator suites still live under the root tests/ directory.
"evaluation/memory/locomo/**", "evaluation/memory/locomo_plus/**", "evaluation/memory/write_gate/**",
Expand All @@ -32,6 +33,7 @@ database = [
"tests/e2e/*native_code*", "tests/e2e/test_recall_sufficiency_gate.py",
"tests/e2e/test_context_text_assembly.py", "tests/e2e/test_scope_search_migration.py",
"scripts/run_atomic_memory_clean_migration.py",
"scripts/check_atomic_memory_migration_shards.py",
]
dify = ["integrations/dify/**", "tests/*dify*"]
hermes = ["integrations/hermes/**", "tests/*hermes*"]
Expand Down
62 changes: 53 additions & 9 deletions .github/workflows/e2e-harness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,20 +297,24 @@ jobs:
if: always()
run: docker rm --force recall-effort-oceanbase || true

atomic-memory-clean-oceanbase:
name: Atomic Memory clean migration (OceanBase)
atomic-memory-clean-oceanbase-shards:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 保留旧 job 名后,两份 observability 文档仍把 test_background_metrics.py 的执行归给该 job

本 PR 把执行体移到 atomic-memory-clean-oceanbase-shards(矩阵 0/1/2),而沿用旧 id 的 atomic-memory-clean-oceanbase 现在只做「下载三个 artifact + 跑 check_atomic_memory_migration_shards.py」。但 docs/en/development/observability.md:136 与 docs/zh/development/observability.md:138 仍写「E2E harness workflow 的 atomic-memory-clean-oceanbase job 通过 clean migration acceptance runner 执行 tests/e2e/test_background_metrics.py,不允许测试 skip」。

影响:按 job 名排查 background listener 失败时会落到聚合 job,那里没有 RUN <node> 行、也没有 background-metrics/background.log;这些证据现在位于 atomic-memory-clean-oceanbase-<sha>-shard-0 artifact 的 background-metrics/ 下(该组只有 1 个 node,因此只有 shard 0 有 background.log)。文档描述的「执行者」与本 head 的实现不一致。

证据:git grep -n "atomic-memory-clean-oceanbase" -- docs/ 只命中上述两行;本 head 的聚合 job 121522025190 日志(Check out → Download shard reports → Verify complete execution coverage)只有 5 行 ... cases passed exactly once without skips. 汇总,没有任何 RUN/pytest 输出。仓库内不存在其它引用旧 job 名的脚本或 workflow(.github/ci-paths.toml、master.yml 的 seekdb 路径都用 --shard 默认值,未受影响)。

建议:同一 PR 内把两份文档改成「...-shards(三个 runner)执行、atomic-memory-clean-oceanbase 负责聚合校验」,并说明 background.log 在 shard 0 的 artifact 内。聚合 job 本身已 success(scope-search 2 / background-metrics 1 / clean-migration 47 / startup-snapshot 3 / long-query 8),所以这条不影响门禁,只影响按文档排障。

name: Atomic Memory clean migration (OceanBase shard ${{ matrix.shard }}/3)
needs: changes
if: >-
${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.suite != 'recall-effort') &&
(needs.changes.result != 'success' || fromJSON(needs.changes.outputs.suites || '{}').database) }}
runs-on: ubuntu-latest
timeout-minutes: 145
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
env:
OCEANBASE_IMAGE: ghcr.io/oceanbase/oceanbase-ce:4.3.5.6-106000012026040916
POWERCONTEXT_TEST_MIGRATION_OCEANBASE_URL: >-
mysql+aoceanbase://root%40test:powercontext-e2e@127.0.0.1:2881/powercontext_test?charset=utf8mb4
PYTHONUNBUFFERED: "1"
MIGRATION_REPORTS: ${{ github.workspace }}/.powercontext/atomic-memory-clean-oceanbase
MIGRATION_REPORTS: ${{ github.workspace }}/.powercontext/atomic-memory-clean-oceanbase-shard-${{ matrix.shard }}
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -381,28 +385,28 @@ jobs:
timeout-minutes: 10
run: |
uv run --locked python scripts/run_atomic_memory_clean_migration.py \
oceanbase --output "$MIGRATION_REPORTS/scope-search" --tests \
oceanbase --shard "${{ matrix.shard }}/3" --output "$MIGRATION_REPORTS/scope-search" --tests \
tests/e2e/test_scope_search_migration.py

- name: Test background listener lifecycle without skips
timeout-minutes: 5
run: |
uv run --locked --extra server python scripts/run_atomic_memory_clean_migration.py \
oceanbase --output "$MIGRATION_REPORTS/background-metrics" --tests \
oceanbase --shard "${{ matrix.shard }}/3" --output "$MIGRATION_REPORTS/background-metrics" --tests \
tests/e2e/test_background_metrics.py

- name: Run clean migration evidence, grants and vectors without skips
timeout-minutes: 60
run: |
uv run --locked python scripts/run_atomic_memory_clean_migration.py \
oceanbase --output "$MIGRATION_REPORTS/clean-migration"
oceanbase --shard "${{ matrix.shard }}/3" --output "$MIGRATION_REPORTS/clean-migration"

- name: Test concurrent managed startup without skips
# Collection and three isolated cases retain their 180s process deadlines.
timeout-minutes: 15
run: |
uv run --locked --extra server python scripts/run_atomic_memory_clean_migration.py \
oceanbase --output "$MIGRATION_REPORTS/startup-snapshot" --tests \
oceanbase --shard "${{ matrix.shard }}/3" --output "$MIGRATION_REPORTS/startup-snapshot" --tests \
tests/e2e/test_managed_startup_read_consistency.py
test "$(wc -l < "$MIGRATION_REPORTS/startup-snapshot/nodes.txt")" -eq 3

Expand All @@ -411,7 +415,7 @@ jobs:
timeout-minutes: 30
run: |
uv run --locked python scripts/run_atomic_memory_clean_migration.py \
oceanbase --output "$MIGRATION_REPORTS/long-query" --tests \
oceanbase --shard "${{ matrix.shard }}/3" --output "$MIGRATION_REPORTS/long-query" --tests \
tests/e2e/test_atomic_artifact_search_http.py \
tests/e2e/test_atomic_memory_long_fts_query.py

Expand Down Expand Up @@ -443,7 +447,7 @@ jobs:
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: atomic-memory-clean-oceanbase-${{ github.sha }}
name: atomic-memory-clean-oceanbase-${{ github.sha }}-shard-${{ matrix.shard }}
path: ${{ env.MIGRATION_REPORTS }}/
include-hidden-files: true
if-no-files-found: error
Expand All @@ -453,6 +457,46 @@ jobs:
if: always()
run: docker rm --force atomic-memory-clean-oceanbase || true

atomic-memory-clean-oceanbase:
name: Atomic Memory clean migration (OceanBase)
needs: [changes, atomic-memory-clean-oceanbase-shards]
if: >-
${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.suite != 'recall-effort') &&
(needs.changes.result != 'success' || fromJSON(needs.changes.outputs.suites || '{}').database) }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every shard to succeed
env:
SHARD_RESULT: ${{ needs.atomic-memory-clean-oceanbase-shards.result }}
run: |
if [ "$SHARD_RESULT" != success ]; then
echo "OceanBase migration shards did not all succeed: $SHARD_RESULT" >&2
exit 1
fi

- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"

- name: Download shard reports into separate directories
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
with:
pattern: atomic-memory-clean-oceanbase-${{ github.sha }}-shard-*
path: ${{ runner.temp }}/atomic-memory-clean-oceanbase-shards
merge-multiple: false

- name: Verify complete execution coverage
run: >-
python scripts/check_atomic_memory_migration_shards.py
--reports "${{ runner.temp }}/atomic-memory-clean-oceanbase-shards" --shards 3

acceptance:
needs: validate
runs-on: ubuntu-latest
Expand Down
37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,43 @@ git push origin name-of-your-bugfix-or-feature

# Pull Request Guidelines

## OceanBase migration acceptance

The E2E harness runs Scope search migrations, background listener lifecycle, clean
Atomic Memory migrations, managed startup consistency, and long-query acceptance
on three isolated OceanBase runners. Every group collects and validates its full
test collection before selecting zero-based shard `INDEX/COUNT`. Cases within each shard still run
serially in separate pytest processes; skips and failures fail the shard.

With the migration OceanBase URL configured as in `.github/workflows/e2e-harness.yml`,
reproduce the clean migration part of one shard with:

```bash
uv run --locked python scripts/run_atomic_memory_clean_migration.py \
oceanbase --shard 0/3 --output .powercontext/migration-shard-0/clean-migration
```

Omit `--shard` to run the complete collection. Use `--tests` to select the other
groups' files from the workflow, or `--collect-only` to inspect the full collection
without running cases. Every invocation writes `summary.json`; only successful
execution, including a legitimately empty shard, marks the report complete.

The aggregate check retains the name `Atomic Memory clean migration (OceanBase)`.
It requires all three runners to succeed and verifies that their reports cover
every collected node exactly once in each of the five groups. To verify downloaded
artifacts locally, keep each shard artifact in a separate subdirectory and run:

```bash
uv run --locked python scripts/check_atomic_memory_migration_shards.py \
--reports .powercontext/downloaded-migration-shards --shards 3
```

Collection-only or incomplete reports cannot satisfy the aggregate check. Include
links and measured before/after job and workflow times in migration CI PRs, and
report runner queue time separately from execution time.

## Submission checklist

Before you submit a pull request, check that it meets these guidelines:

1. Add behavior tests for new externally observable behavior. Add a regression test when a defect is likely to
Expand Down
20 changes: 13 additions & 7 deletions docs/en/development/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,16 @@ These checks exercise real spawn, OTLP/HTTP, a local model fixture, Pydantic AI
surface. They prove local reception, not storage/query acceptance by an external backend.

The background CLI listener lifecycle requires a disposable OceanBase instance and
`POWERCONTEXT_TEST_MIGRATION_OCEANBASE_URL`. The `E2E harness` workflow's `atomic-memory-clean-oceanbase` job runs
`tests/e2e/test_background_metrics.py` through the clean migration acceptance runner, which rejects skipped tests.
The test initializes its fresh database before starting the real background CLI, then checks listener startup,
authorized metrics, absence of business routes and clean SIGTERM shutdown. The listener startup budget excludes
first-run schema and index creation. CI retains `background-metrics/background.log` with the migration reports;
startup failures include the child return code and a bounded log tail. Backend acceptance requires this step to pass.
Without the backend setting, default test runs skip that lifecycle test.
`POWERCONTEXT_TEST_MIGRATION_OCEANBASE_URL`. The `E2E harness` workflow's
`atomic-memory-clean-oceanbase-shards` matrix runs
`tests/e2e/test_background_metrics.py` through the clean migration acceptance runner
on three runners, and rejects skipped tests. The legacy-named
`atomic-memory-clean-oceanbase` job only downloads the three shard artifacts and
checks their complete, exactly-once coverage. The test initializes its fresh database
before starting the real background CLI, then checks listener startup, authorized
metrics, absence of business routes and clean SIGTERM shutdown. The listener startup
budget excludes first-run schema and index creation. CI retains
`background-metrics/background.log` in the shard artifact; this group has one node,
so the log is in shard `0`. Startup failures include the child return code and a
bounded log tail. Backend acceptance requires this step to pass. Without the backend
setting, default test runs skip that lifecycle test.
16 changes: 10 additions & 6 deletions docs/zh/development/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,13 @@ uv run --locked --extra tracing-otlp pytest tests/e2e/test_worker_otlp.py \
这些证据证明本地 reception,不能代替外部 backend 的存储与查询验收。

Background CLI listener 生命周期测试需要可丢弃的 OceanBase 实例及
`POWERCONTEXT_TEST_MIGRATION_OCEANBASE_URL`。`E2E harness` workflow 的 `atomic-memory-clean-oceanbase` job
通过 clean migration acceptance runner 执行 `tests/e2e/test_background_metrics.py`,不允许测试 skip。
该测试先初始化新建数据库,再启动真实 background CLI,检查 listener 启动、metrics 授权、business routes
不可用,以及 SIGTERM 后正常退出。Listener 启动预算不包含首次建表与索引创建。
CI 在 migration reports 中保留 `background-metrics/background.log`;启动失败时输出 child return code
和有界 log tail。此步骤通过后,才能认定 backend 验收通过。未设置 backend 变量时,默认测试会跳过此用例。
`POWERCONTEXT_TEST_MIGRATION_OCEANBASE_URL`。`E2E harness` workflow 的
`atomic-memory-clean-oceanbase-shards` 矩阵在三个 runner 上通过 clean migration
acceptance runner 执行 `tests/e2e/test_background_metrics.py`,不允许测试 skip。
沿用旧名称的 `atomic-memory-clean-oceanbase` job 只负责下载三个分片的 artifact,
并校验完整且恰好一次的覆盖。该测试先初始化新建数据库,再启动真实 background CLI,
检查 listener 启动、metrics 授权、business routes 不可用,以及 SIGTERM 后正常退出。
Listener 启动预算不包含首次建表与索引创建。CI 在分片 artifact 的
`background-metrics/background.log` 中保留日志;该组只有一个 node,因此日志位于
shard `0`。启动失败时输出 child return code 和有界 log tail。此步骤通过后,才能认定
backend 验收通过。未设置 backend 变量时,默认测试会跳过此用例。
107 changes: 107 additions & 0 deletions scripts/check_atomic_memory_migration_shards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Verify complete, exactly-once OceanBase acceptance across downloaded shard artifacts."""

from __future__ import annotations

import argparse
import json
import sys
from collections import Counter
from pathlib import Path

GROUPS = ("scope-search", "background-metrics", "clean-migration", "startup-snapshot", "long-query")


def verify_group(paths: list[Path], shards: int) -> int:
"""Require matching collections and successful, disjoint execution of every node."""
collection: list[str] | None = None
passed: list[str] = []
indices: set[int] = set()
for path in paths:
report = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(report, dict):
message = f"{path}: expected a summary object"
raise TypeError(message)
index = report.get("shard_index")
if (
type(index) is not int
or not 0 <= index < shards
or index in indices
or type(report.get("shard_count")) is not int
or report["shard_count"] != shards
or report.get("backend") != "oceanbase"
or report.get("complete") is not True
):
message = f"{path}: incomplete summary or invalid shard identity"
raise ValueError(message)
nodes = report.get("collected_nodes")
actual = report.get("passed_nodes")
if (
not isinstance(nodes, list)
or not nodes
or not all(isinstance(node, str) and node for node in nodes)
or len(set(nodes)) != len(nodes)
or not isinstance(actual, list)
or not all(isinstance(node, str) for node in actual)
):
message = f"{path}: invalid collection or passed nodes"
raise ValueError(message)
if collection is None:
collection = nodes
if nodes != collection:
message = f"{path}: full collections differ between shards"
raise ValueError(message)
if actual != nodes[index::shards]:
message = f"{path}: passed nodes differ from the assigned shard"
raise ValueError(message)
indices.add(index)
passed.extend(actual)
if indices != set(range(shards)) or Counter(passed) != Counter(collection or []):
message = "shard coverage must contain every collected node exactly once"
raise ValueError(message)
return len(passed)


def verify_reports(directory: Path, shards: int) -> None:
"""Verify every acceptance group in exactly one artifact per expected shard."""
artifacts = sorted(path for path in directory.iterdir() if path.is_dir())
if len(artifacts) != shards:
message = f"expected {shards} shard artifacts, found {len(artifacts)}"
raise ValueError(message)
for group in GROUPS:
count = verify_group([artifact / group / "summary.json" for artifact in artifacts], shards)
print(f"{group}: all {count} OceanBase cases passed exactly once without skips.", flush=True)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--reports", type=Path, required=True, help="Directory containing one artifact directory per shard"
)
parser.add_argument("--shards", type=int, required=True)
args = parser.parse_args()
if args.shards < 1:
parser.error("--shards must be positive")
try:
verify_reports(args.reports, args.shards)
except (OSError, TypeError, ValueError) as error:
print(f"Shard verification failed: {error}", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading