diff --git a/assistant/openapi.yaml b/assistant/openapi.yaml index 19274a02fbd..06c2e16fd9a 100644 --- a/assistant/openapi.yaml +++ b/assistant/openapi.yaml @@ -1317,6 +1317,77 @@ paths: required: true schema: type: string + /v1/audit: + get: + operationId: audit_get + summary: List recent tool invocations + description: Returns recent tool invocation records from the audit log, ordered by most recent first. + tags: + - audit + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: object + properties: + invocations: + type: array + items: {} + description: Tool invocation records + required: + - invocations + additionalProperties: false + parameters: + - name: limit + in: query + required: false + schema: + type: integer + description: Maximum number of entries to return (default 20) + /v1/auth/info: + get: + operationId: auth_info_get + summary: Get authentication status + description: Returns platform identity and authentication status for this assistant. + tags: + - auth + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: object + properties: + platformUrl: + anyOf: + - type: string + - type: "null" + assistantId: + anyOf: + - type: string + - type: "null" + organizationId: + anyOf: + - type: string + - type: "null" + userId: + anyOf: + - type: string + - type: "null" + authenticated: + type: boolean + message: + type: string + required: + - platformUrl + - assistantId + - organizationId + - userId + - authenticated + additionalProperties: false /v1/avatar/character-components: get: operationId: avatar_charactercomponents_get @@ -10875,6 +10946,45 @@ paths: required: true schema: type: string + /v1/oauth/apps/lookup: + get: + operationId: oauth_apps_lookup_get + summary: Get OAuth app + description: Look up a single OAuth app by ID, provider + client_id, or provider (most recent). + tags: + - oauth + responses: + "200": + description: Successful response + parameters: + - name: id + in: query + required: false + schema: + type: string + description: App UUID + - name: provider + in: query + required: false + schema: + type: string + description: Provider key + - name: client_id + in: query + required: false + schema: + type: string + description: OAuth client ID (requires provider) + /v1/oauth/apps/upsert: + post: + operationId: oauth_apps_upsert_post + summary: Upsert OAuth app + description: Create or return an existing OAuth app registration. Updates client secret if provided. + tags: + - oauth + responses: + "200": + description: Successful response /v1/oauth/connections/{id}: delete: operationId: oauth_connections_by_id_delete @@ -10908,7 +11018,31 @@ paths: schema: type: string description: Filter by managed mode support (true/false) + post: + operationId: oauth_providers_post + summary: Register OAuth provider + description: Register a new OAuth provider configuration. + tags: + - oauth + responses: + "201": + description: Successful response /v1/oauth/providers/{providerKey}: + delete: + operationId: oauth_providers_by_providerKey_delete + summary: Delete OAuth provider + description: Delete a custom OAuth provider and optionally cascade-delete its apps and connections. + tags: + - oauth + responses: + "200": + description: Successful response + parameters: + - name: providerKey + in: path + required: true + schema: + type: string get: operationId: oauth_providers_by_providerKey_get summary: Get OAuth provider @@ -10924,6 +11058,21 @@ paths: required: true schema: type: string + patch: + operationId: oauth_providers_by_providerKey_patch + summary: Update OAuth provider + description: Update an existing custom OAuth provider configuration. + tags: + - oauth + responses: + "200": + description: Successful response + parameters: + - name: providerKey + in: path + required: true + schema: + type: string /v1/oauth/start: post: operationId: oauth_start_post @@ -12176,6 +12325,172 @@ paths: - type - name additionalProperties: false + /v1/sequences/cancel-enrollment: + post: + operationId: sequences_cancelenrollment_post + summary: Cancel a specific enrollment + description: Cancel a specific enrollment, stopping all future step deliveries for that contact. + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + enrollmentId: + type: string + minLength: 1 + required: + - enrollmentId + additionalProperties: false + /v1/sequences/get: + post: + operationId: sequences_get_post + summary: Get sequence details + description: Get sequence details with enrollment stats, including step-by-step breakdown and enrollment status counts. + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + minLength: 1 + required: + - id + additionalProperties: false + /v1/sequences/guardrails: + get: + operationId: sequences_guardrails_get + summary: Show guardrail configuration + description: + "Display the current guardrail configuration: daily send cap, hourly rate, step delay, max enrollments, + duplicate check, and cooldown period." + tags: + - sequences + responses: + "200": + description: Successful response + post: + operationId: sequences_guardrails_post + summary: Update a guardrail setting + description: + "Update a single guardrail setting by key. Valid keys: dailySendCap, perSequenceHourlyRate, + minimumStepDelaySec, maxActiveEnrollments, duplicateEnrollmentCheck, cooldownPeriodMs, cooldown_days." + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + key: + type: string + minLength: 1 + value: + type: string + minLength: 1 + required: + - key + - value + additionalProperties: false + /v1/sequences/list: + post: + operationId: sequences_list_post + summary: List sequences + description: List all sequences, optionally filtered by status (active, paused, archived). + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - active + - paused + - archived + additionalProperties: false + /v1/sequences/pause: + post: + operationId: sequences_pause_post + summary: Pause a sequence + description: Pause a sequence, halting all scheduled step deliveries. No-op if already paused. + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + minLength: 1 + required: + - id + additionalProperties: false + /v1/sequences/resume: + post: + operationId: sequences_resume_post + summary: Resume a paused sequence + description: Resume a paused sequence, re-enabling scheduled step deliveries. No-op if already active. + tags: + - sequences + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + minLength: 1 + required: + - id + additionalProperties: false + /v1/sequences/stats: + get: + operationId: sequences_stats_get + summary: Overall sequence stats + description: "Returns aggregate statistics: total/active sequence counts and total/active enrollment counts." + tags: + - sequences + responses: + "200": + description: Successful response /v1/settings/avatar/generate: post: operationId: settings_avatar_generate_post @@ -14838,6 +15153,39 @@ paths: schema: type: integer description: End epoch millis (required) + /v1/user-routes/inspect: + post: + operationId: userroutes_inspect_post + summary: Inspect a user-defined route handler + description: Load a specific handler file and return its exported methods, description, file path, public URL, and metadata. + tags: + - user-routes + responses: + "200": + description: Successful response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + minLength: 1 + required: + - path + additionalProperties: false + /v1/user-routes/list: + get: + operationId: userroutes_list_get + summary: List user-defined route handlers + description: Scan workspace routes directory for handler files and return discovered routes with methods and public URLs. + tags: + - user-routes + responses: + "200": + description: Successful response /v1/watchers/create: post: operationId: watchers_create_post diff --git a/assistant/src/cli/commands/__tests__/domain-register.test.ts b/assistant/src/cli/commands/__tests__/domain-register.test.ts index c17df4c6dbc..9c0d76c8e26 100644 --- a/assistant/src/cli/commands/__tests__/domain-register.test.ts +++ b/assistant/src/cli/commands/__tests__/domain-register.test.ts @@ -1,109 +1,164 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; - -import { - getMockFetchCalls, - mockFetch, - resetMockFetch, -} from "../../../__tests__/mock-fetch.js"; -import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js"; -import { setPlatformAssistantId } from "../../../config/env.js"; -import { credentialKey } from "../../../security/credential-key.js"; -import { - _resetBackend, - deleteSecureKeyAsync, - setSecureKeyAsync, -} from "../../../security/secure-keys.js"; -import { runAssistantCommand } from "../../__tests__/run-assistant-command.js"; - -const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; -const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key"); - -beforeEach(async () => { +/** + * Tests for the domain register CLI subcommand (thin IPC wrapper). + * + * Validates: + * - register calls domain_register with subdomain param + * - register without subdomain sends empty body + * - --json outputs structured response + * - error responses are surfaced correctly + */ + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; + +import { Command } from "commander"; + +// --------------------------------------------------------------------------- +// Mock state +// --------------------------------------------------------------------------- + +let mockIpcCallFn = mock(() => + Promise.resolve({ ok: true, result: {} }), +); + +// --------------------------------------------------------------------------- +// Mocks — must be declared before importing the module under test +// --------------------------------------------------------------------------- + +mock.module("../../../ipc/cli-client.js", () => ({ + cliIpcCall: mockIpcCallFn, + exitFromIpcResult: mock((r: { error?: string }) => { + process.stderr.write((r.error ?? "Unknown error") + "\n"); + process.exitCode = 10; + }), +})); + +mock.module("../../../util/logger.js", () => ({ + getLogger: () => ({ + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), + getCliLogger: () => ({ + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), +})); + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +beforeEach(() => { + mockIpcCallFn = mock(() => Promise.resolve({ ok: true, result: {} })); process.exitCode = 0; - _resetBackend(); - resetMockFetch(); - _setOverridesForTesting({ "email-channel": true }); - setPlatformAssistantId(ASSISTANT_ID); - await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key"); }); afterEach(() => { - resetMockFetch(); - _setOverridesForTesting({}); - setPlatformAssistantId(undefined); - _resetBackend(); + process.exitCode = 0; }); +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function runDomainCommand(...args: string[]) { + mock.module("../../../ipc/cli-client.js", () => ({ + cliIpcCall: mockIpcCallFn, + exitFromIpcResult: mock((r: { error?: string }) => { + process.stderr.write((r.error ?? "Unknown error") + "\n"); + process.exitCode = 10; + }), + })); + + const { registerDomainCommand } = await import("../domain.js"); + + const stdoutChunks: string[] = []; + const origWrite = process.stdout.write.bind(process.stdout); + process.stdout.write = ((chunk: unknown) => { + stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk)); + return true; + }) as typeof process.stdout.write; + + try { + const program = new Command(); + program.exitOverride(); + program.configureOutput({ writeErr: () => {}, writeOut: () => {} }); + registerDomainCommand(program); + await program.parseAsync(["node", "assistant", ...args]); + } finally { + process.stdout.write = origWrite; + } + + return stdoutChunks.join(""); +} + +// --------------------------------------------------------------------------- +// register +// --------------------------------------------------------------------------- + describe("assistant domain register", () => { - test("successful registration with explicit subdomain", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { + test("calls domain_register with subdomain param", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { id: "550e8400-e29b-41d4-a716-446655440000", domain: "becky.vellum.me", status: "active", verified: true, created_at: "2026-04-15T19:00:00Z", }, - status: 201, - }, + }), ); - await runAssistantCommand("domain", "register", "becky"); + await runDomainCommand("domain", "register", "becky"); - const calls = getMockFetchCalls(); - expect(calls).toHaveLength(1); - expect(calls[0].path).toContain(`/v1/assistants/${ASSISTANT_ID}/domains/`); - expect(calls[0].init.method).toBe("POST"); - expect(JSON.parse(calls[0].init.body as string)).toEqual({ - subdomain: "becky", + expect(mockIpcCallFn).toHaveBeenCalledWith("domain_register", { + body: { subdomain: "becky" }, }); expect(process.exitCode).toBe(0); }); test("registration without subdomain sends empty body", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { id: "550e8400-e29b-41d4-a716-446655440000", domain: "my-assistant.vellum.me", status: "active", verified: true, created_at: "2026-04-15T19:00:00Z", }, - status: 201, - }, + }), ); - await runAssistantCommand("domain", "register"); + await runDomainCommand("domain", "register"); - const calls = getMockFetchCalls(); - expect(calls).toHaveLength(1); - expect(JSON.parse(calls[0].init.body as string)).toEqual({}); + expect(mockIpcCallFn).toHaveBeenCalledWith("domain_register", { + body: {}, + }); expect(process.exitCode).toBe(0); }); test("--json outputs structured response", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { id: "550e8400-e29b-41d4-a716-446655440000", domain: "becky.vellum.me", status: "active", verified: true, created_at: "2026-04-15T19:00:00Z", }, - status: 201, - }, + }), ); - const output = await runAssistantCommand( + const output = await runDomainCommand( "domain", "--json", "register", @@ -116,119 +171,37 @@ describe("assistant domain register", () => { expect(process.exitCode).toBe(0); }); - test("duplicate domain returns error", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { - detail: "This assistant already has a registered domain.", - }, - status: 400, - }, - ); + test("IPC error with --json outputs error envelope", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: false, + error: "This assistant already has a registered domain.", + statusCode: 400, + }), + ) as unknown as typeof mockIpcCallFn; - const output = await runAssistantCommand( + const output = await runDomainCommand( "domain", "--json", "register", "becky", ); - expect(process.exitCode).toBe(1); + expect(process.exitCode).not.toBe(0); const parsed = JSON.parse(output.trim()); expect(parsed.error).toContain("already has a registered domain"); }); - test("subdomain validation error is surfaced", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { subdomain: ["Enter a valid value."] }, - status: 400, - }, - ); - - const output = await runAssistantCommand( - "domain", - "--json", - "register", - "invalid subdomain!", - ); - - expect(process.exitCode).toBe(1); - const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("valid value"); - }); - - test("missing platform credentials returns error", async () => { - await deleteSecureKeyAsync(API_KEY_CREDENTIAL); - - const output = await runAssistantCommand( - "domain", - "--json", - "register", - "velly", - ); - - expect(process.exitCode).toBe(1); - const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("Platform credentials not configured"); - }); - - test("missing assistant ID returns error", async () => { - setPlatformAssistantId(""); - - const output = await runAssistantCommand( - "domain", - "--json", - "register", - "becky", - ); - - expect(process.exitCode).toBe(1); - const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("Assistant ID"); - }); - - test("platform 5xx returns error", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { body: { detail: "Internal server error" }, status: 500 }, - ); - - const output = await runAssistantCommand( - "domain", - "--json", - "register", - "becky", - ); - - expect(process.exitCode).toBe(1); - const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("Internal server error"); - }); - - test("unverified domain shows warning", async () => { - mockFetch( - "/domains/", - { method: "POST" }, - { - body: { - id: "550e8400-e29b-41d4-a716-446655440000", - domain: "becky.vellum.me", - status: "pending", - verified: false, - created_at: "2026-04-15T19:00:00Z", - }, - status: 201, - }, - ); - - // Just verify it doesn't crash — the warning goes to stderr/log - await runAssistantCommand("domain", "register", "becky"); - expect(process.exitCode).toBe(0); + test("IPC error without --json calls exitFromIpcResult", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: false, + error: "Platform credentials not configured", + statusCode: 401, + }), + ) as unknown as typeof mockIpcCallFn; + + await runDomainCommand("domain", "register", "velly"); + expect(process.exitCode).not.toBe(0); }); }); diff --git a/assistant/src/cli/commands/__tests__/domain-status.test.ts b/assistant/src/cli/commands/__tests__/domain-status.test.ts index 8f0cff3f9a1..dc2c820fd66 100644 --- a/assistant/src/cli/commands/__tests__/domain-status.test.ts +++ b/assistant/src/cli/commands/__tests__/domain-status.test.ts @@ -1,46 +1,110 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; - -import { - getMockFetchCalls, - mockFetch, - resetMockFetch, -} from "../../../__tests__/mock-fetch.js"; -import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js"; -import { setPlatformAssistantId } from "../../../config/env.js"; -import { credentialKey } from "../../../security/credential-key.js"; -import { - _resetBackend, - deleteSecureKeyAsync, - setSecureKeyAsync, -} from "../../../security/secure-keys.js"; -import { runAssistantCommand } from "../../__tests__/run-assistant-command.js"; - -const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; -const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key"); - -beforeEach(async () => { +/** + * Tests for the domain status CLI subcommand (thin IPC wrapper). + * + * Validates: + * - status calls domain_status + * - --json outputs structured response + * - no domain shows helpful message + * - error responses are surfaced correctly + */ + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; + +import { Command } from "commander"; + +// --------------------------------------------------------------------------- +// Mock state +// --------------------------------------------------------------------------- + +let mockIpcCallFn = mock(() => + Promise.resolve({ ok: true, result: {} }), +); + +// --------------------------------------------------------------------------- +// Mocks — must be declared before importing the module under test +// --------------------------------------------------------------------------- + +mock.module("../../../ipc/cli-client.js", () => ({ + cliIpcCall: mockIpcCallFn, + exitFromIpcResult: mock((r: { error?: string }) => { + process.stderr.write((r.error ?? "Unknown error") + "\n"); + process.exitCode = 10; + }), +})); + +mock.module("../../../util/logger.js", () => ({ + getLogger: () => ({ + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), + getCliLogger: () => ({ + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }), +})); + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +beforeEach(() => { + mockIpcCallFn = mock(() => Promise.resolve({ ok: true, result: {} })); process.exitCode = 0; - _resetBackend(); - resetMockFetch(); - _setOverridesForTesting({ "email-channel": true }); - setPlatformAssistantId(ASSISTANT_ID); - await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key"); }); afterEach(() => { - resetMockFetch(); - _setOverridesForTesting({}); - setPlatformAssistantId(undefined); - _resetBackend(); + process.exitCode = 0; }); +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function runDomainCommand(...args: string[]) { + mock.module("../../../ipc/cli-client.js", () => ({ + cliIpcCall: mockIpcCallFn, + exitFromIpcResult: mock((r: { error?: string }) => { + process.stderr.write((r.error ?? "Unknown error") + "\n"); + process.exitCode = 10; + }), + })); + + const { registerDomainCommand } = await import("../domain.js"); + + const stdoutChunks: string[] = []; + const origWrite = process.stdout.write.bind(process.stdout); + process.stdout.write = ((chunk: unknown) => { + stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk)); + return true; + }) as typeof process.stdout.write; + + try { + const program = new Command(); + program.exitOverride(); + program.configureOutput({ writeErr: () => {}, writeOut: () => {} }); + registerDomainCommand(program); + await program.parseAsync(["node", "assistant", ...args]); + } finally { + process.stdout.write = origWrite; + } + + return stdoutChunks.join(""); +} + +// --------------------------------------------------------------------------- +// status +// --------------------------------------------------------------------------- + describe("assistant domain status", () => { - test("shows domain info when registered", async () => { - mockFetch( - "/domains/", - {}, - { - body: { + test("calls domain_status", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { results: [ { id: "550e8400-e29b-41d4-a716-446655440000", @@ -51,25 +115,20 @@ describe("assistant domain status", () => { }, ], }, - status: 200, - }, + }), ); - await runAssistantCommand("domain", "status"); + await runDomainCommand("domain", "status"); - const calls = getMockFetchCalls(); - expect(calls).toHaveLength(1); - expect(calls[0].path).toContain(`/v1/assistants/${ASSISTANT_ID}/domains/`); - expect(calls[0].init.method).toBeUndefined(); + expect(mockIpcCallFn).toHaveBeenCalledWith("domain_status"); expect(process.exitCode).toBe(0); }); test("--json outputs structured response", async () => { - mockFetch( - "/domains/", - {}, - { - body: { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { results: [ { id: "550e8400-e29b-41d4-a716-446655440000", @@ -80,11 +139,10 @@ describe("assistant domain status", () => { }, ], }, - status: 200, - }, + }), ); - const output = await runAssistantCommand("domain", "--json", "status"); + const output = await runDomainCommand("domain", "--json", "status"); const parsed = JSON.parse(output.trim()); expect(parsed.results).toHaveLength(1); @@ -93,40 +151,43 @@ describe("assistant domain status", () => { }); test("no domain registered shows helpful message", async () => { - mockFetch( - "/domains/", - {}, - { - body: { results: [] }, - status: 200, - }, + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: true, + result: { results: [] }, + }), ); - await runAssistantCommand("domain", "status"); + await runDomainCommand("domain", "status"); expect(process.exitCode).toBe(0); }); - test("missing platform credentials returns error", async () => { - await deleteSecureKeyAsync(API_KEY_CREDENTIAL); + test("IPC error with --json outputs error envelope", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: false, + error: "Service unavailable", + statusCode: 503, + }), + ) as unknown as typeof mockIpcCallFn; - const output = await runAssistantCommand("domain", "--json", "status"); + const output = await runDomainCommand("domain", "--json", "status"); - expect(process.exitCode).toBe(1); + expect(process.exitCode).not.toBe(0); const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("Platform credentials not configured"); + expect(parsed.error).toContain("Service unavailable"); }); - test("platform error is surfaced", async () => { - mockFetch( - "/domains/", - {}, - { body: { detail: "Service unavailable" }, status: 503 }, - ); - - const output = await runAssistantCommand("domain", "--json", "status"); - - expect(process.exitCode).toBe(1); - const parsed = JSON.parse(output.trim()); - expect(parsed.error).toContain("Service unavailable"); + test("IPC error without --json calls exitFromIpcResult", async () => { + mockIpcCallFn = mock(() => + Promise.resolve({ + ok: false, + error: "Platform credentials not configured", + statusCode: 401, + }), + ) as unknown as typeof mockIpcCallFn; + + await runDomainCommand("domain", "status"); + expect(process.exitCode).not.toBe(0); }); });