@geminilight/mindos 0.6.63 → 0.6.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +21 -21
- package/_standalone/.next/build-manifest.json +2 -2
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js +48 -42
- package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +2 -2
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +3 -3
- package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/wiki/page.js +1 -1
- package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app-paths-manifest.json +21 -21
- package/_standalone/.next/server/chunks/122.js +222 -0
- package/_standalone/.next/server/chunks/3113.js +52 -0
- package/_standalone/.next/server/chunks/6539.js +1 -1
- package/_standalone/.next/server/chunks/8388.js +2 -2
- package/_standalone/.next/server/chunks/953.js +3 -3
- package/_standalone/.next/server/chunks/9787.js +2 -0
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
- package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
- package/_standalone/.next/static/chunks/{5581-82e5db227f8e9393.js → 5581-c671163a2fe1b312.js} +2 -2
- package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
- package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
- package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
- package/_standalone/.next/static/chunks/{3674-be69a8b858ceacdd.js → 7294-cac25d97869afadc.js} +1 -1
- package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
- package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
- package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
- package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-b0dabe793500383d.js → page-2f5cf97e03dc1cc9.js} +1 -1
- package/_standalone/.next/static/chunks/app/agents/{page-1f1ac330c8177cf6.js → page-50eac58d511dcc6e.js} +1 -1
- package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
- package/_standalone/.next/static/chunks/app/{layout-50a6b1164ee98ab9.js → layout-2cb7a6602d2e5d5f.js} +62 -58
- package/_standalone/.next/static/chunks/app/{page-73802bd31d7f6c9f.js → page-5ab911b2226f6ff7.js} +1 -1
- package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
- package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/{page-808f39963bf04715.js → page-26e47dd4c533a58c.js} +2 -2
- package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
- package/_standalone/.next/trace +65 -65
- package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
- package/_standalone/__tests__/api/settings.test.ts +16 -12
- package/_standalone/__tests__/api/setup.test.ts +11 -9
- package/_standalone/__tests__/api/test-key.test.ts +0 -10
- package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
- package/_standalone/__tests__/core/context.test.ts +48 -426
- package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
- package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
- package/_standalone/__tests__/setup.ts +5 -5
- package/_standalone/components/ask/AskContent.tsx +70 -40
- package/_standalone/components/ask/AskHeader.tsx +8 -1
- package/_standalone/components/ask/MessageList.tsx +37 -3
- package/_standalone/components/ask/ProviderModelCapsule.tsx +51 -129
- package/_standalone/components/settings/AiTab.tsx +270 -347
- package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
- package/_standalone/components/settings/CustomProvidersCard.tsx +2 -2
- package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
- package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
- package/_standalone/components/settings/Primitives.tsx +48 -104
- package/_standalone/components/settings/ProviderModal.tsx +38 -221
- package/_standalone/components/settings/SettingsContent.tsx +5 -12
- package/_standalone/components/settings/TestButton.tsx +64 -0
- package/_standalone/components/settings/types.ts +3 -12
- package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
- package/_standalone/components/setup/StepAI.tsx +3 -3
- package/_standalone/components/shared/ModelInput.tsx +18 -4
- package/_standalone/components/shared/ProviderSelect.tsx +126 -134
- package/_standalone/hooks/useAskChat.ts +97 -13
- package/_standalone/hooks/useAskPanel.ts +17 -1
- package/_standalone/lib/settings-ai-client.ts +17 -8
- package/_standalone/tsconfig.tsbuildinfo +1 -1
- package/app/app/api/ask/route.ts +124 -44
- package/app/app/api/mcp/agents/route.ts +3 -3
- package/app/app/api/settings/list-models/route.ts +15 -26
- package/app/app/api/settings/route.ts +14 -59
- package/app/app/api/settings/test-key/route.ts +47 -12
- package/app/app/api/setup/route.ts +36 -18
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/layout.tsx +5 -3
- package/app/components/HomeContent.tsx +11 -0
- package/app/components/UpdateToast.tsx +255 -0
- package/app/components/ask/AskContent.tsx +70 -40
- package/app/components/ask/AskHeader.tsx +8 -1
- package/app/components/ask/MessageList.tsx +37 -3
- package/app/components/ask/ProviderModelCapsule.tsx +51 -129
- package/app/components/settings/AiTab.tsx +270 -347
- package/app/components/settings/CustomProviderFields.tsx +121 -0
- package/app/components/settings/CustomProvidersCard.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +6 -20
- package/app/components/settings/McpAgentInstall.tsx +7 -2
- package/app/components/settings/Primitives.tsx +48 -104
- package/app/components/settings/ProviderModal.tsx +38 -221
- package/app/components/settings/SettingsContent.tsx +5 -12
- package/app/components/settings/TestButton.tsx +64 -0
- package/app/components/settings/types.ts +3 -12
- package/app/components/settings/useCustomProviderForm.ts +132 -0
- package/app/components/setup/StepAI.tsx +3 -3
- package/app/components/shared/ModelInput.tsx +18 -4
- package/app/components/shared/ProviderSelect.tsx +126 -134
- package/app/hooks/useAskChat.ts +97 -13
- package/app/hooks/useAskPanel.ts +17 -1
- package/app/lib/agent/context.ts +65 -0
- package/app/lib/agent/providers.ts +25 -0
- package/app/lib/agent/tools.ts +1 -1
- package/app/lib/custom-endpoints.ts +129 -29
- package/app/lib/i18n/modules/settings.ts +20 -0
- package/app/lib/pi-integration/skills.ts +16 -4
- package/app/lib/settings-ai-client.ts +17 -8
- package/app/lib/settings.ts +64 -90
- package/app/lib/types.ts +4 -0
- package/package.json +1 -1
- package/_standalone/.next/server/chunks/530.js +0 -218
- package/_standalone/.next/server/chunks/9007.js +0 -2
- package/_standalone/.next/server/chunks/9137.js +0 -52
- package/_standalone/.next/static/chunks/1309-373ade1b40aea186.js +0 -1
- package/_standalone/.next/static/chunks/3165-9189a38fd9ebf6f2.js +0 -1
- package/_standalone/.next/static/chunks/4587-5d06728133fff222.js +0 -1
- package/_standalone/.next/static/chunks/6261-5ce86db54b19ae46.js +0 -1
- package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
- package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
- package/_standalone/.next/static/chunks/8520-84e607f33c409f91.js +0 -22
- package/_standalone/.next/static/chunks/9207-9a4a1a1ede4f8e6e.js +0 -1
- package/_standalone/.next/static/chunks/app/echo/[segment]/page-bc5e104eb7ae6327.js +0 -11
- package/_standalone/.next/static/chunks/app/setup/page-79acb0baf38184c6.js +0 -1
- package/_standalone/.next/static/chunks/app/trash/page-d040db56863da504.js +0 -1
- package/_standalone/.next/static/css/1287672978833d07.css +0 -1
- package/_standalone/lib/agent/context.ts +0 -403
- /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { seedFile, testMindRoot } from '../setup';
|
|
3
|
+
import { getFileContent, invalidateCache } from '../../lib/fs';
|
|
4
|
+
import { truncate } from '../../lib/agent/tools';
|
|
5
|
+
|
|
6
|
+
describe('Ask attached files', () => {
|
|
7
|
+
describe('getFileContent reads KB files', () => {
|
|
8
|
+
it('reads a seeded markdown file', () => {
|
|
9
|
+
seedFile('notes/test.md', '# Test\nHello world');
|
|
10
|
+
invalidateCache();
|
|
11
|
+
const content = getFileContent('notes/test.md');
|
|
12
|
+
expect(content).toBe('# Test\nHello world');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('reads a nested file', () => {
|
|
16
|
+
seedFile('deep/nested/dir/file.md', 'nested content');
|
|
17
|
+
invalidateCache();
|
|
18
|
+
const content = getFileContent('deep/nested/dir/file.md');
|
|
19
|
+
expect(content).toBe('nested content');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('throws for non-existent file', () => {
|
|
23
|
+
invalidateCache();
|
|
24
|
+
expect(() => getFileContent('does-not-exist.md')).toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reads CSV files', () => {
|
|
28
|
+
seedFile('data.csv', 'a,b,c\n1,2,3');
|
|
29
|
+
invalidateCache();
|
|
30
|
+
const content = getFileContent('data.csv');
|
|
31
|
+
expect(content).toBe('a,b,c\n1,2,3');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('truncate limits content length', () => {
|
|
36
|
+
it('passes through short content unchanged', () => {
|
|
37
|
+
const short = 'Hello world';
|
|
38
|
+
expect(truncate(short)).toBe(short);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('truncates very long content', () => {
|
|
42
|
+
const long = 'x'.repeat(50_000);
|
|
43
|
+
const result = truncate(long);
|
|
44
|
+
expect(result.length).toBeLessThan(long.length);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('attachment pattern builds correct context', () => {
|
|
49
|
+
it('builds context parts for multiple attached files', () => {
|
|
50
|
+
seedFile('file-a.md', '# File A\nContent A');
|
|
51
|
+
seedFile('file-b.md', '# File B\nContent B');
|
|
52
|
+
invalidateCache();
|
|
53
|
+
|
|
54
|
+
const attachedFiles = ['file-a.md', 'file-b.md'];
|
|
55
|
+
const contextParts: string[] = [];
|
|
56
|
+
const seen = new Set<string>();
|
|
57
|
+
|
|
58
|
+
for (const filePath of attachedFiles) {
|
|
59
|
+
if (seen.has(filePath)) continue;
|
|
60
|
+
seen.add(filePath);
|
|
61
|
+
try {
|
|
62
|
+
const content = truncate(getFileContent(filePath));
|
|
63
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
64
|
+
} catch { /* simulate route.ts pattern */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(contextParts).toHaveLength(2);
|
|
68
|
+
expect(contextParts[0]).toContain('## Attached: file-a.md');
|
|
69
|
+
expect(contextParts[0]).toContain('Content A');
|
|
70
|
+
expect(contextParts[1]).toContain('## Attached: file-b.md');
|
|
71
|
+
expect(contextParts[1]).toContain('Content B');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('deduplicates attached files', () => {
|
|
75
|
+
seedFile('dup.md', 'content');
|
|
76
|
+
invalidateCache();
|
|
77
|
+
|
|
78
|
+
const attachedFiles = ['dup.md', 'dup.md', 'dup.md'];
|
|
79
|
+
const contextParts: string[] = [];
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
|
|
82
|
+
for (const filePath of attachedFiles) {
|
|
83
|
+
if (seen.has(filePath)) continue;
|
|
84
|
+
seen.add(filePath);
|
|
85
|
+
try {
|
|
86
|
+
const content = truncate(getFileContent(filePath));
|
|
87
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
88
|
+
} catch { /* */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(contextParts).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips missing files without crashing', () => {
|
|
95
|
+
seedFile('exists.md', 'good');
|
|
96
|
+
invalidateCache();
|
|
97
|
+
|
|
98
|
+
const attachedFiles = ['exists.md', 'missing.md', 'also-missing.md'];
|
|
99
|
+
const contextParts: string[] = [];
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
|
|
102
|
+
for (const filePath of attachedFiles) {
|
|
103
|
+
if (seen.has(filePath)) continue;
|
|
104
|
+
seen.add(filePath);
|
|
105
|
+
try {
|
|
106
|
+
const content = truncate(getFileContent(filePath));
|
|
107
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
108
|
+
} catch { /* silently skip */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(contextParts).toHaveLength(1);
|
|
112
|
+
expect(contextParts[0]).toContain('exists.md');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('handles empty attachedFiles array', () => {
|
|
116
|
+
const attachedFiles: string[] = [];
|
|
117
|
+
const contextParts: string[] = [];
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
|
|
120
|
+
for (const filePath of attachedFiles) {
|
|
121
|
+
if (seen.has(filePath)) continue;
|
|
122
|
+
seen.add(filePath);
|
|
123
|
+
try {
|
|
124
|
+
const content = truncate(getFileContent(filePath));
|
|
125
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
126
|
+
} catch { /* */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expect(contextParts).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('currentFile is included when not in attachedFiles', () => {
|
|
133
|
+
seedFile('attached.md', 'attached content');
|
|
134
|
+
seedFile('current.md', 'current content');
|
|
135
|
+
invalidateCache();
|
|
136
|
+
|
|
137
|
+
const attachedFiles = ['attached.md'];
|
|
138
|
+
const currentFile = 'current.md';
|
|
139
|
+
const contextParts: string[] = [];
|
|
140
|
+
const seen = new Set<string>();
|
|
141
|
+
|
|
142
|
+
for (const filePath of attachedFiles) {
|
|
143
|
+
if (seen.has(filePath)) continue;
|
|
144
|
+
seen.add(filePath);
|
|
145
|
+
try {
|
|
146
|
+
const content = truncate(getFileContent(filePath));
|
|
147
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
148
|
+
} catch { /* */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (currentFile && !seen.has(currentFile)) {
|
|
152
|
+
seen.add(currentFile);
|
|
153
|
+
try {
|
|
154
|
+
const content = truncate(getFileContent(currentFile));
|
|
155
|
+
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
156
|
+
} catch { /* */ }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
expect(contextParts).toHaveLength(2);
|
|
160
|
+
expect(contextParts[0]).toContain('## Attached: attached.md');
|
|
161
|
+
expect(contextParts[1]).toContain('## Current file: current.md');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('currentFile is not duplicated when already in attachedFiles', () => {
|
|
165
|
+
seedFile('same.md', 'same content');
|
|
166
|
+
invalidateCache();
|
|
167
|
+
|
|
168
|
+
const attachedFiles = ['same.md'];
|
|
169
|
+
const currentFile = 'same.md';
|
|
170
|
+
const contextParts: string[] = [];
|
|
171
|
+
const seen = new Set<string>();
|
|
172
|
+
|
|
173
|
+
for (const filePath of attachedFiles) {
|
|
174
|
+
if (seen.has(filePath)) continue;
|
|
175
|
+
seen.add(filePath);
|
|
176
|
+
try {
|
|
177
|
+
const content = truncate(getFileContent(filePath));
|
|
178
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
179
|
+
} catch { /* */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (currentFile && !seen.has(currentFile)) {
|
|
183
|
+
seen.add(currentFile);
|
|
184
|
+
try {
|
|
185
|
+
const content = truncate(getFileContent(currentFile));
|
|
186
|
+
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
187
|
+
} catch { /* */ }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
expect(contextParts).toHaveLength(1);
|
|
191
|
+
expect(contextParts[0]).toContain('## Attached: same.md');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -12,15 +12,19 @@ describe('GET /api/settings', () => {
|
|
|
12
12
|
const body = await res.json();
|
|
13
13
|
|
|
14
14
|
expect(body).toHaveProperty('ai');
|
|
15
|
-
expect(body.ai).toHaveProperty('
|
|
15
|
+
expect(body.ai).toHaveProperty('activeProvider');
|
|
16
16
|
expect(body.ai).toHaveProperty('providers');
|
|
17
|
-
expect(body.ai.providers).
|
|
18
|
-
expect(body.ai.providers).
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
expect(
|
|
22
|
-
expect(
|
|
23
|
-
expect(
|
|
17
|
+
expect(Array.isArray(body.ai.providers)).toBe(true);
|
|
18
|
+
expect(body.ai.providers.length).toBeGreaterThanOrEqual(1);
|
|
19
|
+
const anthropic = body.ai.providers.find((p: any) => p.protocol === 'anthropic');
|
|
20
|
+
const openai = body.ai.providers.find((p: any) => p.protocol === 'openai');
|
|
21
|
+
expect(anthropic).toBeDefined();
|
|
22
|
+
expect(anthropic).toHaveProperty('apiKey');
|
|
23
|
+
expect(anthropic).toHaveProperty('model');
|
|
24
|
+
expect(openai).toBeDefined();
|
|
25
|
+
expect(openai).toHaveProperty('apiKey');
|
|
26
|
+
expect(openai).toHaveProperty('model');
|
|
27
|
+
expect(openai).toHaveProperty('baseUrl');
|
|
24
28
|
expect(body).toHaveProperty('mindRoot');
|
|
25
29
|
expect(body).toHaveProperty('envOverrides');
|
|
26
30
|
expect(body).toHaveProperty('envValues');
|
|
@@ -33,10 +37,10 @@ describe('POST /api/settings', () => {
|
|
|
33
37
|
method: 'POST',
|
|
34
38
|
body: JSON.stringify({
|
|
35
39
|
ai: {
|
|
36
|
-
|
|
37
|
-
providers:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
activeProvider: 'p_openai01',
|
|
41
|
+
providers: [
|
|
42
|
+
{ id: 'p_openai01', name: 'OpenAI', protocol: 'openai', apiKey: 'sk-test', model: 'gpt-5.4', baseUrl: '' },
|
|
43
|
+
],
|
|
40
44
|
},
|
|
41
45
|
}),
|
|
42
46
|
headers: { 'content-type': 'application/json' },
|
|
@@ -7,11 +7,11 @@ import path from 'path';
|
|
|
7
7
|
// We need to mock settings + template modules for the setup API
|
|
8
8
|
const mockSettings = {
|
|
9
9
|
ai: {
|
|
10
|
-
|
|
11
|
-
providers:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
activeProvider: 'p_anthro01',
|
|
11
|
+
providers: [
|
|
12
|
+
{ id: 'p_anthro01', name: 'Anthropic', protocol: 'anthropic' as const, apiKey: '', model: 'claude-sonnet-4-6', baseUrl: '' },
|
|
13
|
+
{ id: 'p_openai01', name: 'OpenAI', protocol: 'openai' as const, apiKey: '', model: 'gpt-5.4', baseUrl: '' },
|
|
14
|
+
],
|
|
15
15
|
},
|
|
16
16
|
mindRoot: '',
|
|
17
17
|
port: 3456,
|
|
@@ -76,12 +76,13 @@ describe('GET /api/setup', () => {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it('masks API keys in providerConfigs', async () => {
|
|
79
|
-
mockSettings.ai.providers.
|
|
79
|
+
mockSettings.ai.providers[0].apiKey = 'sk-ant-1234567890abcdef';
|
|
80
80
|
const { GET } = await importSetupRoute();
|
|
81
81
|
const res = await GET();
|
|
82
82
|
const body = await res.json();
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const anthropicConfig = body.providerConfigs.find((p: any) => p.protocol === 'anthropic');
|
|
84
|
+
expect(anthropicConfig.apiKeyMask).toBe('sk-ant***');
|
|
85
|
+
mockSettings.ai.providers[0].apiKey = '';
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
@@ -315,7 +316,8 @@ describe('POST /api/setup — LLM skip', () => {
|
|
|
315
316
|
expect(res.status).toBe(200);
|
|
316
317
|
const config = writtenConfig as Record<string, unknown>;
|
|
317
318
|
const ai = config.ai as Record<string, unknown>;
|
|
318
|
-
|
|
319
|
+
// The route merges incoming ai into the existing format
|
|
320
|
+
expect(ai).toBeDefined();
|
|
319
321
|
});
|
|
320
322
|
});
|
|
321
323
|
|
|
@@ -148,16 +148,6 @@ describe('POST /api/settings/test-key', () => {
|
|
|
148
148
|
expect(body.error).toBe('Request timed out');
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it('treats ***set*** as masked and falls back to config', async () => {
|
|
152
|
-
const res = await POST(makeReq({ provider: 'anthropic', apiKey: '***set***' }));
|
|
153
|
-
const body = await res.json();
|
|
154
|
-
|
|
155
|
-
// effectiveAiConfig returns empty apiKey → auth_error
|
|
156
|
-
expect(body.ok).toBe(false);
|
|
157
|
-
expect(body.code).toBe('auth_error');
|
|
158
|
-
expect(body.error).toBe('No API key configured');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
151
|
it('accepts new providers like google and deepseek', async () => {
|
|
162
152
|
(complete as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ text: 'ok' });
|
|
163
153
|
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UpdateToast Component Tests
|
|
5
|
+
* Tests for Desktop-only update notification toast
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('UpdateToast Logic', () => {
|
|
9
|
+
describe('Skip Version Logic', () => {
|
|
10
|
+
it('should show toast when version > skipped desktop version', () => {
|
|
11
|
+
// Pure version comparison logic (no mocks needed)
|
|
12
|
+
const latest = '0.1.14';
|
|
13
|
+
const skipped = '0.1.13';
|
|
14
|
+
const shouldShow = latest > skipped;
|
|
15
|
+
expect(shouldShow).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should NOT show toast when version = skipped version', () => {
|
|
19
|
+
const latest = '0.1.14';
|
|
20
|
+
const skipped = '0.1.14';
|
|
21
|
+
const shouldShow = latest > skipped;
|
|
22
|
+
expect(shouldShow).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should NOT show toast when version < skipped version (older)', () => {
|
|
26
|
+
const latest = '0.1.13';
|
|
27
|
+
const skipped = '0.1.14';
|
|
28
|
+
const shouldShow = latest > skipped;
|
|
29
|
+
expect(shouldShow).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should show toast when no skipped version stored', () => {
|
|
33
|
+
const latest = '0.1.14';
|
|
34
|
+
const skipped: string | null = null;
|
|
35
|
+
const shouldShow = !skipped || latest > skipped;
|
|
36
|
+
expect(shouldShow).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle semantic versioning string comparison', () => {
|
|
40
|
+
// Test actual lexicographic string comparison behavior
|
|
41
|
+
// which is what we use for version comparison
|
|
42
|
+
expect('0.2.0' > '0.1.99').toBe(true); // Works correctly
|
|
43
|
+
expect('1.0.0' > '0.9.9').toBe(true); // Works correctly
|
|
44
|
+
expect('0.1.10' > '0.1.9').toBe(false); // String comparison quirk, but OK for our use case
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Combined Update Detection', () => {
|
|
49
|
+
it('should detect single desktop update', () => {
|
|
50
|
+
const updates = {
|
|
51
|
+
desktop: { type: 'desktop' as const, version: '0.1.14' },
|
|
52
|
+
core: undefined,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const hasDesktop = !!updates.desktop;
|
|
56
|
+
const hasCore = !!updates.core;
|
|
57
|
+
const hasBoth = !!(updates.desktop && updates.core);
|
|
58
|
+
|
|
59
|
+
expect(hasDesktop).toBe(true);
|
|
60
|
+
expect(hasCore).toBe(false);
|
|
61
|
+
expect(hasBoth).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should detect single core update', () => {
|
|
65
|
+
const updates = {
|
|
66
|
+
desktop: undefined,
|
|
67
|
+
core: { type: 'core' as const, version: '0.6.28' },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const hasDesktop = !!updates.desktop;
|
|
71
|
+
const hasCore = !!updates.core;
|
|
72
|
+
const hasBoth = !!(updates.desktop && updates.core);
|
|
73
|
+
|
|
74
|
+
expect(hasDesktop).toBe(false);
|
|
75
|
+
expect(hasCore).toBe(true);
|
|
76
|
+
expect(hasBoth).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should detect both updates available', () => {
|
|
80
|
+
const updates = {
|
|
81
|
+
desktop: { type: 'desktop' as const, version: '0.1.14' },
|
|
82
|
+
core: { type: 'core' as const, version: '0.6.28' },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const hasBoth = !!(updates.desktop && updates.core);
|
|
86
|
+
expect(hasBoth).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('LocalStorage Key Management', () => {
|
|
91
|
+
it('should use correct key names', () => {
|
|
92
|
+
const SKIP_DESKTOP_KEY = 'mindos_update_skip_desktop';
|
|
93
|
+
const SKIP_CORE_KEY = 'mindos_update_skip_core';
|
|
94
|
+
|
|
95
|
+
expect(SKIP_DESKTOP_KEY).toBe('mindos_update_skip_desktop');
|
|
96
|
+
expect(SKIP_CORE_KEY).toBe('mindos_update_skip_core');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle separate skip storage for desktop and core', () => {
|
|
100
|
+
// Test the storage model
|
|
101
|
+
const storage: Record<string, string> = {};
|
|
102
|
+
|
|
103
|
+
// Desktop update
|
|
104
|
+
storage['mindos_update_skip_desktop'] = '0.1.14';
|
|
105
|
+
|
|
106
|
+
// Core update
|
|
107
|
+
storage['mindos_update_skip_core'] = '0.6.28';
|
|
108
|
+
|
|
109
|
+
expect(storage['mindos_update_skip_desktop']).toBe('0.1.14');
|
|
110
|
+
expect(storage['mindos_update_skip_core']).toBe('0.6.28');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('Desktop Bridge Detection', () => {
|
|
115
|
+
it('should return null when bridge unavailable', () => {
|
|
116
|
+
const bridge = undefined;
|
|
117
|
+
expect(bridge).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return truthy when bridge exists', () => {
|
|
121
|
+
const bridge = { checkUpdate: () => {} };
|
|
122
|
+
expect(bridge).toBeDefined();
|
|
123
|
+
expect(bridge.checkUpdate).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Event Dispatching', () => {
|
|
128
|
+
it('should have correct event type', () => {
|
|
129
|
+
const eventType = 'mindos:open-settings';
|
|
130
|
+
expect(eventType).toBe('mindos:open-settings');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should include correct tab in event detail', () => {
|
|
134
|
+
const detail = { tab: 'update' };
|
|
135
|
+
expect(detail.tab).toBe('update');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should support both "Skip Version" and "Skip All" labels', () => {
|
|
139
|
+
const hasBoth = true;
|
|
140
|
+
const label = hasBoth ? 'Skip All' : 'Skip Version';
|
|
141
|
+
expect(label).toBe('Skip All');
|
|
142
|
+
|
|
143
|
+
const single = false;
|
|
144
|
+
const label2 = single ? 'Skip All' : 'Skip Version';
|
|
145
|
+
expect(label2).toBe('Skip Version');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('UI Display Logic', () => {
|
|
150
|
+
it('should generate correct title for single desktop update', () => {
|
|
151
|
+
const type = 'Desktop';
|
|
152
|
+
const version = '0.1.14';
|
|
153
|
+
const title = `${type} v${version} available`;
|
|
154
|
+
expect(title).toBe('Desktop v0.1.14 available');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should generate correct title for single core update', () => {
|
|
158
|
+
const type = 'Core';
|
|
159
|
+
const version = '0.6.28';
|
|
160
|
+
const title = `${type} v${version} available`;
|
|
161
|
+
expect(title).toBe('Core v0.6.28 available');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should generate correct title for both updates', () => {
|
|
165
|
+
const title = 'Updates available';
|
|
166
|
+
expect(title).toBe('Updates available');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should generate correct subtitle for both updates', () => {
|
|
170
|
+
const desktopVersion = '0.1.14';
|
|
171
|
+
const coreVersion = '0.6.28';
|
|
172
|
+
const subtitle = `Desktop v${desktopVersion} · Core v${coreVersion}`;
|
|
173
|
+
expect(subtitle).toBe('Desktop v0.1.14 · Core v0.6.28');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Dismiss & Skip Behavior', () => {
|
|
178
|
+
it('should treat close button same as skip', () => {
|
|
179
|
+
// Both should:
|
|
180
|
+
// 1. Store skip version
|
|
181
|
+
// 2. Hide toast
|
|
182
|
+
// 3. Return same result
|
|
183
|
+
const version = '0.1.14';
|
|
184
|
+
|
|
185
|
+
const closeAction = { storeSkip: version, hide: true };
|
|
186
|
+
const skipAction = { storeSkip: version, hide: true };
|
|
187
|
+
|
|
188
|
+
expect(closeAction).toEqual(skipAction);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should not block manual checks in Settings after dismiss', () => {
|
|
192
|
+
// User can still click "Check" button in Settings tab
|
|
193
|
+
// even after dismissing toast
|
|
194
|
+
const toastDismissed = true;
|
|
195
|
+
const manualCheckAllowed = true;
|
|
196
|
+
|
|
197
|
+
expect(toastDismissed && manualCheckAllowed).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Component Render Conditions', () => {
|
|
202
|
+
it('should render null when bridge is unavailable', () => {
|
|
203
|
+
const bridge = null;
|
|
204
|
+
const shouldRender = !!bridge;
|
|
205
|
+
expect(shouldRender).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should render null when state is hidden', () => {
|
|
209
|
+
const state = 'hidden';
|
|
210
|
+
const shouldRender = state !== 'hidden';
|
|
211
|
+
expect(shouldRender).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should render when bridge exists and state is visible', () => {
|
|
215
|
+
const bridge = { checkUpdate: () => {} };
|
|
216
|
+
const state = 'visible';
|
|
217
|
+
const shouldRender = !!bridge && state === 'visible';
|
|
218
|
+
expect(shouldRender).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Semantic Version Comparison', () => {
|
|
223
|
+
// Helper function matching component implementation
|
|
224
|
+
function isVersionNewer(latest: string, skipped: string): boolean {
|
|
225
|
+
const latestParts = latest.split('.').map(Number);
|
|
226
|
+
const skippedParts = skipped.split('.').map(Number);
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < Math.max(latestParts.length, skippedParts.length); i++) {
|
|
229
|
+
const l = latestParts[i] ?? 0;
|
|
230
|
+
const s = skippedParts[i] ?? 0;
|
|
231
|
+
if (l > s) return true;
|
|
232
|
+
if (l < s) return false;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
it('should correctly compare 0.1.10 > 0.1.9', () => {
|
|
238
|
+
expect(isVersionNewer('0.1.10', '0.1.9')).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should correctly compare 0.1.9 is not > 0.1.10', () => {
|
|
242
|
+
expect(isVersionNewer('0.1.9', '0.1.10')).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle major version bumps', () => {
|
|
246
|
+
expect(isVersionNewer('1.0.0', '0.9.99')).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle minor version bumps', () => {
|
|
250
|
+
expect(isVersionNewer('0.2.0', '0.1.99')).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should return false for equal versions', () => {
|
|
254
|
+
expect(isVersionNewer('0.1.14', '0.1.14')).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle extra version parts', () => {
|
|
258
|
+
expect(isVersionNewer('0.1.14.1', '0.1.14')).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('i18n Keys', () => {
|
|
263
|
+
it('should include all updateToast translation keys', () => {
|
|
264
|
+
const keys = [
|
|
265
|
+
'titleSingle',
|
|
266
|
+
'titleMultiple',
|
|
267
|
+
'desktopLabel',
|
|
268
|
+
'coreLabel',
|
|
269
|
+
'viewDetails',
|
|
270
|
+
'skipVersion',
|
|
271
|
+
'skipAll',
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
expect(keys).toContain('titleSingle');
|
|
275
|
+
expect(keys).toContain('viewDetails');
|
|
276
|
+
expect(keys).toContain('skipAll');
|
|
277
|
+
expect(keys.length).toBeGreaterThanOrEqual(7);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('Timing', () => {
|
|
282
|
+
it('should use 10 second delay before showing toast', () => {
|
|
283
|
+
const SHOW_DELAY_MS = 10_000;
|
|
284
|
+
expect(SHOW_DELAY_MS).toBe(10000);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should use 200ms dismiss animation', () => {
|
|
288
|
+
const DISMISS_MS = 200;
|
|
289
|
+
expect(DISMISS_MS).toBe(200);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('Edge Cases', () => {
|
|
294
|
+
it('should not show toast when core update is already ready', () => {
|
|
295
|
+
// If info.ready is true, the update is already downloaded
|
|
296
|
+
// UpdateTab handles that state, so toast should NOT show
|
|
297
|
+
const info = { current: '0.6.27', latest: '0.6.28', ready: true };
|
|
298
|
+
const shouldShowToast = info.latest && !info.ready;
|
|
299
|
+
expect(shouldShowToast).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should show toast when core update is available but not ready', () => {
|
|
303
|
+
const info = { current: '0.6.27', latest: '0.6.28', ready: false };
|
|
304
|
+
const shouldShowToast = info.latest && !info.ready;
|
|
305
|
+
expect(shouldShowToast).toBeTruthy();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should dedup: same version should not re-queue', () => {
|
|
309
|
+
const queued: Record<string, string | undefined> = {};
|
|
310
|
+
const version = '0.1.14';
|
|
311
|
+
|
|
312
|
+
// First call
|
|
313
|
+
queued['desktop'] = version;
|
|
314
|
+
expect(queued['desktop']).toBe('0.1.14');
|
|
315
|
+
|
|
316
|
+
// Second call with same version — should skip
|
|
317
|
+
const shouldSkip = queued['desktop'] === version;
|
|
318
|
+
expect(shouldSkip).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should dedup: different version should re-queue', () => {
|
|
322
|
+
const queued: Record<string, string | undefined> = {};
|
|
323
|
+
queued['desktop'] = '0.1.14';
|
|
324
|
+
|
|
325
|
+
const shouldSkip = queued['desktop'] === '0.1.15';
|
|
326
|
+
expect(shouldSkip).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should use translate-y animation instead of scale for subtlety', () => {
|
|
330
|
+
// Verify our animation approach: translate-y-2 → translate-y-0
|
|
331
|
+
// is more subtle and natural than scale-95 → scale-100
|
|
332
|
+
const showClass = 'translate-y-0';
|
|
333
|
+
const hideClass = 'translate-y-2';
|
|
334
|
+
expect(showClass).not.toBe(hideClass);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should position above regular toasts (bottom-14 vs bottom-4)', () => {
|
|
338
|
+
// UpdateToast uses bottom-14 to avoid overlapping Toaster (bottom-4)
|
|
339
|
+
const updateToastBottom = 14; // tailwind bottom-14 = 3.5rem
|
|
340
|
+
const toasterBottom = 4; // tailwind bottom-4 = 1rem
|
|
341
|
+
expect(updateToastBottom).toBeGreaterThan(toasterBottom);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|