openpencil/tests/engine/tauri/mcp-spawn.test.ts

246 lines
9.2 KiB
TypeScript
Raw Normal View History

import { afterEach, describe, expect, test, vi } from 'bun:test'
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
import {
getAutomationAuthToken,
getAutomationHealthFailure,
getAutomationStartupFailure,
readAutomationHealth,
spawnMCPIfNeeded
} from '@/app/automation/mcp/spawn'
test: typecheck the test suites and fix what that found (#896) * build: typecheck the test suites Tests were in no TypeScript program: no tsconfig included tests/** or packages/*/tests/**, and bun strips types without checking them, so a fixture could drop a required field and keep passing until something read it. @types/bun moves to the root because it was installed per package only, and #cli-tests/* joins the paths the root config already carries. * test: fix the type errors the test suites were hiding Typechecking the tests turned up 1123 errors. Most were ordinary strictness, but some were real: `NodeChange` bound to Figma's plugin typings rather than the Kiwi codec in thirteen .fig tests, materializeInstance was called with six arguments against five so the blobs and source children were dropped, CanvasKit pixels were written to a plain object that never reached WASM, and assertions were made through accessors that do not exist, so they asserted nothing. Fixtures that had quietly lost a required field now carry it, nullable results are narrowed through the existing expectDefined helper rather than assumed, and stand-ins for CanvasKit and the editor go through one named helper instead of an unexplained cast at each site. No test was deleted, skipped, or weakened, and no `any`, non-null assertion, or ts-expect-error was introduced. * docs: record what typechecking the tests established Pins the app program's global types with an assertion rather than a note, since an unpinned types list lets any root @types package decide which platform src/** is judged against. The two environment faults that look like code regressions — Vite's dependency pre-bundle outliving a package rebuild, and heavy .fig suites failing under load — go to the development docs, where an explanation belongs. * fix: align @types/bun and keep node types resolvable when extended The root manifest declared a newer @types/bun than every package, which check:monorepo rejects, and pinning the app program's types left them unresolvable from a config that extends this one out of tree. * fix: fail the test typecheck when the compiler itself fails The gate matched diagnostics by substring, so a compiler or config failure that named no test file printed a pass while having checked nothing. Diagnostics are now split by whether they name a file: an unscoped one is the run failing and stops the gate, a test file's is a finding, and a source file's stays out by design. Also drops the parameter planComponentConstruction never read, and makes the inner-shadow verification script exit non-zero when it renders no image instead of logging and succeeding. * chore: merge master into tests-typecheck
2026-10-05 12:42:38 +00:00
import { fetchStub } from '#tests/helpers/fetch'
import { clearTauriMocks, installTauriMockWindow, mockTauriIPC } from '#tests/helpers/tauri/mocks'
const DISCOVERY_PATH = '/mock/home/.openpencil/mcp.json'
const DISCOVERY_JSON = JSON.stringify({
pid: 1234,
socketPath: '/mock/home/.openpencil/mcp.sock',
httpPort: 7600,
authRequired: true,
authToken: 'discovery-token',
version: '0.0.0-test',
startedAt: new Date().toISOString()
})
afterEach(async () => {
await clearTauriMocks()
vi.restoreAllMocks()
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'navigator')
Reflect.deleteProperty(globalThis, 'location')
})
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
describe('MCP health probe diagnostics', () => {
test('records a rejected token separately from an unreachable server', async () => {
installTauriMockWindow()
const reject = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValue(new Response('', { status: 401 }))
await expect(readAutomationHealth('stale-token')).resolves.toBeNull()
expect(getAutomationHealthFailure()).toEqual({ code: 'rejected', detail: 'HTTP 401' })
reject.mockResolvedValue(new Response('', { status: 500 }))
await expect(readAutomationHealth('stale-token')).resolves.toBeNull()
expect(getAutomationHealthFailure()).toEqual({ code: 'malformed', detail: 'HTTP 500' })
reject.mockRejectedValue(new Error('connection refused'))
await expect(readAutomationHealth('stale-token')).resolves.toBeNull()
expect(getAutomationHealthFailure()).toEqual({
code: 'unreachable',
detail: expect.any(String)
})
})
test('records an unexpected health payload and clears the failure on success', async () => {
installTauriMockWindow()
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValue(new Response(JSON.stringify({ unexpected: true }), { status: 200 }))
await expect(readAutomationHealth()).resolves.toBeNull()
expect(getAutomationHealthFailure()).toEqual({ code: 'malformed', detail: 'HTTP 200' })
fetchMock.mockResolvedValue(
new Response(JSON.stringify({ status: 'ok', version: '0.0.0-test' }), { status: 200 })
)
await expect(readAutomationHealth()).resolves.toMatchObject({ status: 'ok' })
expect(getAutomationHealthFailure()).toBeNull()
})
})
describe('Tauri MCP spawning', () => {
test('retains an early startup failure for MCP-dependent features', async () => {
installTauriMockWindow()
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { platform: 'Win32', userAgent: 'Windows' }
})
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
await mockTauriIPC((cmd, args) => {
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
if (cmd === 'mcp_lookup')
return { available: true, path: '/mock/bin/openpencil-mcp-http', searched: ['/mock/bin'] }
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
if (cmd === 'plugin:fs|exists') return false
if (cmd === 'plugin:shell|spawn') {
const onEvent = (args as { onEvent: { onmessage: (event: unknown) => void } }).onEvent
.onmessage
queueMicrotask(() => {
onEvent({
event: 'Stderr',
payload: Array.from(
new TextEncoder().encode('El sistema no puede encontrar el archivo especificado.')
)
})
onEvent({ event: 'Terminated', payload: { code: 1, signal: null } })
})
return 88
}
return null
})
expect(await spawnMCPIfNeeded()).toBeNull()
expect(getAutomationAuthToken()).rejects.toThrow('MCP server exited before startup completed')
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
expect(getAutomationStartupFailure()).toMatchObject({
code: 'exited',
detail: expect.stringContaining('code=')
})
})
test('retains unexpected spawn errors for MCP-dependent features', async () => {
installTauriMockWindow()
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { platform: 'MacIntel', userAgent: 'Macintosh' }
})
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
await mockTauriIPC((cmd) => {
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
if (cmd === 'mcp_lookup')
return { available: true, path: '/mock/bin/openpencil-mcp-http', searched: ['/mock/bin'] }
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
if (cmd === 'plugin:fs|exists') return false
if (cmd === 'plugin:shell|spawn') throw new Error('shell permission denied')
return null
})
expect(await spawnMCPIfNeeded()).toBeNull()
expect(getAutomationAuthToken()).rejects.toThrow('shell permission denied')
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
expect(getAutomationStartupFailure()).toMatchObject({ code: 'permission-denied' })
})
test('retains a language-independent missing executable diagnostic', async () => {
installTauriMockWindow()
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { platform: 'MacIntel', userAgent: 'Macintosh' }
})
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
await mockTauriIPC((cmd) => {
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
if (cmd === 'plugin:fs|exists') return false
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
if (cmd === 'mcp_lookup')
return { available: false, path: null, searched: ['/usr/local/bin', '/mock/home/.bun/bin'] }
return null
})
expect(await spawnMCPIfNeeded()).toBeNull()
expect(getAutomationAuthToken()).rejects.toThrow('MCP automation is not installed')
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
expect(getAutomationStartupFailure()).toMatchObject({
code: 'not-installed',
// The searched directories let a report show where the app looked.
detail: expect.stringContaining('/mock/home/.bun/bin')
})
})
test('spawns MCP server with shell plugin when health check is missing', async () => {
installTauriMockWindow()
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { platform: 'MacIntel' }
})
let healthChecks = 0
test: typecheck the test suites and fix what that found (#896) * build: typecheck the test suites Tests were in no TypeScript program: no tsconfig included tests/** or packages/*/tests/**, and bun strips types without checking them, so a fixture could drop a required field and keep passing until something read it. @types/bun moves to the root because it was installed per package only, and #cli-tests/* joins the paths the root config already carries. * test: fix the type errors the test suites were hiding Typechecking the tests turned up 1123 errors. Most were ordinary strictness, but some were real: `NodeChange` bound to Figma's plugin typings rather than the Kiwi codec in thirteen .fig tests, materializeInstance was called with six arguments against five so the blobs and source children were dropped, CanvasKit pixels were written to a plain object that never reached WASM, and assertions were made through accessors that do not exist, so they asserted nothing. Fixtures that had quietly lost a required field now carry it, nullable results are narrowed through the existing expectDefined helper rather than assumed, and stand-ins for CanvasKit and the editor go through one named helper instead of an unexplained cast at each site. No test was deleted, skipped, or weakened, and no `any`, non-null assertion, or ts-expect-error was introduced. * docs: record what typechecking the tests established Pins the app program's global types with an assertion rather than a note, since an unpinned types list lets any root @types package decide which platform src/** is judged against. The two environment faults that look like code regressions — Vite's dependency pre-bundle outliving a package rebuild, and heavy .fig suites failing under load — go to the development docs, where an explanation belongs. * fix: align @types/bun and keep node types resolvable when extended The root manifest declared a newer @types/bun than every package, which check:monorepo rejects, and pinning the app program's types left them unresolvable from a config that extends this one out of tree. * fix: fail the test typecheck when the compiler itself fails The gate matched diagnostics by substring, so a compiler or config failure that named no test file printed a pass while having checked nothing. Diagnostics are now split by whether they name a file: an unscoped one is the run failing and stops the gate, a test file's is a finding, and a source file's stays out by design. Also drops the parameter planComponentConstruction never read, and makes the inner-shadow verification script exit non-zero when it renders no image instead of logging and succeeding. * chore: merge master into tests-typecheck
2026-10-05 12:42:38 +00:00
vi.spyOn(globalThis, 'fetch').mockImplementation(
fetchStub(async () => {
healthChecks += 1
if (healthChecks === 1) return new Response('', { status: 404 })
return new Response(
JSON.stringify({
status: 'ok',
version: '0.0.0-test',
authRequired: true,
discoveryPath: DISCOVERY_PATH
}),
{ status: 200 }
)
})
)
test: typecheck the test suites and fix what that found (#896) * build: typecheck the test suites Tests were in no TypeScript program: no tsconfig included tests/** or packages/*/tests/**, and bun strips types without checking them, so a fixture could drop a required field and keep passing until something read it. @types/bun moves to the root because it was installed per package only, and #cli-tests/* joins the paths the root config already carries. * test: fix the type errors the test suites were hiding Typechecking the tests turned up 1123 errors. Most were ordinary strictness, but some were real: `NodeChange` bound to Figma's plugin typings rather than the Kiwi codec in thirteen .fig tests, materializeInstance was called with six arguments against five so the blobs and source children were dropped, CanvasKit pixels were written to a plain object that never reached WASM, and assertions were made through accessors that do not exist, so they asserted nothing. Fixtures that had quietly lost a required field now carry it, nullable results are narrowed through the existing expectDefined helper rather than assumed, and stand-ins for CanvasKit and the editor go through one named helper instead of an unexplained cast at each site. No test was deleted, skipped, or weakened, and no `any`, non-null assertion, or ts-expect-error was introduced. * docs: record what typechecking the tests established Pins the app program's global types with an assertion rather than a note, since an unpinned types list lets any root @types package decide which platform src/** is judged against. The two environment faults that look like code regressions — Vite's dependency pre-bundle outliving a package rebuild, and heavy .fig suites failing under load — go to the development docs, where an explanation belongs. * fix: align @types/bun and keep node types resolvable when extended The root manifest declared a newer @types/bun than every package, which check:monorepo rejects, and pinning the app program's types left them unresolvable from a config that extends this one out of tree. * fix: fail the test typecheck when the compiler itself fails The gate matched diagnostics by substring, so a compiler or config failure that named no test file printed a pass while having checked nothing. Diagnostics are now split by whether they name a file: an unscoped one is the run failing and stops the gate, a test file's is a finding, and a source file's stays out by design. Also drops the parameter planComponentConstruction never read, and makes the inner-shadow verification script exit non-zero when it renders no image instead of logging and succeeding. * chore: merge master into tests-typecheck
2026-10-05 12:42:38 +00:00
const spawned: { onEvent: ((event: unknown) => void) | null } = { onEvent: null }
const calls: Array<{ cmd: string; args: unknown }> = []
await mockTauriIPC((cmd, args) => {
calls.push({ cmd, args })
feat(settings): configure tool access, MCP failures, and step limits * feat(settings): configure tool access and agent step limits Built-in AI exposed only a hardcoded subset of the tool registry, and the maximum agent steps was a constant, so users could neither enable extended tools such as create_component nor adjust long-running tasks. Built-in AI and the local MCP server now keep independent, locally saved tool permissions over one shared catalog, with searchable read-only and side-effect groups and per-target defaults. Chat settings gain a validated maximum-steps field whose captured value drives the stop condition, remaining-step warnings, and limit detection for each message. Tool access, the local server, browser access, and MCP connections are grouped under a single Automation settings page. Closes #573 Closes #584 * refactor(settings): split automation into MCP and Tool access pages The Automation page mixed a permission matrix with server endpoints behind a Tools/Connections switch, and the view switch was indistinguishable from the provider switch. The nested scroll region showed three of 110 tools. Rename the MCP-facing page to MCP and give tool permissions their own Tool access page. The page owns a fixed toolbar for the target, count, defaults, and search, so the list uses the full dialog body and no row is clipped. * fix(automation): explain MCP startup failures with localized guidance Every startup failure collapsed into "MCP server did not become healthy": the spawn layer recorded the real error but the runtime discarded it, and health probes could not distinguish a rejected token from a missing server. The message also surfaced raw English text as the alert heading. Classify failures by reason (not installed, denied command, early exit, startup timeout, rejected token, unexpected response, unreachable) and render translated heading and guidance from the catalog, keeping captured stderr or HTTP status as labeled diagnostic detail. * refactor(ui): share one collapsible disclosure primitive Six features each wired Reka's collapsible with their own motion classes and one settings-only theme token, so the same interaction drifted in spacing, icon size, and reduced-motion handling. Add AppCollapsible with a family theme and move the settings disclosure and the model editor's advanced settings onto it. Chat and frame-preset call sites keep their distinct visuals for a follow-up. * fix(automation): explain MCP failures with localized details The failure alert carried raw English error text as its heading, and the diagnostic payload sat in a sibling block outside the alert with no relationship to it. Classify failures by reason, render translated heading and guidance from the catalog, and keep the payload in a collapsible inside the alert, which unmounts while collapsed so the live region announces only the summary. Add a copy action for issue reports. Find the executable where a graphical launch can: extend PATH with the common global bin directories before the lookup and report the searched directories as diagnostic detail. * fix(automation): keep MCP failure details out of reasons already explained An unreachable address and a rejected token already name their cause in the translated guidance, so repeating it under Details added noise. Details now carry only output the summary cannot: stderr, HTTP status, or an unknown error message. * test(settings): browse every MCP failure reason in Storybook The failure copy lived inside the settings panel, so reviewing the eight reasons meant reproducing each failure and the mapping could only be checked through the panel's dependencies. Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail visibility, copy action, and restart action, and add a story covering every reason plus the collapsed-details behavior. * fix(ui): order alert details above the recovery actions The alert rendered its action buttons before the details slot, so the collapsible explanation of a failure appeared under the controls it explains. Details now render directly after the description. * fix(automation): correct MCP failure classification and detail Review follow-ups on the failure diagnostics. Only 401 and 403 mean the server refused our token; any other status now reports an unexpected response instead of telling the user to replace a token that was never the problem. The install hint rendered the whole diagnostic detail as its package argument, so searched directories appeared inside the install command. The install target is now a domain constant and the searched directories stay as detail, which not-installed failures surface again since they are the actionable desktop diagnostic. Exited failures also record the process exit code and signal so copied diagnostics stay conclusive when stderr is empty. The bundled PATH test now covers the append branch instead of only the unchanged path. * feat(settings): accept custom values for presets and retention Retention was a closed set of three counts while the AI step limit was a free number, so two bounded numeric preferences looked and behaved differently for no product reason. Add a shared preset-or-custom field: presets stay one click, the escape hatch reveals a validated numeric field, and the model carries only the resolved number. Diagnostics retention becomes a bounded number (50 to 20,000) with the presets as shortcuts, and the hardcoded revalidation in the panel is replaced by one domain resolver. * fix(settings): label the preset and custom fields Replacing the labeled provider field with the shared control left the AI step limit as a bare select with a detached hint paragraph, outside the settings group, so nothing on screen said what the number meant. The accessibility name came from aria-label, which is why behavior tests passed while the panel was unreadable. Move both controls into labeled settings rows with their descriptions, and give the revealed field its own accessible name so the two controls in one row differ. The specs now assert the control lives inside the row that names it, which is the check that would have caught this. * fix(mcp): allow the desktop app origin by default A server started manually bound the port and answered curl but the app webview could not use it: no CORS origin was configured, so the browser blocked every fetch and the app reported the server as unhealthy. The workaround required an undocumented environment variable. Allow the desktop app origins by default, accept a comma-separated override, and document the default in the CLI help and the security notes. Authenticated requests still need the bearer token, and browsers set Origin themselves, so only the app webview can present these origins. * fix(settings): address review findings on the new controls Copy details awaited nothing and confirmed the copy before the write finished. VueUse never rejects and falls back to a legacy write, so the await is what makes the confirmation honest rather than an error branch. The preset field only left custom mode when a preset arrived; a non-preset value assigned from the owner left the select showing a value absent from its options with the field still hidden. The watcher now follows the model in both directions. The story play functions queried the revealed field by the row label, which Testing Library matches as a whole string, so those interactions could not find it. The Storybook smoke assertion also assumed a button or tab, which skipped every story built from other primitives.
2026-09-17 20:55:58 +00:00
if (cmd === 'mcp_lookup')
return { available: true, path: '/mock/bin/openpencil-mcp-http', searched: ['/mock/bin'] }
if (cmd === 'plugin:shell|spawn') {
expect(args).toMatchObject({
program: 'openpencil-mcp-http',
args: [],
options: {
env: {
PORT: '7600',
OPENPENCIL_MCP_AUTH_TOKEN: expect.any(String),
OPENPENCIL_MCP_CORS_ORIGIN: 'tauri://localhost',
OPENPENCIL_MCP_TCP: '1',
fix(mcp): close orphaned servers after app disconnects (#494) * fix(mcp): close orphaned servers that no app ever claims - Add ServerOptions.appAttachTimeoutMs: if no app registers within this window after startup, the server closes itself and removes its discovery file, instead of squatting the port indefinitely. - Wire it through the openpencil-mcp-http CLI as OPENPENCIL_MCP_APP_TIMEOUT_MS (opt-in, unset/0 disables it — a bare CLI invocation for manual testing should not self-terminate). - The desktop app opts in with a 30s timeout when it spawns the server. Without this, a server that outlives its spawning app (renderer crash, forced reload) keeps holding its port with a stale discovery file. The app's liveness check only asks whether something answers /health, not whether an app has ever registered (see /health's no_app status) — so every later launch finds the orphan already listening and defers to it, and MCP tool calls fail with "app is not connected" until someone manually kills the orphaned process. Closing self-caused orphans at the source means the next launch finds no discovery file and takes the normal fresh-spawn path. Fixes #488 * fix(mcp): clean up servers after app disconnects - Re-arm the orphan watchdog when the registered app disconnects - Cancel pending shutdown when the app reconnects within the grace period - Reject timeout values that overflow the runtime timer range * test(mcp): make watchdog reconnect coverage deterministic - Wait for the disconnected health state before reconnecting - Report distinct safe-integer and timer-range validation errors --------- Co-authored-by: swe-sanad <sanad.arousi@export119.com>
2026-08-12 15:44:31 +00:00
OPENPENCIL_MCP_ROOT: '/mock/home',
OPENPENCIL_MCP_APP_TIMEOUT_MS: '30000'
}
}
})
test: typecheck the test suites and fix what that found (#896) * build: typecheck the test suites Tests were in no TypeScript program: no tsconfig included tests/** or packages/*/tests/**, and bun strips types without checking them, so a fixture could drop a required field and keep passing until something read it. @types/bun moves to the root because it was installed per package only, and #cli-tests/* joins the paths the root config already carries. * test: fix the type errors the test suites were hiding Typechecking the tests turned up 1123 errors. Most were ordinary strictness, but some were real: `NodeChange` bound to Figma's plugin typings rather than the Kiwi codec in thirteen .fig tests, materializeInstance was called with six arguments against five so the blobs and source children were dropped, CanvasKit pixels were written to a plain object that never reached WASM, and assertions were made through accessors that do not exist, so they asserted nothing. Fixtures that had quietly lost a required field now carry it, nullable results are narrowed through the existing expectDefined helper rather than assumed, and stand-ins for CanvasKit and the editor go through one named helper instead of an unexplained cast at each site. No test was deleted, skipped, or weakened, and no `any`, non-null assertion, or ts-expect-error was introduced. * docs: record what typechecking the tests established Pins the app program's global types with an assertion rather than a note, since an unpinned types list lets any root @types package decide which platform src/** is judged against. The two environment faults that look like code regressions — Vite's dependency pre-bundle outliving a package rebuild, and heavy .fig suites failing under load — go to the development docs, where an explanation belongs. * fix: align @types/bun and keep node types resolvable when extended The root manifest declared a newer @types/bun than every package, which check:monorepo rejects, and pinning the app program's types left them unresolvable from a config that extends this one out of tree. * fix: fail the test typecheck when the compiler itself fails The gate matched diagnostics by substring, so a compiler or config failure that named no test file printed a pass while having checked nothing. Diagnostics are now split by whether they name a file: an unscoped one is the run failing and stops the gate, a test file's is a finding, and a source file's stays out by design. Also drops the parameter planComponentConstruction never read, and makes the inner-shadow verification script exit non-zero when it renders no image instead of logging and succeeding. * chore: merge master into tests-typecheck
2026-10-05 12:42:38 +00:00
spawned.onEvent = (
args as { onEvent: { onmessage: (event: unknown) => void } }
).onEvent.onmessage
return 77
}
const pathArg = (args as { path?: string })?.path
if (cmd === 'plugin:fs|exists') {
return pathArg === DISCOVERY_PATH
}
if (cmd === 'plugin:fs|read_text_file') {
// Only return discovery data when reading the expected discovery path
if (pathArg === DISCOVERY_PATH) return new TextEncoder().encode(DISCOVERY_JSON)
return null
}
// Handle homeDir() IPC call from resolveTauriHomeDir
if (cmd === 'plugin:path|resolve_directory') {
return '/mock/home'
}
return null
})
const handle = await spawnMCPIfNeeded({ earlyExitMs: 5, healthPollMs: 5 })
await expect(getAutomationAuthToken()).resolves.toBe('discovery-token')
test: typecheck the test suites and fix what that found (#896) * build: typecheck the test suites Tests were in no TypeScript program: no tsconfig included tests/** or packages/*/tests/**, and bun strips types without checking them, so a fixture could drop a required field and keep passing until something read it. @types/bun moves to the root because it was installed per package only, and #cli-tests/* joins the paths the root config already carries. * test: fix the type errors the test suites were hiding Typechecking the tests turned up 1123 errors. Most were ordinary strictness, but some were real: `NodeChange` bound to Figma's plugin typings rather than the Kiwi codec in thirteen .fig tests, materializeInstance was called with six arguments against five so the blobs and source children were dropped, CanvasKit pixels were written to a plain object that never reached WASM, and assertions were made through accessors that do not exist, so they asserted nothing. Fixtures that had quietly lost a required field now carry it, nullable results are narrowed through the existing expectDefined helper rather than assumed, and stand-ins for CanvasKit and the editor go through one named helper instead of an unexplained cast at each site. No test was deleted, skipped, or weakened, and no `any`, non-null assertion, or ts-expect-error was introduced. * docs: record what typechecking the tests established Pins the app program's global types with an assertion rather than a note, since an unpinned types list lets any root @types package decide which platform src/** is judged against. The two environment faults that look like code regressions — Vite's dependency pre-bundle outliving a package rebuild, and heavy .fig suites failing under load — go to the development docs, where an explanation belongs. * fix: align @types/bun and keep node types resolvable when extended The root manifest declared a newer @types/bun than every package, which check:monorepo rejects, and pinning the app program's types left them unresolvable from a config that extends this one out of tree. * fix: fail the test typecheck when the compiler itself fails The gate matched diagnostics by substring, so a compiler or config failure that named no test file printed a pass while having checked nothing. Diagnostics are now split by whether they name a file: an unscoped one is the run failing and stops the gate, a test file's is a finding, and a source file's stays out by design. Also drops the parameter planComponentConstruction never read, and makes the inner-shadow verification script exit non-zero when it renders no image instead of logging and succeeding. * chore: merge master into tests-typecheck
2026-10-05 12:42:38 +00:00
spawned.onEvent?.({ event: 'Stderr', payload: [119, 97, 114, 110] })
handle?.disconnect()
await Promise.resolve()
expect(handle?.authToken).toBe('discovery-token')
expect(calls.some((c) => c.cmd === 'plugin:shell|spawn')).toBe(true)
expect(
calls.some(
(c) =>
c.cmd === 'plugin:fs|read_text_file' &&
(c.args as { path?: string })?.path === DISCOVERY_PATH
)
).toBe(true)
expect(calls.at(-1)).toEqual({ cmd: 'plugin:shell|kill', args: { cmd: 'killChild', pid: 77 } })
})
})