feat(memhub): expose offline migration bundle command
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
76e8419e1c
commit
bbbcbae692
2 files changed
+176
No files matched your search
@@ -75,6 +75,8 @@ def run(arguments: Sequence[str], *, output: TextIO | None = None) -> int:
|
||||
command, *options = arguments
|
||||
if command == "restore":
|
||||
return restore(options, stream)
|
||||
if command == "migration-bundle":
|
||||
return migration_bundle(options, stream)
|
||||
if command == "boundary" and not options:
|
||||
from one_memory.workbuddy_boundary import write_hook
|
||||
|
||||
@@ -95,6 +97,7 @@ def run(arguments: Sequence[str], *, output: TextIO | None = None) -> int:
|
||||
stream.write(
|
||||
"Usage: one-memory [serve [--maintenance|--reader]|status [--json]|doctor [--json]"
|
||||
"|restore --source <managed-backup> --yes [--json]"
|
||||
"|migration-bundle --bundle-id <id> --runtime-build <build> --yes [--json]"
|
||||
"|boundary [--peek|--ack TOKEN]|capture SOURCE]\n"
|
||||
)
|
||||
return 2
|
||||
@@ -198,6 +201,93 @@ def _write_restore(
|
||||
stream.write(f"{text}\n")
|
||||
|
||||
|
||||
def migration_bundle(options: Sequence[str], stream: TextIO) -> int:
|
||||
"""Create a complete management + SDK bundle while Runtime is stopped."""
|
||||
bundle_id: str | None = None
|
||||
runtime_build: str | None = None
|
||||
confirmed = False
|
||||
as_json = False
|
||||
index = 0
|
||||
while index < len(options):
|
||||
option = options[index]
|
||||
if option == "--bundle-id" and index + 1 < len(options):
|
||||
bundle_id = options[index + 1]
|
||||
index += 2
|
||||
continue
|
||||
if option == "--runtime-build" and index + 1 < len(options):
|
||||
runtime_build = options[index + 1]
|
||||
index += 2
|
||||
continue
|
||||
if option == "--yes":
|
||||
confirmed = True
|
||||
index += 1
|
||||
continue
|
||||
if option == "--json":
|
||||
as_json = True
|
||||
index += 1
|
||||
continue
|
||||
stream.write(f"one-memory: 参数无效:{option}\n")
|
||||
return 2
|
||||
if not bundle_id or not runtime_build:
|
||||
stream.write(
|
||||
"one-memory: migration-bundle 需要 "
|
||||
"--bundle-id <id> --runtime-build <build>\n"
|
||||
)
|
||||
return 2
|
||||
|
||||
settings = load_settings()
|
||||
status = inspect_runtime(settings.data_dir)
|
||||
if status.state != "stopped":
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"state": "blocked",
|
||||
"code": "sdk_migration_bundle_runtime_running",
|
||||
"runtime_state": status.state,
|
||||
}
|
||||
_write_restore(stream, as_json, document, "Runtime 仍在运行;不能创建迁移备份。")
|
||||
return 4
|
||||
if not confirmed:
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"state": "preview",
|
||||
"bundle_id": bundle_id,
|
||||
"requires_confirmation": True,
|
||||
}
|
||||
_write_restore(stream, as_json, document, "预览通过;加 --yes 后创建完整迁移备份。")
|
||||
return 4
|
||||
|
||||
from one_memory.sdk_migration_bundle import (
|
||||
MigrationBundleRejected,
|
||||
create_sdk_migration_bundle,
|
||||
)
|
||||
|
||||
try:
|
||||
result = create_sdk_migration_bundle(
|
||||
data_dir=settings.data_dir,
|
||||
bundle_id=bundle_id,
|
||||
runtime_build=runtime_build,
|
||||
runtime_stopped=True,
|
||||
)
|
||||
except MigrationBundleRejected as error:
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"state": "failed",
|
||||
"code": error.code,
|
||||
}
|
||||
_write_restore(stream, as_json, document, f"迁移备份失败:{error.code}")
|
||||
return 1
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"state": "created",
|
||||
"bundle_id": result.bundle_id,
|
||||
"bundle_path": str(result.bundle_path),
|
||||
"file_count": result.file_count,
|
||||
"total_bytes": result.total_bytes,
|
||||
}
|
||||
_write_restore(stream, as_json, document, f"完整迁移备份已创建:{result.bundle_path}")
|
||||
return 0
|
||||
|
||||
|
||||
def _status_text(command: str, status: RuntimeStatus) -> str:
|
||||
if status.state == "running":
|
||||
location = f"{status.host}:{status.port}"
|
||||
|
||||
@@ -66,3 +66,89 @@ def test_restore_success_returns_zero(tmp_path, monkeypatch) -> None:
|
||||
)
|
||||
assert code == 0
|
||||
assert '"state":"restored"' in output.getvalue().replace(" ", "")
|
||||
|
||||
|
||||
def test_migration_bundle_refuses_while_runtime_is_running(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(cli, "load_settings", lambda: Settings(_env_file=None, data_dir=tmp_path))
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"inspect_runtime",
|
||||
lambda _data_dir: RuntimeStatus("running", "runtime_verified"),
|
||||
)
|
||||
output = io.StringIO()
|
||||
|
||||
code = cli.run(
|
||||
[
|
||||
"migration-bundle",
|
||||
"--bundle-id",
|
||||
"migration-1",
|
||||
"--runtime-build",
|
||||
"build-13",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
output=output,
|
||||
)
|
||||
assert code == 4
|
||||
assert "sdk_migration_bundle_runtime_running" in output.getvalue()
|
||||
|
||||
|
||||
def test_migration_bundle_previews_before_writing(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(cli, "load_settings", lambda: Settings(_env_file=None, data_dir=tmp_path))
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"inspect_runtime",
|
||||
lambda _data_dir: RuntimeStatus("stopped", "runtime_receipt_missing"),
|
||||
)
|
||||
output = io.StringIO()
|
||||
|
||||
code = cli.run(
|
||||
[
|
||||
"migration-bundle",
|
||||
"--bundle-id",
|
||||
"migration-1",
|
||||
"--runtime-build",
|
||||
"build-13",
|
||||
"--json",
|
||||
],
|
||||
output=output,
|
||||
)
|
||||
assert code == 4
|
||||
assert '"state": "preview"' in output.getvalue() or '"state":"preview"' in output.getvalue()
|
||||
|
||||
|
||||
def test_migration_bundle_success_reports_bundle_identity(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(cli, "load_settings", lambda: Settings(_env_file=None, data_dir=tmp_path))
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"inspect_runtime",
|
||||
lambda _data_dir: RuntimeStatus("stopped", "runtime_receipt_missing"),
|
||||
)
|
||||
|
||||
class Result:
|
||||
bundle_id = "migration-1"
|
||||
bundle_path = tmp_path / "backups" / "migration-1.sdk-bundle"
|
||||
file_count = 4
|
||||
total_bytes = 123
|
||||
|
||||
monkeypatch.setattr(
|
||||
"one_memory.sdk_migration_bundle.create_sdk_migration_bundle",
|
||||
lambda **_kwargs: Result(),
|
||||
)
|
||||
output = io.StringIO()
|
||||
code = cli.run(
|
||||
[
|
||||
"migration-bundle",
|
||||
"--bundle-id",
|
||||
"migration-1",
|
||||
"--runtime-build",
|
||||
"build-13",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
output=output,
|
||||
)
|
||||
assert code == 0
|
||||
document = output.getvalue().replace(" ", "")
|
||||
assert '"state":"created"' in document
|
||||
assert '"bundle_id":"migration-1"' in document
|
||||
Reference in new issue
Block a user