import type { Page } from '@playwright/test';
import { completeSetup, expect, installIpcMocks, test } from './fixtures/electron';
function stableStringify(value: unknown): string {
if (value == null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
const entries = Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`);
return `{${entries.join(',')}}`;
}
function buildDreamingEnabledPatchRaw(enabled: boolean): string {
return JSON.stringify({
plugins: {
entries: {
'memory-core': {
config: {
dreaming: {
enabled,
},
},
},
},
},
});
}
async function enableDeveloperMode(page: Page): Promise<void> {
await page.getByTestId('sidebar-nav-settings').click();
await expect(page.getByTestId('settings-page')).toBeVisible();
const devModeToggle = page.getByTestId('settings-dev-mode-switch');
if ((await devModeToggle.getAttribute('data-state')) !== 'checked') {
await devModeToggle.click();
}
await expect(devModeToggle).toHaveAttribute('data-state', 'checked');
}
test.describe('OpenClaw Dreams', () => {
const dreamsRpcMocks = {
[stableStringify(['sessions.list', {}])]: {
success: true,
result: { sessions: [] },
},
[stableStringify(['doctor.memory.status', {}])]: {
success: true,
result: {
dreaming: {
enabled: true,
timezone: 'Asia/Shanghai',
storageMode: 'inline',
shortTermCount: 2,
groundedSignalCount: 1,
totalSignalCount: 3,
promotedToday: 1,
phases: {
light: {
enabled: true,
cron: '0 * * * *',
nextRunAtMs: Date.parse('2026-05-01T03:00:00Z'),
},
rem: { enabled: false },
deep: { enabled: true, cron: '0 3 * * *' },
},
shortTermEntries: [
{
key: 'native-dreams-ui',
path: 'memory/dreams/native-ui.md',
snippet: 'User expects Dreams to be a native ClawX interface, not only an external jump.',
startLine: 4,
totalSignalCount: 2,
},
],
promotedEntries: [],
},
},
},
[stableStringify(['doctor.memory.dreamDiary', {}])]: {
success: true,
result: {
found: true,
path: 'DREAMS.md',
content: [
'<!-- openclaw:dreaming:diary:start -->',
'*2026-05-01*',
'What Happened',
'- Native dreams page landed [memory/dreams/native-ui.md]',
'---',
'*2026-04-30*',
'Reflections',
'- Older note',
'<!-- openclaw:dreaming:diary:end -->',
].join('\n'),
},
},
[stableStringify(['doctor.memory.backfillDreamDiary', {}])]: {
success: true,
result: { written: 2 },
},
[stableStringify(['doctor.memory.dedupeDreamDiary', {}])]: {
success: true,
result: { removedEntries: 2, keptEntries: 5 },
},
[stableStringify(['doctor.memory.resetDreamDiary', {}])]: {
success: true,
result: { removedEntries: 4 },
},
[stableStringify(['doctor.memory.resetGroundedShortTerm', {}])]: {
success: true,
result: { removedShortTermEntries: 3 },
},
};
test('renders the native Dreams page and runs a maintenance action', async ({ electronApp, page }) => {
await installIpcMocks(electronApp, {
gatewayStatus: { state: 'running', port: 18789, pid: 12345, gatewayReady: true },
gatewayRpc: dreamsRpcMocks,
hostApi: {
[stableStringify(['/api/gateway/status', 'GET'])]: {
ok: true,
data: {
status: 200,
ok: true,
json: { state: 'running', port: 18789, pid: 12345, gatewayReady: true },
},
},
},
});
await completeSetup(page);
await enableDeveloperMode(page);
await expect(page.getByTestId('sidebar-nav-dreams')).toBeVisible();
await page.getByTestId('sidebar-nav-dreams').click();
await expect(page.getByTestId('dreams-page')).toBeVisible();
await expect(page.getByTestId('dreams-disable')).toBeVisible();
await expect(page.getByText('Native dreams page landed')).toBeVisible();
await expect(page.getByText('User expects Dreams to be a native ClawX interface')).toBeVisible();
await page.getByTestId('dreams-action-backfill').click();
await expect(page.getByTestId('dreams-action-message')).toContainText(/Backfilled 2 dream diary entries\.|已回填 2 条梦境日记。/);
await page.getByTestId('dreams-action-dedupe').click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('dreams-action-message')).toContainText(/Removed 2 duplicate dream entries and kept 5\.|已移除 2 条重复梦境,保留 5 条。/);
await page.getByTestId('dreams-action-reset-grounded').click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('dreams-action-message')).toContainText(/Cleared 3 replayed short-term entries\.|已清理 3 条回放短期记忆。/);
await page.getByTestId('dreams-action-reset-diary').click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('dreams-action-message')).toContainText(/Removed 4 backfilled dream diary entries\.|已移除 4 条回填梦境日记。/);
});
test('starts Dreams from the native page when dreaming is disabled', async ({ electronApp, page }) => {
const configHash = 'dreams-config-hash';
await installIpcMocks(electronApp, {
gatewayStatus: { state: 'running', port: 18789, pid: 12345, gatewayReady: true },
gatewayRpc: {
...dreamsRpcMocks,
[stableStringify(['doctor.memory.status', {}])]: {
success: true,
result: {
dreaming: {
enabled: false,
timezone: 'Asia/Shanghai',
storageMode: 'inline',
shortTermCount: 0,
groundedSignalCount: 0,
totalSignalCount: 0,
promotedToday: 0,
phases: {
light: { enabled: false },
rem: { enabled: false },
deep: { enabled: false },
},
shortTermEntries: [],
promotedEntries: [],
},
},
},
[stableStringify(['config.get', {}])]: {
success: true,
result: { hash: configHash },
},
[stableStringify(['config.patch', {
raw: buildDreamingEnabledPatchRaw(true),
baseHash: configHash,
note: 'Enable memory dreaming from ClawX Dreams.',
}])]: {
success: true,
result: { ok: true },
},
},
hostApi: {
[stableStringify(['/api/gateway/status', 'GET'])]: {
ok: true,
data: {
status: 200,
ok: true,
json: { state: 'running', port: 18789, pid: 12345, gatewayReady: true },
},
},
},
});
await completeSetup(page);
await enableDeveloperMode(page);
await page.getByTestId('sidebar-nav-dreams').click();
await expect(page.getByTestId('dreams-page')).toBeVisible();
await expect(page.getByTestId('dreams-enable')).toBeVisible();
await page.getByTestId('dreams-enable').click();
await expect(page.getByTestId('dreams-action-message')).toBeVisible();
await expect(page.getByTestId('dreams-disable')).toBeVisible();
});
test('waits for the gateway process before loading Dreams data', async ({ electronApp, page }) => {
await installIpcMocks(electronApp, {
gatewayStatus: { state: 'stopped', port: 18789 },
gatewayRpc: dreamsRpcMocks,
hostApi: {
[stableStringify(['/api/gateway/status', 'GET'])]: {
ok: true,
data: {
status: 200,
ok: true,
json: { state: 'stopped', port: 18789 },
},
},
},
});
await completeSetup(page);
await enableDeveloperMode(page);
await page.getByTestId('sidebar-nav-dreams').click();
await expect(page.getByTestId('dreams-page')).toBeVisible();
await expect(page.getByTestId('dreams-refresh')).toBeDisabled();
await expect(page.getByTestId('dreams-enable')).toBeDisabled();
await expect(page.getByTestId('dreams-action-backfill')).toBeDisabled();
await expect(page.getByTestId('dreams-error')).toHaveCount(0);
await electronApp.evaluate(({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows()[0];
win?.webContents.send('gateway:status-changed', {
state: 'running',
port: 18789,
pid: 12345,
gatewayReady: true,
connectedAt: Date.now(),
});
});
await expect(page.getByText('Native dreams page landed')).toBeVisible();
await expect(page.getByTestId('dreams-action-backfill')).toBeEnabled();
});
});