Files
Hy-Memory/memhub/apps/cli/src/cli.test.ts
T
6d1d29e16d refactor(memhub): move memory authority to SDK (#132)
* docs(memhub): define SDK-owned memory architecture

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(memhub): freeze legacy memory authority debt

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): define SDK data-plane migration contract

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): add SDK cutover migration ledger

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): create verified SDK migration bundles

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): expose offline migration bundle command

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): keep durable write scheduling in Core

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): delegate SDK backup to public API

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): restore complete SDK migration bundles

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): route SDK snapshots through bridge

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): persist SDK cutover state

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): add SDK-native memory projection

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): recover writes from SDK outcomes

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(memhub): pin SDK authority dev preview

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): classify legacy and SDK memory inventories

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): trust SDK physical delete receipts

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): execute classified memory migrations

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(memhub): pin exact migration SDK dev preview

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(memhub): preserve source SDK version in migration bundle

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): use one schema-driven legacy migration

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): migrate Evidence into SDK raw

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): run resumable offline SDK backfill

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): expose offline SDK backfill command

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): expose SDK-native memory APIs

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): add SDK-native memory client

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): migrate recall scopes by memory id

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): remove fabricated per-memory harness ACL

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): remove legacy per-memory recall controls

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): use SDK source filters for future harness policy

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): remove per-memory scope mutation API

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): remove dead recall scope component

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(memhub): pin raw-search SDK dev preview

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): search SDK raw through existing API

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(memhub): add honest SDK-native Context v2

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): switch library to sdk authority

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): cut host context over to sdk v2

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): run migration during app startup

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): add sdk raw scheduling receipts

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert "refactor(memhub): add sdk raw scheduling receipts"

This reverts commit d07ae91c93.

* refactor(memhub): persist sdk ids after queued add

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): stop governance mirroring after cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): fence startup migration and resume effects

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): release transient evidence after sdk success

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): gate startup authority cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): run sdk migration in app loading

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): block cutover until product paths switch

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): resume sdk staging cleanup on startup

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): add sdk-native ask v2

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): migrate matters to sdk references

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): hydrate matters from sdk references

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): switch matter reads to sdk references

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): queue sdk matter notes

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): discover matters from sdk authority

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): retire legacy digest and boundary after cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): complete sdk raw and matter cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): stop legacy maintenance after cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): retire remaining legacy read surfaces

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): document sdk matter and raw cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(runtime): allow receipt write under parallel load

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): publish sdk authority architecture

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(memhub): repair acceptance gate fixtures

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): separate fresh schema from legacy cleanup

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(memhub): finish SDK authority cutover

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(memhub): close CodeQL exposure paths

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(memhub): hash workspace identities without path expansion

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(memhub): add app migration acceptance guide

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 15:24:20 +08:00

2915 lines
88 KiB
TypeScript

