diff --git a/packages/boxel-cli/src/commands/realm/archive.ts b/packages/boxel-cli/src/commands/realm/archive.ts new file mode 100644 index 0000000000..e7e2602de1 --- /dev/null +++ b/packages/boxel-cli/src/commands/realm/archive.ts @@ -0,0 +1,140 @@ +import type { Command } from 'commander'; +import { ensureTrailingSlash } from '@cardstack/runtime-common/paths'; +import { + getProfileManager, + NO_ACTIVE_PROFILE_ERROR, + type ProfileManager, +} from '../../lib/profile-manager.ts'; +import { prompt } from '../../lib/prompt.ts'; +import { DIM, FG_CYAN, FG_GREEN, FG_RED, RESET } from '../../lib/colors.ts'; + +export interface ArchiveRealmOptions { + realmUrl: string; + profileManager?: ProfileManager; +} + +export interface ArchiveRealmResult { + /** Normalized URL the operation targeted (always trailing-slashed). */ + realmUrl: string; + /** True when POST /_archive-realm returned 200. */ + archived: boolean; + error?: string; +} + +/** + * Archive a realm via `POST /_archive-realm`. Owner-only; the server + * returns 403 when the caller is not an owner. Programmatic API: returns + * a result object on every path, never prompts, never calls + * `process.exit`. The CLI wraps this with a TTY confirmation step (see + * `registerArchiveCommand`). + */ +export async function archiveRealm( + options: ArchiveRealmOptions, +): Promise { + let realmUrl = ensureTrailingSlash(options.realmUrl.trim()); + let pm = options.profileManager ?? getProfileManager(); + let active = pm.getActiveProfile(); + if (!active) { + return { + realmUrl, + archived: false, + error: NO_ACTIVE_PROFILE_ERROR, + }; + } + + let realmServerUrl = active.profile.realmServerUrl.replace(/\/$/, ''); + let response: Response; + try { + response = await pm.authedRealmServerFetch( + `${realmServerUrl}/_archive-realm`, + { + method: 'POST', + headers: { 'Content-Type': 'application/vnd.api+json' }, + body: JSON.stringify({ + data: { type: 'realm', id: realmUrl }, + }), + }, + ); + } catch (err) { + return { + realmUrl, + archived: false, + error: `Failed to reach realm server: ${ + err instanceof Error ? err.message : String(err) + }`, + }; + } + + if (!response.ok) { + let body = await safeReadResponseText(response); + let error = + response.status === 403 + ? `You do not own this realm and cannot archive it. Server returned 403: ${body}` + : `Realm server returned ${response.status}: ${body}`; + return { + realmUrl, + archived: false, + error, + }; + } + + return { + realmUrl, + archived: true, + }; +} + +async function safeReadResponseText(response: Response): Promise { + try { + return await response.text(); + } catch { + return ''; + } +} + +interface ArchiveCliOptions { + yes?: boolean; +} + +export function registerArchiveCommand(realm: Command): void { + realm + .command('archive') + .description( + 'Archive a realm — hides it from enumeration and stops its indexer (owner-only)', + ) + .argument('', 'realm URL to archive') + .option('-y, --yes', 'Skip the interactive confirmation prompt') + .action(async (realmUrlInput: string, opts: ArchiveCliOptions) => { + let normalized = ensureTrailingSlash(realmUrlInput.trim()); + + console.log(`Archive target: ${FG_CYAN}${normalized}${RESET}`); + + if (!opts.yes) { + if (!process.stdin.isTTY) { + console.error( + `${FG_RED}Error:${RESET} stdin is not a TTY. Pass --yes to confirm in non-interactive mode.`, + ); + process.exit(1); + } + let answer = await prompt( + 'This will archive the realm: it will be hidden from your realm list, sealed for content, and its indexer will stop. You can restore it later with `boxel realm restore`. Proceed? (y/N) ', + ); + if (!/^y/i.test(answer)) { + console.log(`${DIM}Cancelled.${RESET}`); + return; + } + } + + let result = await archiveRealm({ realmUrl: normalized }); + if (result.error || !result.archived) { + console.error( + `${FG_RED}Error:${RESET} ${result.error ?? 'Archive did not complete.'}`, + ); + process.exit(1); + } + + console.log( + `${FG_GREEN}Archived:${RESET} ${FG_CYAN}${result.realmUrl}${RESET}`, + ); + }); +} diff --git a/packages/boxel-cli/src/commands/realm/index.ts b/packages/boxel-cli/src/commands/realm/index.ts index 2cb2b681c5..9ba00ed110 100644 --- a/packages/boxel-cli/src/commands/realm/index.ts +++ b/packages/boxel-cli/src/commands/realm/index.ts @@ -1,4 +1,5 @@ import type { Command } from 'commander'; +import { registerArchiveCommand } from './archive.ts'; import { registerCancelIndexingCommand } from './cancel-indexing.ts'; import { registerCreateCommand } from './create.ts'; import { registerHistoryCommand } from './history.ts'; @@ -10,6 +11,7 @@ import { registerPublishCommand } from './publish.ts'; import { registerPullCommand } from './pull.ts'; import { registerPushCommand } from './push.ts'; import { registerRemoveCommand } from './remove.ts'; +import { registerRestoreCommand } from './restore.ts'; import { registerStatusCommand } from './status.ts'; import { registerSyncCommand } from './sync.ts'; import { registerUnpublishCommand } from './unpublish.ts'; @@ -21,6 +23,7 @@ export function registerRealmCommand(program: Command): void { .command('realm') .description('Manage realms on the realm server'); + registerArchiveCommand(realm); registerCancelIndexingCommand(realm); registerCreateCommand(realm); registerHistoryCommand(realm); @@ -32,6 +35,7 @@ export function registerRealmCommand(program: Command): void { registerPullCommand(realm); registerPushCommand(realm); registerRemoveCommand(realm); + registerRestoreCommand(realm); const sync = registerSyncCommand(realm); registerStatusCommand(sync); registerUnpublishCommand(realm); diff --git a/packages/boxel-cli/src/commands/realm/list.ts b/packages/boxel-cli/src/commands/realm/list.ts index b47dc9cbe9..bae81a8b2d 100644 --- a/packages/boxel-cli/src/commands/realm/list.ts +++ b/packages/boxel-cli/src/commands/realm/list.ts @@ -13,6 +13,7 @@ const MUTUALLY_EXCLUSIVE_FLAGS_ERROR = export interface RealmSummary { url: string; hidden: boolean; + archived: boolean; } export interface ListRealmsResult { @@ -23,6 +24,7 @@ export interface ListRealmsResult { export interface ListRealmsOptions { allAccessible?: boolean; hidden?: boolean; + includeArchived?: boolean; profileManager?: ProfileManager; } @@ -30,17 +32,24 @@ interface ListCliOptions { json?: boolean; allAccessible?: boolean; hidden?: boolean; + includeArchived?: boolean; } /** * List realms accessible to the active profile. * - * Calls `_realm-auth` to discover all realms the user can access, then - * marks each as `hidden` based on whether it appears in the user's + * Calls `_realm-auth` to discover the user's accessible non-archived + * realms, then marks each as `hidden` based on whether it appears in the * `app.boxel.realms` Matrix account data (the UI realm list). * - * Default mode shows only non-hidden realms; `--all-accessible` shows - * everything; `--hidden` shows only hidden ones. + * Archived realms are hidden by default (matching the workspace + * chooser). With `--include-archived`, the owner-only `_archived-realms` + * endpoint is consulted and the caller's archived realms are appended + * with `archived: true`. + * + * Default mode shows only non-hidden, non-archived realms; + * `--all-accessible` shows everything accessible; `--hidden` shows only + * hidden non-archived ones. */ export async function listRealms( options: ListRealmsOptions = {}, @@ -92,6 +101,7 @@ export async function listRealms( let summaries: RealmSummary[] = accessibleUrls.map((url) => ({ url, hidden: !userRealmsSet.has(url), + archived: false, })); if (options.allAccessible) { @@ -102,6 +112,38 @@ export async function listRealms( summaries = summaries.filter((r) => !r.hidden); } + if (options.includeArchived) { + let archivedResponse = await pm.authedRealmServerFetch( + `${realmServerUrl}/_archived-realms`, + { + method: 'GET', + headers: { Accept: 'application/vnd.api+json' }, + }, + ); + if (!archivedResponse.ok) { + let text = await archivedResponse.text(); + return { + realms: [], + error: `Archived realms lookup failed: ${archivedResponse.status} ${text}`, + }; + } + let archivedBody = (await archivedResponse.json()) as { + data?: Array<{ id?: string }>; + }; + let archivedUrls = (archivedBody.data ?? []) + .map((entry) => (entry?.id ? ensureTrailingSlash(entry.id) : null)) + .filter((u): u is string => u !== null); + + let alreadyListed = new Set(summaries.map((r) => r.url)); + for (let url of archivedUrls) { + if (alreadyListed.has(url)) { + continue; + } + summaries.push({ url, hidden: !userRealmsSet.has(url), archived: true }); + alreadyListed.add(url); + } + } + summaries.sort((a, b) => a.url.localeCompare(b.url)); return { realms: summaries }; } @@ -117,12 +159,17 @@ export function registerListCommand(realm: Command): void { 'Show all accessible realms, including hidden ones', ) .option('--hidden', "Show only realms not in the user's UI realm list") + .option( + '--include-archived', + 'Also list realms the caller owns that have been archived', + ) .action(async (opts: ListCliOptions) => { let result: ListRealmsResult; try { result = await listRealms({ allAccessible: opts.allAccessible, hidden: opts.hidden, + includeArchived: opts.includeArchived, }); } catch (err) { console.error( @@ -149,7 +196,10 @@ export function registerListCommand(realm: Command): void { console.log(`${BOLD}${result.realms.length} realm(s):${RESET}`); for (let r of result.realms) { - let tag = r.hidden ? ` ${DIM}(hidden)${RESET}` : ''; + let tags: string[] = []; + if (r.archived) tags.push('archived'); + if (r.hidden && !r.archived) tags.push('hidden'); + let tag = tags.length ? ` ${DIM}(${tags.join(', ')})${RESET}` : ''; console.log(` ${FG_CYAN}${r.url}${RESET}${tag}`); } }); diff --git a/packages/boxel-cli/src/commands/realm/restore.ts b/packages/boxel-cli/src/commands/realm/restore.ts new file mode 100644 index 0000000000..2e754b13f4 --- /dev/null +++ b/packages/boxel-cli/src/commands/realm/restore.ts @@ -0,0 +1,114 @@ +import type { Command } from 'commander'; +import { ensureTrailingSlash } from '@cardstack/runtime-common/paths'; +import { + getProfileManager, + NO_ACTIVE_PROFILE_ERROR, + type ProfileManager, +} from '../../lib/profile-manager.ts'; +import { FG_CYAN, FG_GREEN, FG_RED, RESET } from '../../lib/colors.ts'; + +export interface RestoreRealmOptions { + realmUrl: string; + profileManager?: ProfileManager; +} + +export interface RestoreRealmResult { + /** Normalized URL the operation targeted (always trailing-slashed). */ + realmUrl: string; + /** True when POST /_unarchive-realm returned 200. */ + restored: boolean; + error?: string; +} + +/** + * Restore a previously archived realm via `POST /_unarchive-realm`. + * Owner-only; the server returns 403 when the caller is not an owner. + * Server-side this also enqueues a full reindex. + */ +export async function restoreRealm( + options: RestoreRealmOptions, +): Promise { + let realmUrl = ensureTrailingSlash(options.realmUrl.trim()); + let pm = options.profileManager ?? getProfileManager(); + let active = pm.getActiveProfile(); + if (!active) { + return { + realmUrl, + restored: false, + error: NO_ACTIVE_PROFILE_ERROR, + }; + } + + let realmServerUrl = active.profile.realmServerUrl.replace(/\/$/, ''); + let response: Response; + try { + response = await pm.authedRealmServerFetch( + `${realmServerUrl}/_unarchive-realm`, + { + method: 'POST', + headers: { 'Content-Type': 'application/vnd.api+json' }, + body: JSON.stringify({ + data: { type: 'realm', id: realmUrl }, + }), + }, + ); + } catch (err) { + return { + realmUrl, + restored: false, + error: `Failed to reach realm server: ${ + err instanceof Error ? err.message : String(err) + }`, + }; + } + + if (!response.ok) { + let body = await safeReadResponseText(response); + let error = + response.status === 403 + ? `You do not own this realm and cannot restore it. Server returned 403: ${body}` + : `Realm server returned ${response.status}: ${body}`; + return { + realmUrl, + restored: false, + error, + }; + } + + return { + realmUrl, + restored: true, + }; +} + +async function safeReadResponseText(response: Response): Promise { + try { + return await response.text(); + } catch { + return ''; + } +} + +export function registerRestoreCommand(realm: Command): void { + realm + .command('restore') + .description( + 'Restore a previously archived realm and trigger a full reindex (owner-only)', + ) + .argument('', 'realm URL to restore') + .action(async (realmUrlInput: string) => { + let normalized = ensureTrailingSlash(realmUrlInput.trim()); + + let result = await restoreRealm({ realmUrl: normalized }); + if (result.error || !result.restored) { + console.error( + `${FG_RED}Error:${RESET} ${result.error ?? 'Restore did not complete.'}`, + ); + process.exit(1); + } + + console.log( + `${FG_GREEN}Restored:${RESET} ${FG_CYAN}${result.realmUrl}${RESET}`, + ); + }); +} diff --git a/packages/boxel-cli/tests/integration/realm-archive.test.ts b/packages/boxel-cli/tests/integration/realm-archive.test.ts new file mode 100644 index 0000000000..8a44b6368d --- /dev/null +++ b/packages/boxel-cli/tests/integration/realm-archive.test.ts @@ -0,0 +1,197 @@ +import '../helpers/setup-realm-server.ts'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { createRealm } from '../../src/commands/realm/create.ts'; +import { archiveRealm } from '../../src/commands/realm/archive.ts'; +import { restoreRealm } from '../../src/commands/realm/restore.ts'; +import { ProfileManager } from '../../src/lib/profile-manager.ts'; +import { + startTestRealmServer, + stopTestRealmServer, + createTestProfileDir, + setupTestProfile, + uniqueRealmName, + registerUser, + matrixURL, + matrixRegistrationSecret, + TEST_REALM_SERVER_URL, +} from '../helpers/integration.ts'; + +let profileManager: ProfileManager; +let cleanupProfile: () => void; + +beforeAll(async () => { + await startTestRealmServer(); + let testProfile = createTestProfileDir(); + profileManager = testProfile.profileManager; + cleanupProfile = testProfile.cleanup; + await setupTestProfile(profileManager); +}); + +afterAll(async () => { + cleanupProfile?.(); + await stopTestRealmServer(); +}); + +describe('realm archive (integration)', () => { + it('archives a realm for the owner', async () => { + let name = uniqueRealmName(); + let { realmUrl } = await createRealm(name, `Test ${name}`, { + profileManager, + }); + + let result = await archiveRealm({ realmUrl, profileManager }); + + expect(result.error).toBeUndefined(); + expect(result.archived).toBe(true); + expect(result.realmUrl).toBe(realmUrl); + }); + + it('normalizes a trailing-slash-less input', async () => { + let name = uniqueRealmName(); + let { realmUrl } = await createRealm(name, `Test ${name}`, { + profileManager, + }); + + let withoutSlash = realmUrl.replace(/\/$/, ''); + let result = await archiveRealm({ + realmUrl: withoutSlash, + profileManager, + }); + + expect(result.error).toBeUndefined(); + expect(result.archived).toBe(true); + expect(result.realmUrl).toBe(realmUrl); + }); + + it('returns a 403 error when the caller does not own the realm', async () => { + let realmName = uniqueRealmName(); + let { realmUrl } = await createRealm(realmName, `Test ${realmName}`, { + profileManager, + }); + + let userBSuffix = `userb-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 6)}`; + let userBUsername = `cli-test-${userBSuffix}`; + let userBPassword = 'test-password-userb'; + await registerUser({ + matrixURL, + displayname: 'CLI Test User B', + username: userBUsername, + password: userBPassword, + registrationSecret: matrixRegistrationSecret, + }); + + let userBProfile = createTestProfileDir(); + try { + await userBProfile.profileManager.addProfile( + `@${userBUsername}:localhost`, + userBPassword, + 'CLI Test User B', + matrixURL.href, + `${TEST_REALM_SERVER_URL}/`, + ); + + let result = await archiveRealm({ + realmUrl, + profileManager: userBProfile.profileManager, + }); + + expect(result.archived).toBe(false); + expect(result.error).toMatch(/403/); + expect(result.error).toMatch(/do not own this realm/); + } finally { + userBProfile.cleanup(); + } + }); + + it('returns an error when no active profile', async () => { + let emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boxel-empty-')); + let emptyManager = new ProfileManager(emptyDir); + let result = await archiveRealm({ + realmUrl: `${TEST_REALM_SERVER_URL}/anything/`, + profileManager: emptyManager, + }); + expect(result.archived).toBe(false); + expect(result.error).toContain('No active profile'); + fs.rmSync(emptyDir, { recursive: true, force: true }); + }); +}); + +describe('realm restore (integration)', () => { + it('restores a previously archived realm for the owner', async () => { + let name = uniqueRealmName(); + let { realmUrl } = await createRealm(name, `Test ${name}`, { + profileManager, + }); + let archive = await archiveRealm({ realmUrl, profileManager }); + expect(archive.archived).toBe(true); + + let result = await restoreRealm({ realmUrl, profileManager }); + + expect(result.error).toBeUndefined(); + expect(result.restored).toBe(true); + expect(result.realmUrl).toBe(realmUrl); + }); + + it('returns a 403 error when the caller does not own the realm', async () => { + let realmName = uniqueRealmName(); + let { realmUrl } = await createRealm(realmName, `Test ${realmName}`, { + profileManager, + }); + await archiveRealm({ realmUrl, profileManager }); + + let userBSuffix = `userb-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 6)}`; + let userBUsername = `cli-test-${userBSuffix}`; + let userBPassword = 'test-password-userb'; + await registerUser({ + matrixURL, + displayname: 'CLI Test User B', + username: userBUsername, + password: userBPassword, + registrationSecret: matrixRegistrationSecret, + }); + + let userBProfile = createTestProfileDir(); + try { + await userBProfile.profileManager.addProfile( + `@${userBUsername}:localhost`, + userBPassword, + 'CLI Test User B', + matrixURL.href, + `${TEST_REALM_SERVER_URL}/`, + ); + + let result = await restoreRealm({ + realmUrl, + profileManager: userBProfile.profileManager, + }); + + expect(result.restored).toBe(false); + expect(result.error).toMatch(/403/); + expect(result.error).toMatch(/do not own this realm/); + } finally { + userBProfile.cleanup(); + } + + // Cleanup: restore the realm so it doesn't leak into other tests. + await restoreRealm({ realmUrl, profileManager }); + }); + + it('returns an error when no active profile', async () => { + let emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boxel-empty-')); + let emptyManager = new ProfileManager(emptyDir); + let result = await restoreRealm({ + realmUrl: `${TEST_REALM_SERVER_URL}/anything/`, + profileManager: emptyManager, + }); + expect(result.restored).toBe(false); + expect(result.error).toContain('No active profile'); + fs.rmSync(emptyDir, { recursive: true, force: true }); + }); +}); diff --git a/packages/boxel-cli/tests/integration/realm-list-archived.test.ts b/packages/boxel-cli/tests/integration/realm-list-archived.test.ts new file mode 100644 index 0000000000..82ffc9c829 --- /dev/null +++ b/packages/boxel-cli/tests/integration/realm-list-archived.test.ts @@ -0,0 +1,98 @@ +import '../helpers/setup-realm-server.ts'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createRealm } from '../../src/commands/realm/create.ts'; +import { archiveRealm } from '../../src/commands/realm/archive.ts'; +import { listRealms } from '../../src/commands/realm/list.ts'; +import type { ProfileManager } from '../../src/lib/profile-manager.ts'; +import { + startTestRealmServer, + stopTestRealmServer, + createTestProfileDir, + setupTestProfile, + uniqueRealmName, +} from '../helpers/integration.ts'; + +let profileManager: ProfileManager; +let cleanupProfile: () => void; + +beforeAll(async () => { + await startTestRealmServer(); + let testProfile = createTestProfileDir(); + profileManager = testProfile.profileManager; + cleanupProfile = testProfile.cleanup; + await setupTestProfile(profileManager); +}); + +afterAll(async () => { + cleanupProfile?.(); + await stopTestRealmServer(); +}); + +describe('realm list with archived realms (integration)', () => { + it('hides archived realms by default', async () => { + let name = uniqueRealmName(); + let { realmUrl } = await createRealm(name, `Test ${name}`, { + profileManager, + }); + + let beforeArchive = await listRealms({ profileManager }); + expect(beforeArchive.error).toBeUndefined(); + expect(beforeArchive.realms.map((r) => r.url)).toContain(realmUrl); + + let archive = await archiveRealm({ realmUrl, profileManager }); + expect(archive.archived).toBe(true); + + let afterArchive = await listRealms({ profileManager }); + expect(afterArchive.error).toBeUndefined(); + expect(afterArchive.realms.map((r) => r.url)).not.toContain(realmUrl); + + let allAccessible = await listRealms({ + allAccessible: true, + profileManager, + }); + expect(allAccessible.error).toBeUndefined(); + expect(allAccessible.realms.map((r) => r.url)).not.toContain(realmUrl); + }); + + it('--include-archived surfaces archived realms with an archived marker', async () => { + let name = uniqueRealmName(); + let { realmUrl } = await createRealm(name, `Test ${name}`, { + profileManager, + }); + await archiveRealm({ realmUrl, profileManager }); + + let result = await listRealms({ + includeArchived: true, + profileManager, + }); + + expect(result.error).toBeUndefined(); + let entry = result.realms.find((r) => r.url === realmUrl); + expect(entry).toBeDefined(); + expect(entry?.archived).toBe(true); + }); + + it('lists multiple archived realms together when --include-archived is set', async () => { + let nameA = uniqueRealmName(); + let nameB = uniqueRealmName(); + let { realmUrl: urlA } = await createRealm(nameA, `Test ${nameA}`, { + profileManager, + }); + let { realmUrl: urlB } = await createRealm(nameB, `Test ${nameB}`, { + profileManager, + }); + await archiveRealm({ realmUrl: urlA, profileManager }); + await archiveRealm({ realmUrl: urlB, profileManager }); + + let result = await listRealms({ + includeArchived: true, + profileManager, + }); + + expect(result.error).toBeUndefined(); + let archivedUrls = result.realms + .filter((r) => r.archived) + .map((r) => r.url); + expect(archivedUrls).toEqual(expect.arrayContaining([urlA, urlB])); + }); +}); diff --git a/packages/boxel-cli/tests/integration/realm-list.test.ts b/packages/boxel-cli/tests/integration/realm-list.test.ts index b160780327..e097565c51 100644 --- a/packages/boxel-cli/tests/integration/realm-list.test.ts +++ b/packages/boxel-cli/tests/integration/realm-list.test.ts @@ -61,9 +61,21 @@ describe('realm list (integration)', () => { expect(result.error).toBeUndefined(); expect(result.realms).toHaveLength(3); let byUrl = new Map(result.realms.map((r) => [r.url, r])); - expect(byUrl.get(visibleUrl)).toEqual({ url: visibleUrl, hidden: false }); - expect(byUrl.get(hiddenUrl)).toEqual({ url: hiddenUrl, hidden: true }); - expect(byUrl.get(pendingUrl)).toEqual({ url: pendingUrl, hidden: true }); + expect(byUrl.get(visibleUrl)).toEqual({ + url: visibleUrl, + hidden: false, + archived: false, + }); + expect(byUrl.get(hiddenUrl)).toEqual({ + url: hiddenUrl, + hidden: true, + archived: false, + }); + expect(byUrl.get(pendingUrl)).toEqual({ + url: pendingUrl, + hidden: true, + archived: false, + }); }); it('returns an error when --all-accessible and --hidden are both set', async () => { @@ -79,7 +91,9 @@ describe('realm list (integration)', () => { it('default mode lists only the realm in account data', async () => { let result = await listRealms({ profileManager }); expect(result.error).toBeUndefined(); - expect(result.realms).toEqual([{ url: visibleUrl, hidden: false }]); + expect(result.realms).toEqual([ + { url: visibleUrl, hidden: false, archived: false }, + ]); }); it('--hidden lists only realms missing from account data', async () => { @@ -102,7 +116,9 @@ describe('realm list (integration)', () => { let hidden = await listRealms({ hidden: true, profileManager }); expect(hidden.error).toBeUndefined(); - expect(hidden.realms).toEqual([{ url: hiddenUrl, hidden: true }]); + expect(hidden.realms).toEqual([ + { url: hiddenUrl, hidden: true, archived: false }, + ]); }); it('returns an error when no active profile', async () => {