Skip to content
15 changes: 15 additions & 0 deletions apps/desktop/src/main/__tests__/app-shell-session-ui-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ function seededState(): AppShellSessionUiState {
}

describe('session live run display state', () => {
it('keeps background authority separate from parent status and discards cached activity', () => {
for (const backgroundActivity of ['idle', 'running', 'waiting_for_user', 'blocked'] as const) {
const live = { id: 'root', status: 'running', runningTurnIds: [], backgroundActivity } as unknown as SessionSummary;
const normalized = normalizeSessionSummaryForDisplay(live);
assert.equal(normalized.status, 'active');
assert.equal(normalized.backgroundActivity, backgroundActivity);
const cached = normalizeSessionSummaryForDisplay({ ...live, localState: 'cached' as const });
assert.equal(cached.status, 'active');
assert.equal(Object.hasOwn(cached, 'backgroundActivity'), false);
}
assert.equal(Object.hasOwn(normalizeSessionSummaryForDisplay({
id: 'unknown', status: 'active',
} as SessionSummary), 'backgroundActivity'), false);
});

it('keeps persisted running as a fallback only while live state is unknown', () => {
const unknown = { id: 'unknown', status: 'running' } as SessionSummary;
const knownEmpty = {
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/main/__tests__/desktop-session-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ test('preserves the authenticated shared Session revision', () => {
);
});

test('preserves shared Session background activity through Host identity projection', () => {
for (const backgroundActivity of ['idle', 'running', 'waiting_for_user', 'blocked'] as const) {
const shared = projectDesktopSharedSessionSummary({
kind: 'shared_session', id: 'shared-session', revision: 7, createdAt: 1, activityAt: 2,
name: 'Shared', status: 'active', backgroundActivity,
liveRunState: { schemaVersion: 1, runningTurnIds: [] },
});
const projected = projectDesktopSessionSummary({
hostId: 'remote-root', profileId: 'guest', profileName: 'Guest', profileKind: 'remote',
}, shared);
assert.equal(projected.backgroundActivity, backgroundActivity);
assert.deepEqual(projected.runningTurnIds, []);
}
});

test('retires an active Session only after it leaves the refreshed catalog', () => {
const owner = projectDesktopSessionSummary(
{
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ const bootSource = readFileSync(
fileURLToPath(new URL('../../../src/main/runtime-host-boot.ts', import.meta.url)),
'utf8',
);

test('Owner catalog sees every connection transition before the renderer can refresh', () => {
const stateChanged = bootSource.indexOf('onTargetStateChanged: (state) => {');
const notification = bootSource.indexOf('mainWindowController.send("runtime-host-profiles:changed"', stateChanged);
assert.ok(stateChanged >= 0 && notification > stateChanged);
assert.match(
bootSource.slice(stateChanged, notification),
/if \(localTarget\) \{\s*sessionLocal\.connectionChanged\(localTarget\);/u,
'Owner outages must invalidate catalog authority even without an offline catalog read',
);
});

const earlyWindowSource = readFileSync(
fileURLToPath(new URL('../../../src/main/early-window.ts', import.meta.url)),
'utf8',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { runInNewContext } from 'node:vm';
import test from 'node:test';
import { build } from 'esbuild';
import { deferred } from '@maka/core/test-only/async-primitives';
import type { MakaBridge } from '../../preload/bridge-contract.js';
import type { DesktopSessionSummary, MakaBridge } from '../../preload/bridge-contract.js';
import { normalizeSessionSummaryForDisplay } from '../../renderer/application/contracts/session-status-presentation.js';

test('onboarding and workspace search never fan out Owner IPC to a ready Guest', async () => {
const owner = {
Expand Down Expand Up @@ -108,3 +109,71 @@ test('onboarding and workspace search never fan out Owner IPC to a ready Guest',
assert.equal(calls.filter(call => call.channel === channel && call.hostId === owner.hostId).length, 1);
}
});

test('the actual Guest preload preserves cached history without projecting its old running state', async () => {
const owner = { hostId: 'owner', targetEpoch: 'epoch', profileId: 'local', profileName: 'Local',
profileKind: 'local', profileAccess: 'owner', readiness: 'ready' };
let readiness = 'ready';
let sessionState: 'live' | 'cached' | undefined = 'live';
const session = {
kind: 'shared_session', id: 'shared', revision: 1, createdAt: 1, activityAt: 1,
name: 'Shared graph', status: 'running', backgroundActivity: 'running',
liveRunState: { schemaVersion: 1, runningTurnIds: ['own-turn'] },
};
const ipcRenderer = {
on() {}, off() {}, send() {},
async invoke(channel: string) {
switch (channel) {
case 'runtime-host:identities': return [{ ...owner, epoch: owner.targetEpoch, isDefault: true }];
case 'session-local:catalog': return [{ scope: owner, sessions: [], authoritative: true }];
case 'session-collaboration:mount:list': return [{
mountId: 'guest', name: 'Guest', hostId: 'guest-host', readiness, sessionState, session,
}];
default: throw new Error(`Unexpected channel: ${channel}`);
}
},
};
let bridge: MakaBridge | undefined;
const bundle = await build({
entryPoints: [fileURLToPath(new URL('../../../src/preload/preload.ts', import.meta.url))],
bundle: true, write: false, platform: 'node', format: 'cjs', external: ['electron'],
});
const require = createRequire(import.meta.url);
runInNewContext(bundle.outputFiles[0]!.text, {
require: (id: string) => id === 'electron' ? {
ipcRenderer,
contextBridge: { exposeInMainWorld: (name: string, value: MakaBridge) => {
if (name === 'maka') bridge = value;
} },
} : require(id),
process: { env: {} }, Buffer, console, setTimeout, clearTimeout, TextEncoder, TextDecoder,
crypto: globalThis.crypto,
});
assert.ok(bridge);
const live = (await bridge.sessions.list())[0]!;
assert.equal(live.backgroundActivity, 'running');
assert.equal(live.runningTurnIds?.[0], 'own-turn');
assert.equal(live.localState, undefined);
for (const state of [
{ readiness: 'reconnecting', sessionState: 'live' as const },
{ readiness: 'ready', sessionState: 'cached' as const },
{ readiness: 'unavailable', sessionState: 'cached' as const },
{ readiness: 'ready', sessionState: undefined },
]) {
readiness = state.readiness;
sessionState = state.sessionState;
const cached: DesktopSessionSummary = normalizeSessionSummaryForDisplay((await bridge.sessions.list())[0]!);
assert.equal(cached.name, 'Shared graph');
assert.equal(cached.localState, 'cached');
assert.equal(cached.backgroundActivity, undefined);
assert.equal(cached.runningTurnIds, undefined);
assert.equal(cached.status, 'active', 'cached persisted running cannot supply the fallback pulse');
}
readiness = 'ready';
sessionState = 'live';
session.backgroundActivity = 'idle';
session.liveRunState.runningTurnIds = [];
const settled = (await bridge.sessions.list())[0]!;
assert.equal(settled.localState, undefined);
assert.equal(settled.backgroundActivity, 'idle', 'only a live snapshot can assert completion');
});
Loading