import { mkdtemp, readdir, readFile, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
claimRuntimeServiceForApp,
readRuntimeServiceOwner,
resolveRuntimeCliConfig,
} from "@one-memory/runtime-control";
import { describe, expect, it, vi } from "vitest";
import {
defaultInstallRoot,
managedRuntimeServiceId,
packagedAppCliPath,
run as productRun,
runWithManagedInstallForTest as run,
runtimeIsReady,
} from "./cli.js";
import { PRODUCT } from "./version.js";
const ROOT_VERSION = JSON.parse(
await readFile(new URL("../../../package.json", import.meta.url), "utf8"),
).version as string;
const runtimeMocks = vi.hoisted(() => ({
applyLocalInstall: vi.fn(
async (plan: {
schema_version: 1;
source: string;
install_root: string;
version_directory: string;
current_link: string;
version: string;
platform: "linux";
arch: string;
files: number;
}) => ({
...plan,
previous_current_target: null,
created_version: true,
}),
),
installUserService: vi.fn(async (_layout, options) => ({
schema_version: 1,
platform: process.platform === "darwin" ? "darwin" : "linux",
scope: "user",
service_id: "one-memory.service",
status: "ready",
blocked_reason: null,
service_file: {
path: "/tmp/one-memory.service",
mode: 0o600,
content: "unit",
},
commands: { install: [], start: [], stop: [], uninstall: [] },
options,
})),
uninstallUserService: vi.fn(async (..._args: unknown[]) => "removed"),
restartUserService: vi.fn(async () => undefined),
rollbackLocalInstall: vi.fn(async () => undefined),
planCliLink: vi.fn(async ({ home, installRoot }) => ({
schema_version: 1,
platform: "linux",
home,
install_root: installRoot,
bin_directory: path.join(home, ".local", "bin"),
status: "create",
blocked_reason: null,
link_path: path.join(home, ".local", "bin", "one-memory"),
target_path: path.join(installRoot, "current", "bin", "one-memory"),
})),
installCliLink: vi.fn(async (plan) => ({ ...plan, created: true })),
rollbackCliLink: vi.fn(async () => undefined),
planLocalInstall: vi.fn(async ({ source, installRoot }) => ({
schema_version: 1,
source,
install_root: installRoot,
version_directory: path.join(installRoot, "versions", "1.0.0-linux-x64"),
current_link: path.join(installRoot, "current"),
version: "1.0.0",
platform: "linux",
arch: "x64",
files: 9,
})),
planLocalUninstall: vi.fn(
async ({ installRoot, home, dataDir, cliLinkPath, stopsService }) => ({
schema_version: 1,
install_root: installRoot,
home,
data_dir: dataDir,
cli_link_path: cliLinkPath,
stops_service: stopsService,
status: "ready",
blocked_reason: null,
}),
),
applyLocalUninstall: vi.fn(async () => ({ removed: true })),
inspectRuntimeStatus: vi.fn(async () => ({
state: "stopped",
code: "runtime_not_running",
})),
}));
const aliasMocks = vi.hoisted(() => ({
planManagedCliLink: vi.fn(async (plan, name: string) => ({
...plan,
status: "create",
blocked_reason: null,
link_path: `${plan.bin_directory}/${name}`,
})),
installManagedCliLink: vi.fn(async (plan) => ({ ...plan, created: true })),
rollbackManagedCliLink: vi.fn(async () => undefined),
removeManagedCliLink: vi.fn(async () => "removed" as const),
}));
vi.mock("@one-memory/runtime-control", async (importOriginal) => {
const original =
await importOriginal<typeof import("@one-memory/runtime-control")>();
return {
...original,
planLocalInstall: runtimeMocks.planLocalInstall,
planLocalUninstall: runtimeMocks.planLocalUninstall,
applyLocalUninstall: runtimeMocks.applyLocalUninstall,
inspectRuntimeStatus: runtimeMocks.inspectRuntimeStatus,
applyLocalInstall: runtimeMocks.applyLocalInstall,
installUserService: runtimeMocks.installUserService,
uninstallUserService: runtimeMocks.uninstallUserService,
restartUserService: runtimeMocks.restartUserService,
rollbackLocalInstall: runtimeMocks.rollbackLocalInstall,
planCliLink: runtimeMocks.planCliLink,
installCliLink: runtimeMocks.installCliLink,
rollbackCliLink: runtimeMocks.rollbackCliLink,
};
});
vi.mock("./managed-cli-link.js", () => aliasMocks);
const shellPathMocks = vi.hoisted(() => ({
planShellPath: vi.fn(async (options: { binDirectory: string }) => ({
schema_version: 1 as const,
status: "present" as const,
blocked_reason: null,
bin_directory: options.binDirectory,
profile_path: "/tmp/home/.zprofile",
block: null,
})),
applyShellPath: vi.fn(async (plan) => ({ ...plan, created: false })),
removeShellPath: vi.fn(async () => "not-present" as const),
}));
const lifecycleMocks = vi.hoisted(() => ({
prepareServiceInstall: vi.fn(async (layout: unknown, options: unknown) => ({
layout,
options,
mode: "direct" as const,
previouslyEnabled: false,
})),
applyPreparedServiceInstall: vi.fn(
async (prepared: { layout: unknown; options: unknown }) =>
runtimeMocks.installUserService(
prepared.layout as never,
prepared.options as never,
),
),
uninstallServicePreservingData: vi.fn(
async (layout: unknown, options: unknown) =>
runtimeMocks.uninstallUserService(layout as never, options as never),
),
rollbackPreparedServiceInstall: vi.fn(
async (_prepared: unknown, rollback: () => Promise<void>) => rollback(),
),
}));
vi.mock("./shell-path.js", () => shellPathMocks);
// The macOS service lifecycle probes the real global LaunchAgent label, which
// unit tests must neither depend on nor fight over (a peer probe may hold it).
// Keep the real data-dir wiring by delegating to runtime-control's mocked
// install/uninstall, and skip only the launchd preflight. The real preflight is
// covered in service-lifecycle.test.ts.
vi.mock("./service-lifecycle.js", async () => lifecycleMocks);
describe("public One Memory CLI", () => {
it("derives a scoped service id from a custom Runtime root", () => {
const config = {
...resolveRuntimeCliConfig(
{
HOME: "/tmp/custom-home",
ONE_MEMORY_DATA_DIR: "/tmp/custom-data",
ONE_MEMORY_HOST_HOME: "/tmp/custom-home",
ONE_MEMORY_RUNTIME_ENTRY:
"/tmp/custom-install/current/runtime/runtime.cjs",
},
"/tmp/custom-install/current/bin/one-memory.cjs",
),
};
expect(
managedRuntimeServiceId(
config,
{ ONE_MEMORY_INSTALL_ROOT: "/tmp/custom-install" },
501,
),
).toMatch(/^com\.tencent\.onememory\.runtime\.[a-f0-9]{16}$/);
});
describe("local-experience install surface", () => {
it("recognizes the explicit packaged App fixture on non-macOS test hosts", () => {
const target =
"/Applications/MemHub.app/Contents/Resources/cli/one-memory.cjs";
expect(
packagedAppCliPath(undefined, {
ONE_MEMORY_TEST_ISOLATED: "1",
ONE_MEMORY_APP_BUNDLE_CLI: target,
}),
).toBe(target);
});
it("installs only stable CLI links when invoked from the packaged Mac app", async () => {
vi.clearAllMocks();
const home = "/tmp/app-cli-home";
const bin = "/tmp/app-cli-bin";
const target =
"/Applications/MemHub.app/Contents/Resources/cli/one-memory.cjs";
const output = captureStdout();
let code: number;
try {
code = await productRun(
["install", "--home", home, "--bin-dir", bin, "--yes"],
{
HOME: home,
ONE_MEMORY_TEST_ISOLATED: "1",
ONE_MEMORY_APP_BUNDLE_CLI: target,
},
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(
aliasMocks.planManagedCliLink.mock.calls.map((call) => call[1]),
).toEqual(["memhub", "memhub-cli", "one-memory"]);
expect(aliasMocks.installManagedCliLink).toHaveBeenCalledTimes(3);
expect(runtimeMocks.planLocalInstall).not.toHaveBeenCalled();
expect(runtimeMocks.installUserService).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
apply: true,
cli: [
{ link_path: `${bin}/memhub`, target_path: target },
{ link_path: `${bin}/memhub-cli`, target_path: target },
{ link_path: `${bin}/one-memory`, target_path: target },
],
});
});
it("does not register a second service from the packaged Mac app", async () => {
vi.clearAllMocks();
const output = captureStdout();
let code: number;
try {
code = await productRun(
[
"service-install",
"--install-root",
"/Applications/MemHub.app",
"--yes",
"--json",
],
{
HOME: "/tmp/app-cli-home",
ONE_MEMORY_TEST_ISOLATED: "1",
ONE_MEMORY_APP_BUNDLE_CLI:
"/Applications/MemHub.app/Contents/Resources/cli/one-memory.cjs",
},
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(lifecycleMocks.prepareServiceInstall).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
command: "service-install",
state: "managed_by_app",
apply: false,
});
});
it("removes only stable CLI links when uninstall runs from the packaged Mac app", async () => {
vi.clearAllMocks();
const bin = "/tmp/app-cli-bin";
const output = captureStdout();
let code: number;
try {
code = await productRun(
[
"uninstall",
"--home",
"/tmp/app-cli-home",
"--bin-dir",
bin,
"--yes",
],
{
HOME: "/tmp/app-cli-home",
ONE_MEMORY_TEST_ISOLATED: "1",
ONE_MEMORY_APP_BUNDLE_CLI:
"/Applications/MemHub.app/Contents/Resources/cli/one-memory.cjs",
},
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(aliasMocks.removeManagedCliLink).toHaveBeenCalledTimes(3);
expect(
(
aliasMocks.removeManagedCliLink.mock.calls as unknown as Array<
[{ link_path: string }]
>
).map((call) => path.basename(call[0].link_path)),
).toEqual(["memhub", "memhub-cli", "one-memory"]);
expect(runtimeMocks.applyLocalUninstall).not.toHaveBeenCalled();
expect(
lifecycleMocks.uninstallServicePreservingData,
).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
command: "uninstall",
state: "cli_links_removed",
data_preserved: true,
});
});
it.each([
[
"service-install",
[
"--platform",
"linux",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
],
],
[
"uninstall",
[
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
],
],
[
"install",
[
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/custom",
],
],
] as const)(
"exposes %s through the production CLI",
async (command, args) => {
vi.clearAllMocks();
const streams = captureStreams();
let code: number;
try {
code = await productRun([command, ...args, "--json"], {});
} finally {
streams.restore();
}
expect(code!).not.toBe(2);
expect(`${streams.stdout()}${streams.stderr()}`).not.toContain(
"local_experience_operation_unavailable",
);
},
);
it("runs the complete standalone install at the platform default root", async () => {
vi.clearAllMocks();
const home = "/tmp/local-experience-home";
const installRoot = defaultInstallRoot(process.platform, home);
const output = captureStdout();
let code: number;
try {
code = await productRun(
["install", "--source", "/tmp/dist", "--home", home, "--yes"],
{},
{ waitForRuntimeReady: async () => true },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(runtimeMocks.planLocalInstall).toHaveBeenCalledWith(
expect.objectContaining({ installRoot }),
);
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.installCliLink).toHaveBeenCalledOnce();
expect(lifecycleMocks.applyPreparedServiceInstall).toHaveBeenCalledOnce();
expect(JSON.parse(output.value())).toMatchObject({ apply: true });
});
it("keeps normal upgrade and its compensation path enabled", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
const output = captureStdout();
let code: number;
try {
code = await productRun(
[
"upgrade",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.restartUserService).toHaveBeenCalledOnce();
expect(runtimeMocks.rollbackLocalInstall).not.toHaveBeenCalled();
});
});
describe("explicit service data identity", () => {
const serviceArgs = [
"--platform",
"linux",
"--home",
"/tmp/selected-home",
"--install-root",
"/tmp/install",
];
it.each(["service-plan", "service-install", "service-uninstall"])(
"%s keeps env data and lets an explicit flag override it",
async (command) => {
for (const flag of [false, true]) {
const output = captureStdout();
try {
expect(
await run(
[
command,
...serviceArgs,
...(flag ? ["--data-dir", "/tmp/flag data"] : []),
],
{ ONE_MEMORY_DATA_DIR: "/tmp/env data" },
),
).toBe(0);
expect(JSON.parse(output.value()).service_file.content).toContain(
`ONE_MEMORY_DATA_DIR=${flag ? "/tmp/flag data" : "/tmp/env data"}`,
);
} finally {
output.restore();
}
}
},
);
it.each(["service-install", "service-uninstall"])(
"%s applies the same explicitly selected data directory",
async (command) => {
vi.clearAllMocks();
const output = captureStdout();
try {
expect(
await run(
[
command,
...serviceArgs,
"--data-dir",
"/tmp/flag data",
"--yes",
],
{ ONE_MEMORY_DATA_DIR: "/tmp/env data" },
command === "service-install"
? { waitForRuntimeReady: async () => true }
: {},
),
).toBe(0);
const mutation =
command === "service-install"
? runtimeMocks.installUserService
: runtimeMocks.uninstallUserService;
expect(mutation).toHaveBeenCalledWith(
expect.objectContaining({
dataDir: "/tmp/flag data",
installRoot: "/tmp/install/current",
}),
expect.any(Object),
);
} finally {
output.restore();
}
},
);
it.each(["install", "upgrade"])(
"%s preview retains env/flag/default identity",
async (command) => {
for (const data of [undefined, "/tmp/env data", "/tmp/flag data"]) {
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/selected-home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
const output = captureStdout();
try {
expect(
await run(
[
command,
"--source",
"/tmp/dist",
"--home",
"/tmp/selected-home",
"--install-root",
"/tmp/install",
...(data === "/tmp/flag data" ? ["--data-dir", data] : []),
],
data === undefined
? {}
: { ONE_MEMORY_DATA_DIR: "/tmp/env data" },
),
).toBe(0);
expect(
JSON.parse(output.value()).service.service_file.content,
).toContain(
`ONE_MEMORY_DATA_DIR=${data ?? "/tmp/selected-home/.one-memory"}`,
);
} finally {
output.restore();
}
}
},
);
it.each(["install", "upgrade"])(
"%s passes allowDowngrade only when explicitly requested",
async (command) => {
for (const allowed of [false, true]) {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/selected-home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
const output = captureStdout();
try {
expect(
await run(
[
command,
"--source",
"/tmp/dist",
"--home",
"/tmp/selected-home",
"--install-root",
"/tmp/install",
...(allowed ? ["--allow-downgrade"] : []),
],
{},
),
).toBe(0);
expect(runtimeMocks.planLocalInstall).toHaveBeenCalledWith(
expect.objectContaining({
source: "/tmp/dist",
installRoot: "/tmp/install",
allowDowngrade: allowed,
}),
);
} finally {
output.restore();
}
}
},
);
it("install apply carries env data without changing independent authorization", async () => {
vi.clearAllMocks();
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
],
{ ONE_MEMORY_DATA_DIR: "/tmp/env data" },
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
expect(runtimeMocks.installUserService).toHaveBeenCalledWith(
expect.objectContaining({ dataDir: "/tmp/env data" }),
expect.any(Object),
);
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
} finally {
output.restore();
}
});
// C8 74a7c9e: an install whose HOME is an isolated temp dir must not add
// any entry to that HOME (no `.one-memory` receipt, no `.zprofile`). The
// receipt is only for a real account-home install, where a later zero-env
// short command has no other way to learn the data dir.
it("isolated install writes nothing into the caller's HOME", async () => {
vi.clearAllMocks();
const home = await mkdtemp(path.join(tmpdir(), "memhub-receipt-home-"));
try {
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
home,
"--install-root",
path.join(home, "install"),
"--data-dir",
path.join(home, "custom data"),
"--yes",
],
{ HOME: home },
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
} finally {
output.restore();
}
const entries = await readdir(home);
expect(entries).not.toContain(".one-memory");
expect(entries).not.toContain(".zprofile");
expect(entries).not.toContain(".profile");
} finally {
await rm(home, { recursive: true, force: true });
}
});
it("uses the managed current layout without appending current twice", async () => {
for (const installRoot of ["/tmp/install", "/tmp/install/current"]) {
const output = captureStdout();
try {
expect(
await run([
"service-plan",
"--platform",
"linux",
"--home",
"/tmp/home",
"--install-root",
installRoot,
]),
).toBe(0);
expect(JSON.parse(output.value()).service_file.content).toContain(
"/tmp/install/current/runtime/runtime.cjs",
);
} finally {
output.restore();
}
}
});
it("uses one service identity for macOS lexical and physical tmp paths", async () => {
if (process.platform !== "darwin") return;
const root = await mkdtemp("/tmp/one-memory-service-root-");
try {
const installRoot = await mkdtemp(path.join(root, "install-"));
const physicalRoot = await realpath(installRoot);
const physicalParent = await realpath(root);
const serviceIds = async (candidates: string[]) => {
const ids: string[] = [];
for (const candidate of candidates) {
const output = captureStdout();
try {
expect(
await run([
"service-plan",
"--platform",
"darwin",
"--home",
path.join(root, "home"),
"--install-root",
candidate,
"--data-dir",
path.join(root, "data"),
]),
).toBe(0);
ids.push(
(JSON.parse(output.value()) as { service_id: string })
.service_id,
);
} finally {
output.restore();
}
}
return ids;
};
expect(
new Set(
await serviceIds([
installRoot,
path.join(installRoot, "current"),
physicalRoot,
path.join(physicalRoot, "current"),
]),
),
).toHaveProperty("size", 1);
expect(
new Set(
await serviceIds([
path.join(root, "missing", "install"),
path.join(root, "missing", "install", "current"),
path.join(physicalParent, "missing", "install"),
path.join(physicalParent, "missing", "install", "current"),
]),
),
).toHaveProperty("size", 1);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("explicit data flag overrides an invalid environment without trimming", async () => {
const output = captureStdout();
try {
expect(
await run(
["service-plan", ...serviceArgs, "--data-dir", "/tmp/data "],
{ ONE_MEMORY_DATA_DIR: "relative" },
),
).toBe(0);
expect(JSON.parse(output.value()).service_file.content).toContain(
'ONE_MEMORY_DATA_DIR=/tmp/data "',
);
} finally {
output.restore();
}
});
it("upgrade failure restarts the restored artifact with the same data identity", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
runtimeMocks.restartUserService.mockRejectedValueOnce(
new Error("restart failed"),
);
const streams = captureStreams();
let code: number;
try {
code = await run(
[
"upgrade",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--data-dir",
"/tmp/custom data",
"--yes",
],
{ ONE_MEMORY_DATA_DIR: "/tmp/other" },
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(JSON.parse(streams.stderr())).toMatchObject({
command: "upgrade",
code: "restart failed",
});
expect(runtimeMocks.rollbackLocalInstall).toHaveBeenCalledOnce();
// The CLI manages the objective aliases the package does not own
// (`memhub` + `memhub-cli`); both are rolled back.
expect(aliasMocks.rollbackManagedCliLink).toHaveBeenCalledTimes(2);
expect(runtimeMocks.restartUserService).toHaveBeenCalledTimes(2);
for (const call of runtimeMocks.restartUserService.mock
.calls as unknown[][]) {
expect(call[0]).toMatchObject({
dataDir: "/tmp/custom data",
installRoot: "/tmp/install/current",
});
}
});
// O-22 (2026-09-29 C8 H17): `upgrade --yes` on a manifest-valid but
// crash-looping version reported exit 0 and left `current` on the bad
// version. A restart that merely returns is not proof the new Runtime came
// up, so the CLI now verifies health and rolls back if it never does.
it("rolls back and fails when the upgraded Runtime never becomes healthy", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
const streams = captureStreams();
let code: number;
try {
code = await run(
[
"upgrade",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--data-dir",
"/tmp/custom data",
"--yes",
],
{},
{ waitForRuntimeReady: async () => false },
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(JSON.parse(streams.stderr())).toMatchObject({
command: "upgrade",
code: "upgrade_health_check_failed",
});
// Rolled back to the previous version and restarted it.
expect(runtimeMocks.rollbackLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.restartUserService).toHaveBeenCalledTimes(2);
});
it("reports success once the upgraded Runtime is healthy", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
const output = captureStdout();
let code: number;
try {
code = await run(
[
"upgrade",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--data-dir",
"/tmp/custom data",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(runtimeMocks.rollbackLocalInstall).not.toHaveBeenCalled();
});
it.each([
"service-plan",
"service-install",
"service-uninstall",
"install",
"upgrade",
])(
"%s rejects invalid and duplicate data dirs before planning or mutations",
async (command) => {
for (const invalid of [
"",
"relative",
"/tmp/a\nnext",
"/tmp/a\rnext",
"/tmp/a\0next",
]) {
vi.clearAllMocks();
const base = command.startsWith("service-")
? serviceArgs
: ["--source", "/tmp/dist"];
expect(await run([command, ...base, "--data-dir", invalid], {})).toBe(
2,
);
expect(
await run([command, ...base], { ONE_MEMORY_DATA_DIR: invalid }),
).toBe(2);
expect(runtimeMocks.planLocalInstall).not.toHaveBeenCalled();
expect(runtimeMocks.installUserService).not.toHaveBeenCalled();
expect(runtimeMocks.uninstallUserService).not.toHaveBeenCalled();
}
expect(
await run(
[
command,
...(command.startsWith("service-") ? serviceArgs : []),
"--data-dir",
"/tmp/a",
"--data-dir",
"/tmp/b",
],
{},
),
).toBe(2);
},
);
});
it("rejects unsupported commands without changing the system", async () => {
expect(await run(["unknown"])).toBe(2);
expect(
await run([
"service-plan",
"--platform",
"linux",
"--home",
"/home/example",
"--install-root",
"/opt/one-memory",
"--typo",
"true",
]),
).toBe(2);
});
it("uses platform-native default install roots", () => {
expect(defaultInstallRoot("darwin", "/Users/example")).toBe(
"/Users/example/Library/Application Support/One Memory",
);
expect(defaultInstallRoot("linux", "/home/example")).toBe(
"/home/example/.local/share/one-memory",
);
expect(defaultInstallRoot("win32", "C:\\Users\\Example")).toBe(
"C:\\Users\\Example\\AppData\\Local\\One Memory",
);
expect(
defaultInstallRoot("win32", "C:\\Users\\Example", "D:\\Local Data"),
).toBe("D:\\Local Data\\One Memory");
});
it("gives a custom install root its own Core port but leaves the daily root alone", async () => {
const planFor = async (home: string, installRoot: string) => {
const output = captureStdout();
try {
expect(
await run(
[
"service-plan",
"--platform",
"darwin",
"--home",
home,
"--install-root",
installRoot,
"--data-dir",
`${home}/data`,
"--uid",
"501",
],
{},
),
).toBe(0);
return JSON.parse(output.value()).service_file.content as string;
} finally {
output.restore();
}
};
const home = "/Users/example";
// Two custom roots must not both fall back to the fixed 8765: that is the
// `uninstall_refuses_when_data_lives_inside_the_removed_subtree` failure
// (the second root's Core died with EADDRINUSE).
const a = await planFor(home, "/Users/example/custom-A");
const b = await planFor(home, "/Users/example/custom-B");
const port = (plan: string) =>
plan.match(/ONE_MEMORY_CORE_PORT<\/key><string>(\d+)<\/string>/)?.[1] ??
null;
expect(port(a)).not.toBeNull();
expect(port(b)).not.toBeNull();
expect(port(a)).not.toBe(port(b));
// The daily install must keep the fixed default with no override key.
const daily = await planFor(
home,
`${home}/Library/Application Support/One Memory`,
);
expect(port(daily)).toBeNull();
});
it("prints a blocked Windows service plan instead of pretending support", async () => {
let output = "";
const original = process.stdout.write;
process.stdout.write = ((value: string) => {
output += value;
return true;
}) as typeof process.stdout.write;
try {
expect(
await run([
"service-plan",
"--platform",
"win32",
"--home",
"C:\\Users\\Example",
"--install-root",
"C:\\OneMemory",
]),
).toBe(4);
} finally {
process.stdout.write = original;
}
expect(JSON.parse(output).blocked_reason).toBe(
"windows_acl_and_user_service_not_verified",
);
});
it("previews a local install unless --yes is explicit", async () => {
runtimeMocks.applyLocalInstall.mockClear();
runtimeMocks.installUserService.mockClear();
runtimeMocks.installCliLink.mockClear();
const output = captureStdout();
const installRoot = await mkdtemp(path.join(tmpdir(), "om-cli-install-"));
try {
expect(
await run([
"install",
"--source",
"/tmp/distribution",
"--install-root",
installRoot,
]),
).toBe(0);
expect(JSON.parse(output.value()).apply).toBe(false);
expect(runtimeMocks.applyLocalInstall).not.toHaveBeenCalled();
expect(runtimeMocks.installUserService).not.toHaveBeenCalled();
expect(runtimeMocks.installCliLink).not.toHaveBeenCalled();
} finally {
output.restore();
}
});
it.each(["install", "upgrade"])(
"%s previews the matching remote Runtime without downloading the artifact",
async (command) => {
const resolveRelease = vi.fn(async () => ({
release: {
schema_version: 1 as const,
product: "memhub-release" as const,
version: "0.1.0-internal.1",
build_number: 1,
git_commit: "a".repeat(40),
channel: "prerelease" as const,
cli_protocol: 1,
supported_runtime_protocol: { min: 1, max: 1 },
runtime_protocol: 1,
supported_cli_protocol: { min: 1, max: 1 },
published_at: "2026-10-13T06:00:00Z",
artifacts: [],
},
artifact: {
kind: "runtime" as const,
platform: (process.platform === "darwin" ? "darwin" : "linux") as
| "darwin"
| "linux",
arch: (process.arch === "arm64" ? "arm64" : "x64") as "arm64" | "x64",
format: "tar.gz" as const,
filename: "memhub-runtime.tar.gz",
url: "https://downloads.example.test/runtime.tar.gz",
bytes: 300_000_000,
unpacked_bytes: 700_000_000,
sha256: "a".repeat(64),
runtime_manifest_schema: 1 as const,
},
manifest_url: "https://downloads.example.test/release-manifest.json",
allowed_hosts: ["downloads.example.test"],
}));
const acquireRelease = vi.fn(async () => {
throw new Error("preview_downloaded_runtime");
});
const output = captureStdout();
try {
expect(
await productRun(
[
command,
"--home",
"/tmp/remote-home",
"--install-root",
"/tmp/remote-install",
"--release-manifest-url",
"https://downloads.example.test/release-manifest.json",
"--json",
],
{},
{
resolveRuntimeRelease: resolveRelease as never,
acquireRuntimeDistribution: acquireRelease as never,
},
),
).toBe(0);
} finally {
output.restore();
}
expect(resolveRelease).toHaveBeenCalledOnce();
expect(acquireRelease).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
release: {
version: "0.1.0-internal.1",
channel: "prerelease",
git_commit: "a".repeat(40),
cli_protocol: 1,
runtime_protocol: 1,
download_bytes: 300_000_000,
install_bytes: 700_000_000,
},
install_root: "/tmp/remote-install",
data_preserved: true,
apply: false,
});
},
);
it("fails closed when a source build has no configured Release URL", async () => {
const streams = captureStreams();
try {
expect(
await productRun(
[
"install",
"--home",
"/tmp/no-release-home",
"--install-root",
"/tmp/no-release-install",
"--json",
],
{},
),
).toBe(1);
expect(streams.stdout()).toBe("");
expect(JSON.parse(streams.stderr())).toMatchObject({
status: "error",
code: "release_manifest_url_unavailable",
});
} finally {
streams.restore();
}
});
it("uses a runtime-configured same-version Release template", async () => {
const resolveRelease = vi.fn(async (options: { manifestUrl: string }) => {
throw new Error(`observed:${options.manifestUrl}`);
});
const output = captureStdout();
try {
expect(
await productRun(
[
"install",
"--home",
"/tmp/template-home",
"--install-root",
"/tmp/template-install",
"--json",
],
{
MEMHUB_RELEASE_MANIFEST_URL_TEMPLATE:
"https://releases.example.test/v{version}/release-manifest.json",
},
{ resolveRuntimeRelease: resolveRelease as never },
),
).toBe(1);
} finally {
output.restore();
}
expect(resolveRelease).toHaveBeenCalledWith(
expect.objectContaining({
manifestUrl: `https://releases.example.test/v${ROOT_VERSION}/release-manifest.json`,
}),
);
});
it("downloads a remote Runtime only for --yes and cleans its temporary directory", async () => {
const cleanup = vi.fn(async () => undefined);
const acquireRelease = vi.fn(async () => ({
release: {
schema_version: 1 as const,
product: "memhub-release" as const,
version: "0.1.0-internal.1",
build_number: 1,
git_commit: "a".repeat(40),
channel: "prerelease" as const,
cli_protocol: 1,
supported_runtime_protocol: { min: 1, max: 1 },
runtime_protocol: 1,
supported_cli_protocol: { min: 1, max: 1 },
published_at: "2026-10-13T06:00:00Z",
artifacts: [],
},
artifact: {
kind: "runtime" as const,
platform: "linux" as const,
arch: "x64" as const,
format: "tar.gz" as const,
filename: "runtime.tar.gz",
url: "https://downloads.example.test/runtime.tar.gz",
bytes: 1,
unpacked_bytes: 1,
sha256: "a".repeat(64),
runtime_manifest_schema: 1 as const,
},
temporary_root: "/tmp/download-root",
distribution: "/tmp/download-root/distribution",
cleanup,
[Symbol.asyncDispose]: cleanup,
}));
const output = captureStdout();
try {
expect(
await productRun(
[
"install",
"--home",
"/tmp/remote-home",
"--install-root",
"/tmp/remote-install",
"--release-manifest-url",
"https://downloads.example.test/release-manifest.json",
"--yes",
"--json",
],
{},
{
acquireRuntimeDistribution: acquireRelease as never,
waitForRuntimeReady: async () => true,
},
),
).toBe(0);
} finally {
output.restore();
}
expect(acquireRelease).toHaveBeenCalledOnce();
expect(runtimeMocks.planLocalInstall).toHaveBeenCalledWith(
expect.objectContaining({ source: "/tmp/download-root/distribution" }),
);
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
expect(cleanup).toHaveBeenCalledOnce();
});
it("keeps --source fully offline", async () => {
const resolveRelease = vi.fn(async () => {
throw new Error("local_source_used_network");
});
const acquireRelease = vi.fn(async () => {
throw new Error("local_source_used_network");
});
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/offline-distribution",
"--install-root",
"/tmp/offline-install",
],
{},
{
resolveRuntimeRelease: resolveRelease as never,
acquireRuntimeDistribution: acquireRelease as never,
},
),
).toBe(0);
} finally {
output.restore();
}
expect(resolveRelease).not.toHaveBeenCalled();
expect(acquireRelease).not.toHaveBeenCalled();
expect(runtimeMocks.planLocalInstall).toHaveBeenCalledWith(
expect.objectContaining({ source: "/tmp/offline-distribution" }),
);
});
it("applies a verified local install only with --yes", async () => {
runtimeMocks.applyLocalInstall.mockClear();
runtimeMocks.installUserService.mockClear();
runtimeMocks.installCliLink.mockClear();
aliasMocks.installManagedCliLink.mockClear();
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/distribution",
"--install-root",
"/tmp/one-memory",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
} finally {
output.restore();
}
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.installCliLink).toHaveBeenCalledOnce();
expect(runtimeMocks.installUserService).toHaveBeenCalledOnce();
expect(aliasMocks.installManagedCliLink).toHaveBeenCalledWith(
expect.objectContaining({
link_path: expect.stringMatching(/\/memhub$/),
}),
);
expect(JSON.parse(output.value()).apply).toBe(true);
});
it("rolls back the distribution when service installation fails", async () => {
runtimeMocks.applyLocalInstall.mockResolvedValueOnce({
schema_version: 1,
source: "/tmp/distribution",
install_root: "/tmp/one-memory",
version_directory: "/tmp/one-memory/versions/1.0.0-linux-x64",
current_link: "/tmp/one-memory/current",
version: "1.0.0",
platform: "linux",
arch: "x64",
files: 9,
previous_current_target: null,
created_version: true,
});
runtimeMocks.installUserService.mockRejectedValueOnce(
new Error("service failed"),
);
runtimeMocks.rollbackLocalInstall.mockClear();
runtimeMocks.rollbackCliLink.mockClear();
aliasMocks.rollbackManagedCliLink.mockClear();
const streams = captureStreams();
let code: number;
try {
code = await run([
"install",
"--source",
"/tmp/distribution",
"--install-root",
"/tmp/one-memory",
"--yes",
]);
} finally {
streams.restore();
}
// A failed install now reports a structured error instead of throwing.
expect(code!).toBe(1);
expect(JSON.parse(streams.stderr())).toMatchObject({
command: "install",
status: "error",
code: "service failed",
});
expect(runtimeMocks.rollbackLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.rollbackCliLink).toHaveBeenCalledOnce();
expect(aliasMocks.rollbackManagedCliLink).toHaveBeenCalledTimes(2);
});
it("upgrades only an existing managed CLI and rolls back a failed restart", async () => {
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/one-memory",
bin_directory: "/tmp/home/.local/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/home/.local/bin/one-memory",
target_path: "/tmp/one-memory/current/bin/one-memory",
});
runtimeMocks.restartUserService.mockRejectedValueOnce(
new Error("restart failed"),
);
runtimeMocks.rollbackLocalInstall.mockClear();
runtimeMocks.installCliLink.mockClear();
const streams = captureStreams();
let code: number;
try {
code = await run([
"upgrade",
"--source",
"/tmp/distribution",
"--install-root",
"/tmp/one-memory",
"--home",
"/tmp/home",
"--yes",
]);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(JSON.parse(streams.stderr())).toMatchObject({
command: "upgrade",
status: "error",
code: "restart failed",
});
expect(runtimeMocks.rollbackLocalInstall).toHaveBeenCalledOnce();
expect(runtimeMocks.installCliLink).not.toHaveBeenCalled();
});
it("previews service installation and applies only with --yes", async () => {
runtimeMocks.installUserService.mockClear();
const output = captureStdout();
const platform = process.platform === "darwin" ? "darwin" : "linux";
try {
expect(
await run([
"service-install",
"--platform",
platform,
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
]),
).toBe(0);
expect(runtimeMocks.installUserService).not.toHaveBeenCalled();
} finally {
output.restore();
}
const applied = captureStdout();
try {
expect(
await run(
[
"service-install",
"--platform",
platform,
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
} finally {
applied.restore();
}
expect(runtimeMocks.installUserService).toHaveBeenCalledOnce();
});
it("previews service removal and applies only with --yes", async () => {
runtimeMocks.uninstallUserService.mockClear();
const platform = process.platform === "darwin" ? "darwin" : "linux";
const preview = captureStdout();
try {
expect(
await run([
"service-uninstall",
"--platform",
platform,
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
]),
).toBe(0);
expect(runtimeMocks.uninstallUserService).not.toHaveBeenCalled();
} finally {
preview.restore();
}
const applied = captureStdout();
try {
expect(
await run([
"service-uninstall",
"--platform",
platform,
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
]),
).toBe(0);
} finally {
applied.restore();
}
expect(runtimeMocks.uninstallUserService).toHaveBeenCalledOnce();
});
it("product uninstall removes the managed memhub alias", async () => {
vi.clearAllMocks();
const output = captureStdout();
try {
expect(
await run([
"uninstall",
"--source",
"/tmp/distribution",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
]),
).toBe(0);
} finally {
output.restore();
}
expect(aliasMocks.removeManagedCliLink).toHaveBeenCalledWith(
expect.objectContaining({ link_path: "/tmp/home/.local/bin/memhub" }),
);
});
// H15 P0 regression: integ's runtime-control now makes `<bin>/memhub` its
// primary link and creates the `one-memory` compat itself (it declares
// `compat_link_path`). The CLI must not re-plan those two — doing so raised
// `cli_alias_plan_stale`. It still owns the objective's `memhub-cli` name,
// which the package does not create.
it("defers to runtime-control for its links but still adds memhub-cli", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/home/.local/bin",
status: "create",
blocked_reason: null,
link_path: "/tmp/home/.local/bin/memhub",
target_path: "/tmp/install/current/bin/one-memory",
// The fields integ's `01eedd8` added; the CLI must tolerate them.
compat_link_path: "/tmp/home/.local/bin/one-memory",
compat_target_path: "/tmp/install/current/bin/one-memory",
} as never);
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/distribution",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
} finally {
output.restore();
}
// Runtime-control owns `memhub` + `one-memory`; the CLI plans only the
// remaining objective alias `memhub-cli`.
const planned = aliasMocks.planManagedCliLink.mock.calls.map((call) =>
path.basename((call[1] as string) ?? ""),
);
expect(planned).toEqual(["memhub-cli"]);
// The CLI still installed the distribution and the PATH block.
expect(runtimeMocks.applyLocalInstall).toHaveBeenCalledOnce();
expect(shellPathMocks.applyShellPath).toHaveBeenCalled();
});
// The objective names `memhub` and `memhub-cli` as the primary entries, and
// the npm tarball ships all three names. The managed installer must create
// the same set, otherwise "装完即短指令可用" is only true for the names the
// installer happened to create.
it("creates the memhub-cli primary alias during install", async () => {
vi.clearAllMocks();
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/distribution",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
} finally {
output.restore();
}
const planned = aliasMocks.planManagedCliLink.mock.calls.map((call) =>
path.basename((call[1] as string) ?? ""),
);
// The older link plan owns only `one-memory`, so the CLI must create both
// objective aliases: `memhub` (primary) and `memhub-cli`.
expect(new Set(planned)).toEqual(new Set(["memhub", "memhub-cli"]));
});
it("removes the CLI-owned memhub-cli alias on uninstall", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/home/.local/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/home/.local/bin/memhub",
target_path: "/tmp/install/current/bin/one-memory",
compat_link_path: "/tmp/home/.local/bin/one-memory",
compat_target_path: "/tmp/install/current/bin/one-memory",
} as never);
const output = captureStdout();
try {
expect(
await run([
"uninstall",
"--source",
"/tmp/distribution",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
]),
).toBe(0);
} finally {
output.restore();
}
// runtime-control removes `memhub` + `one-memory`; the CLI removes the one
// objective alias it created and the package does not own (`memhub-cli`).
expect(aliasMocks.removeManagedCliLink).toHaveBeenCalledWith(
expect.objectContaining({
link_path: "/tmp/home/.local/bin/memhub-cli",
}),
);
});
// C8 P1 (2026-09-29): runtime-control's `applyLocalUninstall` only removes
// `plan.cli_link`; it never removes the `compat_link_path` it created. So the
// CLI must NOT treat the compat name as "owned by the package" on uninstall —
// otherwise nobody removes it and a dangling `one-memory` symlink survives
// (its target `current/` is deleted first).
it("also removes runtime-control's compat link so uninstall leaves no dangling entry", async () => {
vi.clearAllMocks();
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/home/.local/bin",
status: "present",
blocked_reason: null,
link_path: "/tmp/home/.local/bin/memhub",
target_path: "/tmp/install/current/bin/one-memory",
compat_link_path: "/tmp/home/.local/bin/one-memory",
compat_target_path: "/tmp/install/current/bin/one-memory",
} as never);
const output = captureStdout();
try {
expect(
await run([
"uninstall",
"--source",
"/tmp/distribution",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--yes",
]),
).toBe(0);
} finally {
output.restore();
}
const removed = (
aliasMocks.removeManagedCliLink.mock.calls as unknown as Array<
[{ link_path: string }]
>
).map((call) => call[0].link_path);
expect(removed).toContain("/tmp/home/.local/bin/one-memory");
expect(removed).toContain("/tmp/home/.local/bin/memhub-cli");
});
});
function captureStdout() {
let value = "";
const original = process.stdout.write;
process.stdout.write = ((chunk: string) => {
value += chunk;
return true;
}) as typeof process.stdout.write;
return {
value: () => value,
restore: () => (process.stdout.write = original),
};
}
// A stopped Runtime is not an argument error. These commands attach directly
// (rather than going through the read-command path) and used to fold
// "runtime not running" into `usage_error`/exit 2, so a script could not tell
// a typo from a missing Runtime. They must answer exit 3 + `runtime_not_running`.
describe("stopped Runtime is exit 3, not a usage error", () => {
it.each([
["path", "list", "--json"],
["adapter", "list", "--json"],
["task", "--id", "nope", "--json"],
[
"write",
"submit",
"--session",
"s",
"--messages-file",
"/tmp/messages.json",
"--json",
],
["backup", "create", "--target", "/tmp/x", "--yes", "--json"],
])("%s ... --json exits 3 with runtime_not_running", async (...argv) => {
const streams = captureStreams();
let code: number;
try {
code = await run(argv as string[], {});
} finally {
streams.restore();
}
expect(code!).toBe(3);
const document = JSON.parse(streams.stderr());
expect(document).toMatchObject({
code: "runtime_not_running",
exit_code: 3,
});
// Never the usage-error shape.
expect(document.status).not.toBe("usage_error");
});
it.each(["pause", "resume"] as const)(
"parses sync %s as the direct sync-set command",
async (action) => {
const streams = captureStreams();
try {
expect(
await run(["sync", action, "--source", "codex", "--json"], {}),
).toBe(3);
} finally {
streams.restore();
}
expect(JSON.parse(streams.stdout())).toMatchObject({
state: "unknown",
code: "runtime_not_running",
});
expect(streams.stderr()).toBe("");
},
);
});
// The objective requires "稳定 JSON/exit code" for every declared command.
// `start`/`stop`/`restart` accepted only the bare verb, so `--json` came back
// as a usage error (exit 2) — a script could not drive lifecycle in JSON.
describe("lifecycle commands accept --json", () => {
it.each(["start", "stop", "restart"])(
"%s --json is not a usage error and emits a JSON document",
async (command) => {
const output = captureStdout();
let code: number;
try {
code = await run([command, "--json"], {});
} finally {
output.restore();
}
// No Runtime is running here, so it stops at the runtime/service check.
// What matters: it is not rejected as an argument error, and stdout is a
// single JSON object a script can parse.
expect(code!).not.toBe(2);
const text = output.value().trim();
expect(text.length).toBeGreaterThan(0);
const document = JSON.parse(text);
expect(document).toMatchObject({
schema_version: 1,
product: PRODUCT,
command,
});
expect(typeof document.state).toBe("string");
},
);
it("restart uses the platform service path or the direct-process fallback", async () => {
const restartManagedRuntime = vi.fn(async () => "absent" as const);
const stopRuntime = vi.fn(async () => "stopped" as const);
const ensureRuntime = vi.fn(async () => "started" as const);
const output = captureStdout();
let code: number;
try {
code = await run(
["restart", "--json"],
{ ONE_MEMORY_DATA_DIR: "/tmp/restart-data" },
{ restartManagedRuntime, stopRuntime, ensureRuntime },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
if (process.platform === "darwin") {
expect(restartManagedRuntime).toHaveBeenCalledOnce();
expect(stopRuntime).toHaveBeenCalledOnce();
expect(ensureRuntime).toHaveBeenCalledOnce();
} else {
expect(restartManagedRuntime).not.toHaveBeenCalled();
expect(stopRuntime).toHaveBeenCalledOnce();
expect(ensureRuntime).toHaveBeenCalledOnce();
}
expect(JSON.parse(output.value())).toMatchObject({
command: "restart",
state: "restarted",
code: "runtime_restarted",
});
});
});
// The objective names `memhub host install <host>` (host as a positional). Both
// that and the `--host <id>` flag form must reach the same command; naming both
// at once is ambiguous and refused rather than guessed.
describe("host short form accepts a positional host id", () => {
it("parses `host install <id>` without a usage error", async () => {
const streams = captureStreams();
let code: number;
try {
// No Runtime is running in this test, so the command stops at the
// runtime check. What matters is that it is *not* a usage error (2).
code = await run(["host", "install", "codex"], {});
} finally {
streams.restore();
}
expect(code!).not.toBe(2);
});
it("parses `host status <id> --json` and reports the host id", async () => {
const streams = captureStreams();
try {
await run(["host", "status", "codex", "--json"], {});
} finally {
streams.restore();
}
const out = streams.stdout();
if (out.trim() !== "") {
expect(JSON.parse(out)).toMatchObject({ host_id: "codex" });
}
});
it("refuses naming the host both positionally and with --host", async () => {
const streams = captureStreams();
let code: number;
try {
code = await run(["host", "install", "codex", "--host", "cursor"], {});
} finally {
streams.restore();
}
expect(code!).toBe(2);
});
});
// A8 (C8 H18r3): the readiness predicate must require `runtime_verified` with an
// C8 H18r4 root-cause (ee07013): `install` registered the service and returned
// 0 without ever calling `waitForRuntimeReady`, unlike start/restart/upgrade.
// On a host where Core cold-start stalls, install therefore reported success
// while `status` was still `runtime_service_running` (not ready).
describe("install gates on Runtime readiness", () => {
// `install`/`upgrade` plan-stage failures (unmanaged current, missing source)
// used to escape `planLocalInstall` as a bare thrown string, so `--json` got
// plain text on stderr instead of the structured envelope every other failure
// has. A script must get a parseable document.
it.each(["install", "upgrade"])(
"%s --json reports a plan-stage failure as structured JSON, not bare text",
async (command) => {
vi.clearAllMocks();
runtimeMocks.planLocalInstall.mockRejectedValueOnce(
new Error("install_current_unmanaged"),
);
const streams = captureStreams();
let code: number;
try {
code = await run(
[
command,
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--bin-dir",
"/tmp/bin",
"--data-dir",
"/tmp/data",
"--yes",
"--json",
],
{},
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
const err = streams.stderr().trim();
expect(err.startsWith("{")).toBe(true);
const document = JSON.parse(err);
expect(document).toMatchObject({
command,
status: "error",
code: "install_current_unmanaged",
});
},
);
function stubInstallPlan() {
runtimeMocks.planCliLink.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
home: "/tmp/home",
install_root: "/tmp/install",
bin_directory: "/tmp/bin",
status: "create",
blocked_reason: null,
link_path: "/tmp/bin/one-memory",
target_path: "/tmp/install/current/bin/one-memory",
});
}
it("fails and rolls back when the installed Runtime never becomes ready", async () => {
vi.clearAllMocks();
stubInstallPlan();
const streams = captureStreams();
let code: number;
try {
code = await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--bin-dir",
"/tmp/bin",
"--data-dir",
"/tmp/data",
"--yes",
],
{},
{ waitForRuntimeReady: async () => false },
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(streams.stderr()).toContain("install_runtime_not_ready");
expect(runtimeMocks.rollbackLocalInstall).toHaveBeenCalledOnce();
expect(
lifecycleMocks.rollbackPreparedServiceInstall,
).toHaveBeenCalledOnce();
});
it("takes CLI ownership after a successful install over an App-owned Runtime", async () => {
vi.clearAllMocks();
stubInstallPlan();
const root = await mkdtemp(path.join(tmpdir(), "memhub-cli-owner-"));
const dataDir = path.join(root, "data");
await claimRuntimeServiceForApp({
dataDir,
appBundle: "/Applications/MemHub.app",
appExecutable: "/Applications/MemHub.app/Contents/MacOS/MemHub",
});
const output = captureStdout();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
path.join(root, "home"),
"--install-root",
path.join(root, "install"),
"--bin-dir",
path.join(root, "bin"),
"--data-dir",
dataDir,
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
),
).toBe(0);
await expect(readRuntimeServiceOwner(dataDir)).resolves.toEqual({
schema_version: 1,
owner: "cli",
});
} finally {
output.restore();
await rm(root, { recursive: true, force: true });
}
});
it("restores App ownership before relaunching the old Runtime after install failure", async () => {
vi.clearAllMocks();
stubInstallPlan();
const root = await mkdtemp(path.join(tmpdir(), "memhub-cli-owner-"));
const dataDir = path.join(root, "data");
await claimRuntimeServiceForApp({
dataDir,
appBundle: "/Applications/MemHub.app",
appExecutable: "/Applications/MemHub.app/Contents/MacOS/MemHub",
});
lifecycleMocks.rollbackPreparedServiceInstall.mockImplementationOnce(
async (_prepared: unknown, rollback: () => Promise<void>) => {
await rollback();
await expect(readRuntimeServiceOwner(dataDir)).resolves.toMatchObject({
owner: "app",
});
},
);
const streams = captureStreams();
try {
expect(
await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
path.join(root, "home"),
"--install-root",
path.join(root, "install"),
"--bin-dir",
path.join(root, "bin"),
"--data-dir",
dataDir,
"--yes",
],
{},
{ waitForRuntimeReady: async () => false },
),
).toBe(1);
await expect(readRuntimeServiceOwner(dataDir)).resolves.toMatchObject({
owner: "app",
});
} finally {
streams.restore();
await rm(root, { recursive: true, force: true });
}
});
it("succeeds when the installed Runtime becomes ready", async () => {
vi.clearAllMocks();
stubInstallPlan();
const output = captureStdout();
let code: number;
try {
code = await run(
[
"install",
"--source",
"/tmp/dist",
"--home",
"/tmp/home",
"--install-root",
"/tmp/install",
"--bin-dir",
"/tmp/bin",
"--data-dir",
"/tmp/data",
"--yes",
],
{},
{ waitForRuntimeReady: async () => true },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(runtimeMocks.rollbackLocalInstall).not.toHaveBeenCalled();
});
});
// address, not just "a process exists". A launchd pid status
// (`runtime_service_running`, no host/port) once made `start` return in 0s
// while Core was still ~12s from answering.
describe("runtime readiness predicate", () => {
it("treats a verified runtime with an address as ready", () => {
expect(
runtimeIsReady({
state: "running",
code: "runtime_verified",
host: "127.0.0.1",
port: 8765,
}),
).toBe(true);
});
it("does not treat a launchd-pid-only status as ready", () => {
expect(
runtimeIsReady({
state: "running",
code: "runtime_service_running",
}),
).toBe(false);
});
it("does not treat stopped/unknown/null as ready", () => {
expect(
runtimeIsReady({ state: "stopped", code: "runtime_not_running" }),
).toBe(false);
expect(runtimeIsReady(null)).toBe(false);
});
});
// `ui` is in the objective's declared command list, but Core does not yet serve
// same-origin web assets or exchange a one-time code for an HttpOnly session.
// The command must be declared and honest — blocked with a stable code — and
// must never build a URL that carries the bare Core token.
describe("ui wires the Core session contract without token leakage", () => {
// With no Runtime attached (the mocked default), `ui` cannot start a session
// and must say so honestly instead of inventing a URL.
it("reports a stable blocked document and exit code in JSON", async () => {
const output = captureStdout();
let code: number;
try {
code = await run(["ui", "--json"], {});
} finally {
output.restore();
}
expect(code!).toBe(4);
const document = JSON.parse(output.value());
expect(document).toMatchObject({
schema_version: 1,
product: PRODUCT,
command: "ui",
state: "blocked",
code: "runtime_not_running",
});
// A bare token in a URL is what the contract forbids, so no token may
// appear anywhere in the output.
expect(output.value()).not.toMatch(/token=[A-Za-z0-9]/i);
});
it("is documented in help with its blocked status", async () => {
const output = captureStdout();
try {
expect(await run(["help", "ui"], {})).toBe(0);
} finally {
output.restore();
}
expect(output.value()).toContain("blocked");
});
});
// `backup restore` replaces live data, so it must be honest at every gate:
// refuse while the Runtime owns the data dir, stop+call Core from a
// maintenance process, and never report success from a malformed response.
describe("backup restore drives the Core managed-restore contract", () => {
it("rejects while the Runtime is running, with a distinct exit code", async () => {
runtimeMocks.inspectRuntimeStatus.mockResolvedValueOnce({
state: "running",
code: "runtime_verified",
});
const output = captureStdout();
let code: number;
try {
code = await run(
[
"backup",
"restore",
"--source",
"/tmp/x",
"--yes",
"--confirm",
"/tmp/x",
"--json",
],
{},
);
} finally {
output.restore();
}
expect(code!).toBe(1);
expect(JSON.parse(output.value())).toMatchObject({
product: PRODUCT,
command: "backup",
verb: "restore",
state: "rejected",
code: "backup_restore_runtime_running",
});
});
it("reports the managed restore result after Core performs the swap", async () => {
const maintenance = vi.fn(async () => ({
state: "restored" as const,
result: {
state: "restored" as const,
restoredBytes: 4096,
previousBackupPath: "/home/.one-memory/backups/pre-restore.sqlite3",
sourceSchemaVersion: 34,
},
}));
const output = captureStdout();
let code: number;
try {
code = await run(
[
"backup",
"restore",
"--source",
"/home/.one-memory/backups/a.sqlite3",
"--yes",
"--confirm",
"/home/.one-memory/backups/a.sqlite3",
"--json",
],
{},
{ runMaintenanceRestore: maintenance },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(maintenance).toHaveBeenCalledWith({
coreCommand: expect.any(String),
dataDir: expect.any(String),
source: "/home/.one-memory/backups/a.sqlite3",
});
expect(JSON.parse(output.value())).toMatchObject({
schema_version: 1,
product: PRODUCT,
command: "backup",
verb: "restore",
state: "restored",
restored_bytes: 4096,
previous_backup_path: "/home/.one-memory/backups/pre-restore.sqlite3",
source_schema_version: 34,
});
});
it("surfaces Core's refusal code and exits 1", async () => {
const maintenance = vi.fn(async () => ({
state: "rejected" as const,
code: "backup_restore_source_invalid",
}));
const output = captureStdout();
let code: number;
try {
code = await run(
[
"backup",
"restore",
"--source",
"/tmp/x",
"--yes",
"--confirm",
"/tmp/x",
"--json",
],
{},
{ runMaintenanceRestore: maintenance },
);
} finally {
output.restore();
}
expect(code!).toBe(1);
expect(JSON.parse(output.value())).toMatchObject({
command: "backup",
verb: "restore",
state: "rejected",
code: "backup_restore_source_invalid",
});
});
it("requires --confirm to repeat the exact source path", async () => {
const maintenance = vi.fn();
const output = captureStdout();
let code: number;
try {
code = await run(
[
"backup",
"restore",
"--source",
"/home/.one-memory/backups/a.sqlite3",
"--yes",
"--confirm",
"/home/.one-memory/backups/b.sqlite3",
"--json",
],
{},
{ runMaintenanceRestore: maintenance },
);
} finally {
output.restore();
}
expect(code!).toBe(1);
expect(maintenance).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
command: "backup",
verb: "restore",
state: "rejected",
code: "backup_restore_confirmation_required",
});
});
it("fails closed when a live service lease survives a missing receipt", async () => {
const home = await mkdtemp(path.join(tmpdir(), "memhub-restore-lease-"));
const dataDir = path.join(home, "data");
await import("node:fs/promises").then(({ mkdir, writeFile }) =>
mkdir(path.join(dataDir, "runtime-service.lock"), {
recursive: true,
mode: 0o700,
}).then(() =>
writeFile(
path.join(dataDir, "runtime-service.lock", "owner.json"),
JSON.stringify({ pid: process.pid }),
{ mode: 0o600 },
),
),
);
const maintenance = vi.fn();
const output = captureStdout();
let code: number;
try {
code = await run(
[
"backup",
"restore",
"--source",
"/tmp/backup.sqlite3",
"--yes",
"--confirm",
"/tmp/backup.sqlite3",
"--json",
],
{ ONE_MEMORY_DATA_DIR: dataDir },
{ runMaintenanceRestore: maintenance },
);
} finally {
output.restore();
await rm(home, { recursive: true, force: true });
}
expect(code!).toBe(1);
expect(maintenance).not.toHaveBeenCalled();
expect(JSON.parse(output.value())).toMatchObject({
command: "backup",
verb: "restore",
state: "rejected",
code: "backup_restore_runtime_state_unknown",
});
});
});
// A typo'd flag must be rejected, not silently dropped. `path list`/`adapter
// list` matched on `arguments_.includes("list")` without validating the rest,
// so `path list --bogus`/`adapter list --bogus` ran anyway (and here returned
// the runtime-not-running code instead of a usage error).
// `--json` is documented under "Global options", so `memhub --json <cmd>` must
// work like `memhub <cmd> --json`. It used to be treated as the command name
// and fail with "未知命令".
describe("leading --json is accepted as a global option", () => {
it("accepts --json before the command and still emits JSON", async () => {
const output = captureStreams();
let code: number;
try {
code = await run(["--json", "status"], {});
} finally {
output.restore();
}
expect(code!).not.toBe(2);
expect(output.stderr()).not.toContain("未知命令");
// The success document comes back as a single JSON object on stdout.
expect(JSON.parse(output.stdout())).toMatchObject({
schema_version: 1,
product: PRODUCT,
});
});
it("keeps a trailing --json working too", async () => {
const output = captureStreams();
try {
await run(["status", "--json"], {});
} finally {
output.restore();
}
expect(JSON.parse(output.stdout()).product).toBe(PRODUCT);
});
});
describe("path/adapter list reject unknown flags", () => {
it.each([
["path", "list", "--bogus"],
["adapter", "list", "--bogus"],
["path", "list", "--json", "--bogus"],
])("%s %s %s exits 2", async (...argv) => {
expect(await run(argv as string[], {})).toBe(2);
});
it("adapter probe rejects a stray flag or extra positional", async () => {
// Only `adapter probe --source <id> [--json]` is valid.
expect(
await run(["adapter", "probe", "--source", "codex", "--bogus"], {}),
).toBe(2);
expect(
await run(["adapter", "probe", "--source", "codex", "junk"], {}),
).toBe(2);
});
it("adapter probe accepts the valid form (positive control)", async () => {
// No Runtime is running here, so it stops at the runtime check — NOT a
// usage error. This is the guard that the strict check above did not
// over-tighten and reject the legitimate invocation.
expect(
await run(["adapter", "probe", "--source", "codex", "--json"], {}),
).not.toBe(2);
});
});
describe("init onboarding", () => {
it("refuses a repeated --json rather than guessing which one counted", async () => {
expect(await run(["init", "--json", "--json"], {})).toBe(2);
});
it.each([
["path", "list"],
["adapter", "list"],
])("refuses a repeated --json on %s %s", async (...command) => {
expect(await run([...command, "--json", "--json"], {})).toBe(2);
});
it("refuses --source with no value", async () => {
expect(await run(["init", "--source"], {})).toBe(2);
});
it("refuses --source whose value is another flag", async () => {
// Otherwise "--source --import" would connect a source literally named
// "--import".
expect(await run(["init", "--source", "--import"], {})).toBe(2);
});
it("refuses an unknown flag", async () => {
expect(await run(["init", "--bogus"], {})).toBe(2);
});
it("refuses the same source twice", async () => {
// A duplicate would connect one source while reporting two.
expect(
await run(["init", "--source", "codex", "--source", "codex"], {}),
).toBe(2);
});
// Boolean flags were idempotent, but accepting them twice hid a duplicated
// argv fragment that could also have duplicated a value-bearing flag.
it.each(["--import", "--full-rescan", "--confirm-overrides", "--dry-run"])(
"refuses a repeated init %s flag",
async (flag) => {
expect(await run(["init", "--source", "codex", flag, flag], {})).toBe(2);
},
);
it("refuses a repeated host approve fingerprint", async () => {
const fingerprintA = "a".repeat(64);
const fingerprintB = "b".repeat(64);
expect(
await run(
[
"host",
"install",
"--host",
"codex",
"--approve",
fingerprintA,
"--approve",
fingerprintB,
],
{},
),
).toBe(2);
});
it("refuses a repeated host sync overwrite flag", async () => {
expect(
await run(
[
"host",
"sync",
"skill",
"--host",
"codex",
"--overwrite-modified",
"--overwrite-modified",
],
{},
),
).toBe(2);
});
// A duplicate paging flag silently took the last value. Scripts that build
// their argv from several sources could then page with a value they never
// intended, and the CLI would not even reach the argument error.
it.each([
["--limit", "1", "2"],
["--offset", "0", "1"],
])(
"refuses a repeated %s memory paging flag",
async (flag, first, second) => {
const streams = captureStreams();
try {
expect(
await run(
["memories", "list", flag, first, flag, second, "--json"],
{},
),
).toBe(2);
} finally {
streams.restore();
}
const payload = JSON.parse(streams.stderr());
expect(payload).toMatchObject({
status: "usage_error",
command: "memories",
exit_code: 2,
});
},
);
it("accepts --dry-run together with --import", async () => {
// Parsing must accept the pair; it stops at the Runtime check, which is
// not a usage failure.
expect(
await run(["init", "--source", "codex", "--import", "--dry-run"], {}),
).not.toBe(2);
});
});
// A1/A9 (2026-09-29 acceptance): a failed `service-install --yes` surfaced as
// a bare thrown string, so the caller saw only "runtime_service_command_failed"
// with no machine-readable envelope or next step. And uninstall has to be
// provably program-only: it must name the preserved data directory and must not
// ever remove it.
describe("service lifecycle failure and data-safety contract", () => {
const serviceArgs = [
"--platform",
"linux",
"--home",
"/tmp/selected-home",
"--install-root",
"/tmp/install",
];
it("reports a service command failure as a structured error, not a throw", async () => {
runtimeMocks.installUserService.mockRejectedValueOnce(
new Error("runtime_service_command_failed"),
);
const streams = captureStreams();
let code: number;
try {
code = await run(
[..."service-install".split(" "), ...serviceArgs, "--yes"],
{},
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(streams.stdout()).toBe("");
const error = JSON.parse(streams.stderr());
expect(error).toMatchObject({
schema_version: 1,
product: PRODUCT,
command: "service-install",
status: "error",
code: "runtime_service_command_failed",
exit_code: 1,
});
expect(error.next_step.length).toBeGreaterThan(0);
});
// C0 `4dc3f0d` / C8 `C0-service-install-readiness-gap-20260930.md`:
// `service-install --yes` used to return 0 as soon as the OS accepted the
// registration, even when the Runtime never became `runtime_verified`.
// require the same readiness predicate as start/restart/install/upgrade and
// fail with a stable structured code when the budget expires.
it("fails closed when service registration never becomes ready", async () => {
runtimeMocks.installUserService.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
scope: "user",
service_id: "one-memory.service",
status: "ready",
blocked_reason: null,
service_file: {
path: "/tmp/one-memory.service",
mode: 0o600,
content: "unit",
},
commands: { install: [], start: [], stop: [], uninstall: [] },
options: {},
});
const streams = captureStreams();
let code: number;
try {
code = await run(
["service-install", ...serviceArgs, "--yes", "--data-dir", "/tmp/data"],
{},
{ waitForRuntimeReady: async () => false },
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(streams.stdout()).toBe("");
expect(JSON.parse(streams.stderr())).toMatchObject({
schema_version: 1,
product: PRODUCT,
command: "service-install",
status: "error",
code: "runtime_service_activation_unverified",
exit_code: 1,
});
});
it("reports success only after service registration is verified ready", async () => {
runtimeMocks.installUserService.mockResolvedValueOnce({
schema_version: 1,
platform: "linux",
scope: "user",
service_id: "one-memory.service",
status: "ready",
blocked_reason: null,
service_file: {
path: "/tmp/one-memory.service",
mode: 0o600,
content: "unit",
},
commands: { install: [], start: [], stop: [], uninstall: [] },
options: {},
});
const output = captureStdout();
let code: number;
try {
code = await run(
["service-install", ...serviceArgs, "--yes", "--data-dir", "/tmp/data"],
{},
{ waitForRuntimeReady: async () => true },
);
} finally {
output.restore();
}
expect(code!).toBe(0);
expect(JSON.parse(output.value())).toMatchObject({ apply: true });
});
it("keeps the preserved data directory and never deletes data on service-uninstall", async () => {
runtimeMocks.uninstallUserService.mockClear();
runtimeMocks.uninstallUserService.mockResolvedValueOnce("removed" as never);
const streams = captureStreams();
try {
expect(
await run(
[
"service-uninstall",
...serviceArgs,
"--data-dir",
"/tmp/keep data",
"--yes",
],
{},
),
).toBe(0);
} finally {
streams.restore();
}
const payload = JSON.parse(streams.stdout());
// The uninstall receipt must name the data directory it left untouched;
// nothing in this command is allowed to delete it.
expect(payload.service_file.content).toContain(
"ONE_MEMORY_DATA_DIR=/tmp/keep data",
);
expect(runtimeMocks.uninstallUserService).toHaveBeenCalledOnce();
});
// O-13 follow-up: `service-uninstall --yes` used to let the underlying
// error escape as a bare throw, while install/upgrade already report a
// structured envelope. A caller could not tell runtime_service_file_modified
// from a crash, and got no next step.
it("reports a refused service-uninstall as a structured error, not a throw", async () => {
runtimeMocks.uninstallUserService.mockReset();
runtimeMocks.uninstallUserService.mockRejectedValueOnce(
new Error("runtime_service_file_modified"),
);
const streams = captureStreams();
let code: number;
try {
code = await run(
[..."service-uninstall".split(" "), ...serviceArgs, "--yes"],
{},
);
} finally {
streams.restore();
}
expect(code!).toBe(1);
expect(streams.stdout()).toBe("");
const error = JSON.parse(streams.stderr());
expect(error).toMatchObject({
schema_version: 1,
product: PRODUCT,
command: "service-uninstall",
status: "error",
code: "runtime_service_file_modified",
exit_code: 1,
});
expect(error.next_step.length).toBeGreaterThan(0);
});
// P2 (2026-09-29 H13 acceptance): install/service-install/service-uninstall
// always print JSON, yet passing --json was rejected as a usage error. A
// caller that adds --json uniformly across commands must not get exit 2.
it("accepts --json on commands that always emit JSON", async () => {
// `install` needs an explicit source here because the test runner's
// argv[1] cannot be used to infer a distribution; the point of this test is
// that `--json` is not itself a usage error.
for (const [command, extra] of [
[
"install",
[
"--source",
"/tmp/distribution",
"--home",
"/tmp/selected-home",
"--install-root",
"/tmp/install",
],
],
["service-install", [...serviceArgs]],
["service-uninstall", [...serviceArgs]],
] as const) {
const streams = captureStreams();
let code: number;
try {
code = await run([command, ...extra, "--json"], {
ONE_MEMORY_DATA_DIR: "/tmp/data",
});
} finally {
streams.restore();
}
// These are previews (no --yes): exit 0 when the plan is ready.
expect(code!, `${command} --json`).not.toBe(2);
expect(() => JSON.parse(streams.stdout())).not.toThrow();
}
});
it.each(["service-plan", "service-install", "service-uninstall"])(
"rejects a repeated --json on %s",
async (command) => {
const streams = captureStreams();
try {
expect(
await run(
[command, "--install-root", "/tmp/install", "--json", "--json"],
{},
),
).toBe(2);
} finally {
streams.restore();
}
expect(streams.stdout()).toBe("");
expect(JSON.parse(streams.stderr())).toMatchObject({
status: "usage_error",
command,
exit_code: 2,
});
},
);
it.each(["install", "upgrade", "uninstall"])(
"rejects a repeated --json on %s",
async (command) => {
const streams = captureStreams();
try {
expect(
await run(
[
command,
"--source",
"/tmp/distribution",
"--install-root",
"/tmp/install",
"--home",
"/tmp/selected-home",
"--json",
"--json",
],
{},
),
).toBe(2);
} finally {
streams.restore();
}
expect(streams.stdout()).toBe("");
expect(JSON.parse(streams.stderr())).toMatchObject({
status: "usage_error",
command,
exit_code: 2,
});
},
);
});
function captureStreams() {
let out = "";
let error = "";
const originalOut = process.stdout.write;
const originalError = process.stderr.write;
process.stdout.write = ((chunk: string) => {
out += chunk;
return true;
}) as typeof process.stdout.write;
process.stderr.write = ((chunk: string) => {
error += chunk;
return true;
}) as typeof process.stderr.write;
return {
stdout: () => out,
stderr: () => error,
restore: () => {
process.stdout.write = originalOut;
process.stderr.write = originalError;
},
};
}