fix(web): harden typed SPA chat lifecycle

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
shaggy (mosaic-dev box)
2026-08-10 00:24:20 -05:00
co-authored by Claude Haiku 4.5
parent b2e005f2b4
commit caebf9ef70
20 changed files with 1778 additions and 125 deletions
+105 -3
View File
@@ -233,7 +233,7 @@ describe('ChatPage', () => {
});
});
it('shows visible alert surfaces for a server error and a failed command result', async () => {
it('shows visible alert surfaces for a server error and a stable failure copy for a failed command result', async () => {
await act(async () => {
fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
fake.serverEmit('error', { conversationId: 'c1', error: 'The model is unavailable' });
@@ -241,14 +241,116 @@ describe('ChatPage', () => {
conversationId: 'c1',
command: 'model',
success: false,
message: 'unknown model id',
message: 'raw internal detail: stack trace at line 42',
});
});
const alerts = [...container.querySelectorAll('[role="alert"]')];
const alertText = alerts.map((node) => node.textContent).join(' ');
expect(alertText).toContain('The model is unavailable');
expect(alertText).toContain('unknown model id');
// Sanitized client copy, not the raw server-provided detail.
expect(alertText).toContain('Command failed.');
expect(alertText).not.toContain('raw internal detail');
});
it('renders a safe fallback when session:info arrives with a malformed (non-array) availableThinkingLevels, without throwing', async () => {
await act(async () => {
fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
fake.serverEmitRaw('session:info', {
conversationId: 'c1',
provider: 'anthropic',
modelId: 'claude',
thinkingLevel: 'medium',
availableThinkingLevels: null,
});
});
expect(container.querySelector('section[aria-label="Session info"]')).toBeTruthy();
const select = container.querySelector(
'select[aria-label="Thinking level"]',
) as HTMLSelectElement;
expect(select).toBeTruthy();
// A malformed level list still shows a visible, safe placeholder option
// rather than a silently empty select.
expect([...select.options]).toHaveLength(1);
expect(select.options[0]?.textContent).toMatch(/unavailable/i);
await act(async () => {
selectValue(select, '');
});
expect(fake.emitted.filter((e) => e.event === 'set:thinking')).toHaveLength(0);
});
it('renders honest unavailable labels — not fabricated zeros — when agent:end usage has malformed/missing numeric fields', async () => {
await act(async () => {
fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
fake.serverEmit('agent:start', { conversationId: 'c1' });
fake.serverEmitRaw('agent:end', {
conversationId: 'c1',
usage: {
provider: { nested: 'object' },
modelId: undefined,
thinkingLevel: 'medium',
tokens: { total: 'not-a-number' },
cost: undefined,
context: { percent: null, window: 200000 },
},
});
});
const usage = container.querySelector('[aria-label="Usage"]');
expect(usage).toBeTruthy();
expect(usage?.textContent).toContain('tokens unavailable');
expect(usage?.textContent).toContain('cost unavailable');
expect(usage?.textContent).not.toContain('0 tokens');
expect(usage?.textContent).not.toContain('$0.0000');
expect(usage?.textContent).toContain('unknown/unknown');
});
it('renders a safe fallback for message:ack when messageId is a malformed non-string value, without throwing', async () => {
await expect(
act(async () => {
fake.serverEmitRaw('message:ack', { conversationId: 'c1', messageId: { bad: 'object' } });
}),
).resolves.not.toThrow();
const status = [...container.querySelectorAll('[role="status"]')].find((node) =>
node.textContent?.includes('Message accepted'),
);
expect(status).toBeTruthy();
// A malformed messageId gets a stable, visible fallback — never blank,
// never the raw object.
expect(status?.textContent).toContain('unknown');
});
it('renders safely and does not throw when system:reload.message is a malformed non-string value', async () => {
await expect(
act(async () => {
fake.serverEmitRaw('system:reload', {
commands: [],
skills: [],
providers: [],
message: { bad: 'object' },
});
}),
).resolves.not.toThrow();
const status = container.querySelector('[role="status"]');
expect(status).toBeTruthy();
// A malformed reload message renders a stable, visible fallback rather
// than a silently empty status line.
expect(status?.textContent).toContain('Commands reloaded.');
});
it('renders safely and does not throw when a scoped error carries a malformed non-string error value', async () => {
await expect(
act(async () => {
fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
fake.serverEmitRaw('error', { conversationId: 'c1', error: ['not', 'a', 'string'] });
}),
).resolves.not.toThrow();
expect(container.querySelector('[role="alert"]')).toBeTruthy();
});
it('sends a message with optional provider/model fields and emits abort from the Stop control', async () => {