homeio
PublicForked from doctor-io/homeio
operations.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import yaml from "js-yaml";
vi.mock("@/lib/server/modules/store/catalog", () => ({
findStoreCatalogTemplateByAppId: vi.fn(),
}));
vi.mock("@/lib/server/modules/docker/compose-parser", () => ({
parseComposeFile: vi.fn(),
extractPrimaryServiceWithName: vi.fn(),
}));
vi.mock("@/lib/server/modules/docker/compose-runner", () => ({
cleanupComposeDataOnUninstall: vi.fn(),
getComposeRuntimeInfo: vi.fn(),
materializeInlineStackFiles: vi.fn(),
runComposeDown: vi.fn(),
runComposePull: vi.fn(),
runComposeRestart: vi.fn(),
runComposeStart: vi.fn(),
runComposeStop: vi.fn(),
runComposeUp: vi.fn(),
sanitizeStackName: vi.fn(() => "adguard-home"),
}));
vi.mock("@/lib/server/modules/store/custom-apps", () => ({
findCustomStoreTemplateByAppId: vi.fn(),
isCustomStoreTemplate: vi.fn(() => false),
}));
vi.mock("@/lib/server/modules/store/docker-client", () => ({}));
vi.mock("@/lib/server/modules/apps/service", () => ({
invalidateInstalledAppsCache: vi.fn(),
}));
vi.mock("@/lib/server/modules/apps/stacks-repository", () => ({
createStoreOperation: vi.fn(),
deleteInstalledStackByAppId: vi.fn(),
findInstalledStackByAppId: vi.fn(),
findStackByWebUiPort: vi.fn(),
findStoreOperationById: vi.fn(),
markStaleOperationsAsError: vi.fn().mockResolvedValue(undefined),
pruneFinishedOperations: vi.fn().mockResolvedValue(undefined),
updateStoreOperation: vi.fn(),
updateStackUpdateStatus: vi.fn(),
upsertInstalledStack: vi.fn(),
}));
import { findStoreCatalogTemplateByAppId } from "@/lib/server/modules/store/catalog";
import {
extractPrimaryServiceWithName,
parseComposeFile,
} from "@/lib/server/modules/docker/compose-parser";
import {
cleanupComposeDataOnUninstall,
getComposeRuntimeInfo,
materializeInlineStackFiles,
runComposeDown,
runComposePull,
runComposeRestart,
runComposeUp,
} from "@/lib/server/modules/docker/compose-runner";
import { findCustomStoreTemplateByAppId } from "@/lib/server/modules/store/custom-apps";
import { invalidateInstalledAppsCache } from "@/lib/server/modules/apps/service";
import {
createStoreOperation,
deleteInstalledStackByAppId,
findInstalledStackByAppId,
findStackByWebUiPort,
updateStoreOperation,
updateStackUpdateStatus,
upsertInstalledStack,
} from "@/lib/server/modules/apps/stacks-repository";
import {
_resetActiveOperationsForTesting,
getLatestStoreOperationEvent,
startStoreOperation,
} from "@/lib/server/modules/apps/operations";
import type { StoreOperationError } from "@/lib/server/modules/apps/operations";
async function waitForLatestEventType(operationId: string, expectedType: string) {
for (let index = 0; index < 50; index += 1) {
const latest = getLatestStoreOperationEvent(operationId);
if (latest?.type === expectedType) {
return latest;
}
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
throw new Error(`Timed out waiting for event type "${expectedType}"`);
}
async function writeCatalogComposeFile(prefix: string, composeContent: string) {
const tempDir = await mkdtemp(path.join(os.tmpdir(), prefix));
const composePath = path.join(tempDir, "docker-compose.yml");
await writeFile(composePath, composeContent, "utf8");
return { tempDir, composePath };
}
describe("store operations", () => {
beforeEach(() => {
_resetActiveOperationsForTesting();
vi.mocked(findStoreCatalogTemplateByAppId).mockReset();
vi.mocked(findCustomStoreTemplateByAppId).mockReset();
vi.mocked(cleanupComposeDataOnUninstall).mockReset();
vi.mocked(parseComposeFile).mockReset();
vi.mocked(extractPrimaryServiceWithName).mockReset();
vi.mocked(getComposeRuntimeInfo).mockReset();
vi.mocked(materializeInlineStackFiles).mockReset();
vi.mocked(runComposePull).mockReset();
vi.mocked(runComposeUp).mockReset();
vi.mocked(invalidateInstalledAppsCache).mockReset();
vi.mocked(createStoreOperation).mockReset();
vi.mocked(deleteInstalledStackByAppId).mockReset();
vi.mocked(findInstalledStackByAppId).mockReset();
vi.mocked(findStackByWebUiPort).mockReset();
vi.mocked(updateStoreOperation).mockReset();
vi.mocked(updateStackUpdateStatus).mockReset();
vi.mocked(upsertInstalledStack).mockReset();
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
status: 200,
}),
);
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "running",
lifecycleStatus: "running",
containerNames: ["app"],
primaryContainerName: "app",
condition: { condition: "running", exitCode: null },
});
});
it("rejects a second active operation for the same app", async () => {
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
await startStoreOperation({
appId: "example",
action: "install",
});
await expect(
startStoreOperation({
appId: "example",
action: "update",
}),
).rejects.toMatchObject({
code: "operation_conflict",
statusCode: 409,
} satisfies Partial<StoreOperationError>);
});
it("rejects new operations when the global concurrency cap is reached", async () => {
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
await startStoreOperation({ appId: "one", action: "install" });
await startStoreOperation({ appId: "two", action: "install" });
await startStoreOperation({ appId: "three", action: "install" });
await expect(
startStoreOperation({ appId: "four", action: "install" }),
).rejects.toMatchObject({
code: "operation_limit_reached",
statusCode: 429,
} satisfies Partial<StoreOperationError>);
});
it("runs install flow and completes with progress updates", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-install-",
`services:
adguard-home:
image: ghcr.io/example/app:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "adguard-home",
templateName: "adguard-home",
name: "AdGuard Home",
description: "dns",
platform: "Docker",
note: "note",
categories: ["Network"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/bigbeartechworld/big-bear-portainer",
stackFile: "Apps/adguard-home/docker-compose.yml",
composePath,
env: [{ name: "TZ", default: "UTC" }],
screenshots: [],
image: "ghcr.io/example/app:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "adguard-home",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/adguard-home",
composePath: "/tmp/store/stacks/adguard-home/docker-compose.yml",
envPath: "/tmp/store/stacks/adguard-home/.env",
stackName: "adguard-home",
webUiPort: 3001,
});
vi.mocked(runComposePull).mockResolvedValue(undefined);
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "adguard-home",
action: "install",
webUiPort: 3001,
env: {
TZ: "Africa/Tunis",
},
});
await done;
expect(operationId).toBeTruthy();
expect(upsertInstalledStack).toHaveBeenCalledWith(
expect.objectContaining({
appId: "adguard-home",
status: "installed",
webUiPort: 3001,
}),
);
expect(materializeInlineStackFiles).toHaveBeenCalledWith(
expect.objectContaining({
appId: "adguard-home",
storageMappingStrategy: "app_target_path",
}),
);
expect(invalidateInstalledAppsCache).toHaveBeenCalledTimes(1);
expect(runComposeUp).toHaveBeenCalled();
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.status).toBe("success");
await rm(tempDir, { recursive: true, force: true });
});
it("marks operation as failed when docker pull fails", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-install-fail-",
`services:
homepage:
image: ghcr.io/example/homepage:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "homepage",
templateName: "homepage",
name: "Homepage",
description: "dashboard",
platform: "Docker",
note: "note",
categories: ["Productivity"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/bigbeartechworld/big-bear-portainer",
stackFile: "Apps/Homepage/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "ghcr.io/example/homepage:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "homepage",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/homepage",
composePath: "/tmp/store/stacks/homepage/docker-compose.yml",
envPath: "/tmp/store/stacks/homepage/.env",
stackName: "homepage",
webUiPort: null,
});
vi.mocked(runComposePull).mockRejectedValue(new Error("unauthorized"));
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "homepage",
action: "install",
});
await done;
const latest = await waitForLatestEventType(operationId, "operation.failed");
expect(latest?.status).toBe("error");
await rm(tempDir, { recursive: true, force: true });
});
it("fails install when compose runtime never reaches running", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-install-health-stopped-",
`services:
grafana:
image: grafana/grafana:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "grafana",
templateName: "grafana",
name: "Grafana",
description: "observability",
platform: "Docker",
note: "note",
categories: ["Developer"],
logoUrl: null,
port: 3000,
repositoryUrl: "https://github.com/grafana/grafana",
stackFile: "Apps/Grafana/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "grafana/grafana:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "grafana",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/grafana",
composePath: "/tmp/store/stacks/grafana/docker-compose.yml",
envPath: "/tmp/store/stacks/grafana/.env",
stackName: "grafana",
webUiPort: 3000,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "stopped",
lifecycleStatus: "stopped",
containerNames: ["grafana"],
primaryContainerName: "grafana",
condition: { condition: "stopped", exitCode: null },
});
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "grafana",
action: "install",
});
await done;
expect(upsertInstalledStack).not.toHaveBeenCalled();
const latest = await waitForLatestEventType(operationId, "operation.failed");
expect(latest?.message).toBe(
"Deployment failed because containers never reached a running state",
);
await rm(tempDir, { recursive: true, force: true });
});
it("fails install when compose runtime remains unknown", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-install-health-unknown-",
`services:
kuma:
image: louislam/uptime-kuma:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "uptime-kuma",
templateName: "uptime-kuma",
name: "Uptime Kuma",
description: "monitoring",
platform: "Docker",
note: "note",
categories: ["Monitoring"],
logoUrl: null,
port: 3001,
repositoryUrl: "https://github.com/louislam/uptime-kuma",
stackFile: "Apps/Uptime-Kuma/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "louislam/uptime-kuma:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "kuma",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/uptime-kuma",
composePath: "/tmp/store/stacks/uptime-kuma/docker-compose.yml",
envPath: "/tmp/store/stacks/uptime-kuma/.env",
stackName: "uptime-kuma",
webUiPort: 3001,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "unknown",
lifecycleStatus: "unknown",
containerNames: [],
primaryContainerName: null,
condition: { condition: "unknown", exitCode: null },
});
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "uptime-kuma",
action: "install",
});
await done;
expect(upsertInstalledStack).not.toHaveBeenCalled();
const latest = await waitForLatestEventType(operationId, "operation.failed");
expect(latest?.message).toBe(
"Deployment failed because runtime status could not be confirmed",
);
await rm(tempDir, { recursive: true, force: true });
});
it("keeps install successful when containers are stable but the Web UI probe fails", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-install-health-webui-",
`services:
home-assistant:
image: ghcr.io/home-assistant/home-assistant:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "home-assistant",
templateName: "home-assistant",
name: "Home Assistant",
description: "automation",
platform: "Docker",
note: "note",
categories: ["Automation"],
logoUrl: null,
port: 8123,
repositoryUrl: "https://github.com/home-assistant/core",
stackFile: "Apps/Home-Assistant/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "ghcr.io/home-assistant/home-assistant:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "home-assistant",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/home-assistant",
composePath: "/tmp/store/stacks/home-assistant/docker-compose.yml",
envPath: "/tmp/store/stacks/home-assistant/.env",
stackName: "home-assistant",
webUiPort: 8123,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
vi.stubGlobal(
"fetch",
vi.fn().mockRejectedValue(new TypeError("Failed to fetch")),
);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "home-assistant",
action: "install",
});
await done;
expect(upsertInstalledStack).toHaveBeenCalledWith(
expect.objectContaining({
appId: "home-assistant",
}),
);
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.status).toBe("success");
expect(updateStoreOperation).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
currentStep: "health-check",
}),
);
await rm(tempDir, { recursive: true, force: true });
});
it("hard-deletes stack record on uninstall and removes data when requested", async () => {
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "2fauth",
templateName: "2fauth",
stackName: "big-bear-2fauth",
composePath: "/tmp/store/stacks/2fauth/docker-compose.yml",
status: "installed",
webUiPort: 8000,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(runComposeDown).mockResolvedValue(undefined);
vi.mocked(cleanupComposeDataOnUninstall).mockResolvedValue(undefined);
vi.mocked(deleteInstalledStackByAppId).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "2fauth",
action: "uninstall",
removeVolumes: true,
});
await done;
expect(runComposeDown).toHaveBeenCalledWith({
composePath: "/tmp/store/stacks/2fauth/docker-compose.yml",
envPath: "/tmp/store/stacks/2fauth/.env",
stackName: "big-bear-2fauth",
removeVolumes: true,
});
expect(cleanupComposeDataOnUninstall).toHaveBeenCalledWith({
composePath: "/tmp/store/stacks/2fauth/docker-compose.yml",
removeVolumes: true,
});
expect(deleteInstalledStackByAppId).toHaveBeenCalledWith("2fauth");
expect(invalidateInstalledAppsCache).toHaveBeenCalledTimes(1);
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.status).toBe("success");
});
it("always cleans up installed stack files during uninstall even when data deletion is disabled", async () => {
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "pingvin-share",
templateName: "pingvin-share",
stackName: "pingvin-share",
composePath: "/var/lib/home-server/Apps/pingvin-share/docker-compose.yml",
status: "installed",
webUiPort: 3410,
env: {},
installedAt: "2026-03-07T00:00:00.000Z",
updatedAt: "2026-03-07T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(runComposeDown).mockResolvedValue(undefined);
vi.mocked(cleanupComposeDataOnUninstall).mockResolvedValue(undefined);
vi.mocked(deleteInstalledStackByAppId).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
await startStoreOperation({
appId: "pingvin-share",
action: "uninstall",
removeVolumes: false,
});
await done;
expect(runComposeDown).toHaveBeenCalledWith({
composePath: "/var/lib/home-server/Apps/pingvin-share/docker-compose.yml",
envPath: "/var/lib/home-server/Apps/pingvin-share/.env",
stackName: "pingvin-share",
removeVolumes: false,
});
expect(cleanupComposeDataOnUninstall).toHaveBeenCalledWith({
composePath: "/var/lib/home-server/Apps/pingvin-share/docker-compose.yml",
removeVolumes: false,
});
expect(deleteInstalledStackByAppId).toHaveBeenCalledWith("pingvin-share");
});
it("uses legacy storage mapping on redeploy for existing apps", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-redeploy-",
`services:
home-assistant:
image: ghcr.io/example/home-assistant:latest
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "home-assistant",
templateName: "home-assistant",
name: "Home Assistant",
description: "automation",
platform: "Docker",
note: "note",
categories: ["Automation"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/bigbeartechworld/big-bear-portainer",
stackFile: "Apps/home-assistant/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "ghcr.io/example/home-assistant:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "home-assistant",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "home-assistant",
templateName: "home-assistant",
stackName: "home-assistant",
composePath: "/tmp/store/stacks/home-assistant/docker-compose.yml",
status: "installed",
webUiPort: null,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/home-assistant",
composePath: "/tmp/store/stacks/home-assistant/docker-compose.yml",
envPath: "/tmp/store/stacks/home-assistant/.env",
stackName: "home-assistant",
webUiPort: null,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
await startStoreOperation({
appId: "home-assistant",
action: "redeploy",
});
await done;
expect(materializeInlineStackFiles).toHaveBeenCalledWith(
expect.objectContaining({
appId: "home-assistant",
storageMappingStrategy: "legacy_named_source",
}),
);
expect(invalidateInstalledAppsCache).toHaveBeenCalledTimes(1);
await rm(tempDir, { recursive: true, force: true });
});
it("uses composeSource for install materialization and bypasses template env whitelist", async () => {
const composeSource = `services:\n immich-server:\n image: ghcr.io/immich-app/immich-server:v2.5.6\n ports:\n - \"2283:2283\"\n environment:\n DB_HOSTNAME: immich-postgres\n CUSTOM_ENV: yes\n`;
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "immich",
templateName: "immich",
name: "Immich",
description: "photos",
platform: "Docker",
note: "note",
categories: ["Photos"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/bigbeartechworld/big-bear-portainer",
stackFile: "Apps/immich/docker-compose.yml",
composePath: "/tmp/store/immich/docker-compose.yml",
env: [{ name: "TZ", default: "UTC" }],
screenshots: [],
image: "ghcr.io/immich-app/immich-server:v2.5.6",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "immich-server",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(parseComposeFile).mockReturnValue({
services: {
"immich-server": {
image: "ghcr.io/immich-app/immich-server:v2.5.6",
ports: ["2283:2283"],
environment: {
DB_HOSTNAME: "immich-postgres",
CUSTOM_ENV: "yes",
},
},
},
});
vi.mocked(extractPrimaryServiceWithName).mockReturnValue({
name: "immich-server",
service: {
image: "ghcr.io/immich-app/immich-server:v2.5.6",
ports: ["2283:2283"],
environment: {
DB_HOSTNAME: "immich-postgres",
CUSTOM_ENV: "yes",
},
},
});
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/immich",
composePath: "/tmp/store/stacks/immich/docker-compose.yml",
envPath: "/tmp/store/stacks/immich/.env",
stackName: "immich",
webUiPort: 2283,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
await startStoreOperation({
appId: "immich",
action: "install",
composeSource,
});
await done;
expect(materializeInlineStackFiles).toHaveBeenCalledWith(
expect.objectContaining({
appId: "immich",
composeContent: composeSource,
}),
);
expect(upsertInstalledStack).toHaveBeenCalledWith(
expect.objectContaining({
appId: "immich",
env: expect.objectContaining({
AppID: "immich",
DB_HOSTNAME: "immich-postgres",
CUSTOM_ENV: "yes",
}),
}),
);
});
it("injects AppID into compose interpolation env during install", async () => {
const composeSource = `services:\n home-assistant:\n image: ghcr.io/home-assistant/home-assistant:latest\n volumes:\n - /DATA/AppData/$AppID/config:/config\n`;
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "home-assistant",
templateName: "home-assistant",
name: "Home Assistant",
description: "automation",
platform: "Docker",
note: "note",
categories: ["Automation"],
logoUrl: null,
port: 8123,
repositoryUrl: "https://github.com/home-assistant/core",
stackFile: "Apps/home-assistant/docker-compose.yml",
composePath: "/tmp/store/home-assistant/docker-compose.yml",
env: [],
screenshots: [],
image: "ghcr.io/home-assistant/home-assistant:latest",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "home-assistant",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue(null);
vi.mocked(findStackByWebUiPort).mockResolvedValue(null);
vi.mocked(parseComposeFile).mockReturnValue({
services: {
"home-assistant": {
image: "ghcr.io/home-assistant/home-assistant:latest",
volumes: ["/DATA/AppData/$AppID/config:/config"],
},
},
});
vi.mocked(extractPrimaryServiceWithName).mockReturnValue({
name: "home-assistant",
service: {
image: "ghcr.io/home-assistant/home-assistant:latest",
volumes: ["/DATA/AppData/$AppID/config:/config"],
},
});
vi.mocked(materializeInlineStackFiles).mockResolvedValue({
stackDir: "/tmp/store/stacks/home-assistant",
composePath: "/tmp/store/stacks/home-assistant/docker-compose.yml",
envPath: "/tmp/store/stacks/home-assistant/.env",
stackName: "home-assistant",
webUiPort: 8123,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
await startStoreOperation({
appId: "home-assistant",
action: "install",
composeSource,
});
await done;
expect(materializeInlineStackFiles).toHaveBeenCalledWith(
expect.objectContaining({
appId: "home-assistant",
env: expect.objectContaining({
AppID: "home-assistant",
}),
}),
);
});
it("updates only service images and preserves other compose fields", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-"));
const composePath = path.join(tempDir, "docker-compose.yml");
const storeComposePath = path.join(tempDir, "store-compose.yml");
const initialCompose = `services:
app:
image: ghcr.io/example/app:v1
environment:
TZ: UTC
APP_URL: http://localhost:3100
ports:
- "3100:3100"
redis:
image: redis:6
`;
await writeFile(composePath, initialCompose, "utf8");
await writeFile(
storeComposePath,
`services:
app:
image: ghcr.io/example/app:v2
redis:
image: redis:7
`,
"utf8",
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath,
status: "installed",
webUiPort: 3100,
env: {
TZ: "UTC",
},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(upsertInstalledStack).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const updatedCompose = await readFile(composePath, "utf8");
const parsed = yaml.load(updatedCompose) as {
services: Record<string, { image?: string; environment?: Record<string, string>; ports?: string[] }>;
};
expect(parsed.services.app.image).toBe("ghcr.io/example/app:v2");
expect(parsed.services.redis.image).toBe("redis:7");
expect(parsed.services.app.environment).toEqual({
TZ: "UTC",
APP_URL: "http://localhost:3100",
});
expect(parsed.services.app.ports).toEqual(["3100:3100"]);
expect(runComposeUp).toHaveBeenCalledTimes(1);
await rm(tempDir, { recursive: true, force: true });
});
it("treats update as no-op when installed images are already current", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-noop-"));
const composePath = path.join(tempDir, "docker-compose.yml");
const storeComposePath = path.join(tempDir, "store-compose.yml");
const initialCompose = `services:\n app:\n image: ghcr.io/example/app:v2\n environment:\n TZ: UTC\n`;
await writeFile(composePath, initialCompose, "utf8");
await writeFile(storeComposePath, `services:\n app:\n image: ghcr.io/example/app:v2\n`, "utf8");
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath,
status: "installed",
webUiPort: 3100,
env: {
TZ: "UTC",
},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let finished = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!finished && patch.status === "success") {
finished = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.status).toBe("success");
expect(runComposeUp).not.toHaveBeenCalled();
expect(runComposePull).not.toHaveBeenCalled();
const composeAfterNoop = await readFile(composePath, "utf8");
expect(composeAfterNoop).toBe(initialCompose);
await rm(tempDir, { recursive: true, force: true });
});
it("fails update with explicit error when installed compose is missing", async () => {
const { tempDir, composePath } = await writeCatalogComposeFile(
"store-update-missing-",
`services:
app:
image: ghcr.io/example/app:v2
`,
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath: "/tmp/missing-compose/docker-compose.yml",
status: "installed",
webUiPort: 3100,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
const { operationId } = await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const latest = await waitForLatestEventType(operationId, "operation.failed");
expect(latest?.message).toBe("Installed compose source is unavailable");
expect(runComposeUp).not.toHaveBeenCalled();
await rm(tempDir, { recursive: true, force: true });
});
it("resolves moved installed compose path before update", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-resolved-compose-"));
const fallbackDir = path.join(tempDir, "missing-compose", "example");
await mkdir(fallbackDir, { recursive: true });
await writeFile(
path.join(fallbackDir, "docker-compose.yml"),
"services:\n app:\n image: ghcr.io/example/app:v1\n",
"utf8",
);
const storeComposePath = path.join(tempDir, "store-compose.yml");
await writeFile(
storeComposePath,
"services:\n app:\n image: ghcr.io/example/app:v2\n",
"utf8",
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath: path.join(tempDir, "missing-compose", "docker-compose.yml"),
status: "installed",
webUiPort: 3100,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
vi.mocked(runComposeUp).mockResolvedValue();
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "running",
primaryContainerName: "example",
condition: { condition: "running", exitCode: null },
});
const { operationId } = await startStoreOperation({
appId: "example",
action: "update",
});
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.type).toBe("operation.completed");
expect(runComposeUp).toHaveBeenCalledWith(
expect.objectContaining({
composePath: path.join(fallbackDir, "docker-compose.yml"),
}),
);
await rm(tempDir, { recursive: true, force: true });
});
it("fails update when installed and store services do not match", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-mismatch-"));
const composePath = path.join(tempDir, "docker-compose.yml");
const storeComposePath = path.join(tempDir, "store-compose.yml");
const initialCompose = `services:\n app:\n image: ghcr.io/example/app:v1\n`;
await writeFile(composePath, initialCompose, "utf8");
await writeFile(
storeComposePath,
`services:
web:
image: ghcr.io/example/app:v2
`,
"utf8",
);
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "web",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath,
status: "installed",
webUiPort: 3100,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const composeAfterFailure = await readFile(composePath, "utf8");
expect(composeAfterFailure).toBe(initialCompose);
expect(runComposeUp).not.toHaveBeenCalled();
await rm(tempDir, { recursive: true, force: true });
});
it("restores backup compose when update apply fails", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-rollback-"));
const composePath = path.join(tempDir, "docker-compose.yml");
const storeComposePath = path.join(tempDir, "store-compose.yml");
const initialCompose = `services:\n app:\n image: ghcr.io/example/app:v1\n`;
await writeFile(composePath, initialCompose, "utf8");
await writeFile(storeComposePath, `services:\n app:\n image: ghcr.io/example/app:v2\n`, "utf8");
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: null,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath,
status: "installed",
webUiPort: 3100,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(runComposeUp)
.mockRejectedValueOnce(new Error("compose up failed"))
.mockResolvedValueOnce(undefined);
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const composeAfterRollback = await readFile(composePath, "utf8");
expect(composeAfterRollback).toBe(initialCompose);
expect(runComposeUp).toHaveBeenCalledTimes(2);
await rm(tempDir, { recursive: true, force: true });
});
it("restores backup compose when update health-check fails after compose up", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "store-update-health-rollback-"));
const composePath = path.join(tempDir, "docker-compose.yml");
const storeComposePath = path.join(tempDir, "store-compose.yml");
const initialCompose = `services:\n app:\n image: ghcr.io/example/app:v1\n`;
await writeFile(composePath, initialCompose, "utf8");
await writeFile(storeComposePath, `services:\n app:\n image: ghcr.io/example/app:v2\n`, "utf8");
vi.mocked(findStoreCatalogTemplateByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
name: "Example",
description: "example",
platform: "Docker",
note: "note",
categories: ["Tools"],
logoUrl: null,
port: 3100,
repositoryUrl: "https://github.com/example/store",
stackFile: "Apps/example/docker-compose.yml",
composePath: storeComposePath,
env: [],
screenshots: [],
image: "ghcr.io/example/app:v2",
volumes: [],
scheme: "http",
index: "/",
mainServiceName: "app",
});
vi.mocked(findCustomStoreTemplateByAppId).mockResolvedValue(null);
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "example",
templateName: "example",
stackName: "example",
composePath,
status: "installed",
webUiPort: 3100,
env: {},
installedAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:00.000Z",
});
vi.mocked(runComposeUp).mockResolvedValue(undefined);
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "stopped",
lifecycleStatus: "restarting",
containerNames: ["example-app-1"],
primaryContainerName: "example-app-1",
condition: { condition: "stopped", exitCode: null },
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStackUpdateStatus).mockResolvedValue(undefined);
let failed = false;
const done = new Promise<void>((resolve) => {
vi.mocked(updateStoreOperation).mockImplementation(async (_id, patch) => {
if (!failed && patch.status === "error") {
failed = true;
resolve();
}
});
});
await startStoreOperation({
appId: "example",
action: "update",
});
await done;
const composeAfterRollback = await readFile(composePath, "utf8");
expect(composeAfterRollback).toBe(initialCompose);
expect(runComposeUp).toHaveBeenCalledTimes(2);
await rm(tempDir, { recursive: true, force: true });
});
it("purges latestOperationEvent after the terminal-event grace window", async () => {
vi.mocked(findInstalledStackByAppId).mockResolvedValue({
appId: "purge-me",
templateName: "purge-me",
stackName: "purge-me",
composePath: "/tmp/store/stacks/purge-me/docker-compose.yml",
status: "installed",
webUiPort: 4001,
env: {},
installedAt: new Date("2026-02-24T00:00:00.000Z"),
updatedAt: new Date("2026-02-24T00:00:00.000Z"),
isUpToDate: true,
});
vi.mocked(createStoreOperation).mockResolvedValue(undefined);
vi.mocked(updateStoreOperation).mockResolvedValue(undefined);
vi.mocked(getComposeRuntimeInfo).mockResolvedValue({
status: "running",
lifecycleStatus: "running",
containerNames: ["purge"],
primaryContainerName: "purge",
condition: { condition: "running", exitCode: null },
});
vi.mocked(runComposeRestart).mockResolvedValue(undefined);
const { operationId } = await startStoreOperation({
appId: "purge-me",
action: "restart",
});
const latest = await waitForLatestEventType(operationId, "operation.completed");
expect(latest?.status).toBe("success");
// Wait past the test TTL (1s) plus a margin so the unref'd timer fires.
await new Promise((resolve) => setTimeout(resolve, 1_200));
expect(getLatestStoreOperationEvent(operationId)).toBeNull();
});
});