) =>
task.status === TaskStatus.COMPLETED ||
task.status === TaskStatus.FAILED
).length;
- const taskProgress = (
- ((finishedTask || 0) / (taskRunning?.length || 0)) *
- 100
- ).toFixed(2);
- setProgressValue(activeTaskId as string, Number(taskProgress));
+ const denominator = taskRunning?.length ?? 0;
+ const taskProgress =
+ denominator === 0
+ ? 0
+ : ((finishedTask || 0) / denominator) * 100;
+ setProgressValue(
+ activeTaskId as string,
+ Number(taskProgress.toFixed(2))
+ );
},
removeTask(taskId: string) {
// Clean up any pending auto-confirm timers when removing a task
diff --git a/test/unit/electron/fileReader.test.ts b/test/unit/electron/fileReader.test.ts
new file mode 100644
index 000000000..5b9592c8d
--- /dev/null
+++ b/test/unit/electron/fileReader.test.ts
@@ -0,0 +1,67 @@
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+/**
+ * Tests for the escapeHtml utility added to electron/main/fileReader.ts.
+ *
+ * Because fileReader.ts imports Electron-only modules (BrowserWindow, app)
+ * that are unavailable in the jsdom vitest environment, we replicate the
+ * pure function here and verify its behaviour independently.
+ */
+import { describe, expect, it } from 'vitest';
+
+// Replicate the escapeHtml function as defined in electron/main/fileReader.ts
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+describe('escapeHtml (fileReader)', () => {
+ it('escapes angle brackets', () => {
+ expect(escapeHtml('')).toBe(
+ '<script>alert(1)</script>'
+ );
+ });
+
+ it('escapes ampersands', () => {
+ expect(escapeHtml('a & b')).toBe('a & b');
+ });
+
+ it('escapes double quotes', () => {
+ expect(escapeHtml('"hello"')).toBe('"hello"');
+ });
+
+ it('escapes single quotes', () => {
+ expect(escapeHtml("it's")).toBe('it's');
+ });
+
+ it('handles strings with no special characters', () => {
+ expect(escapeHtml('hello world')).toBe('hello world');
+ });
+
+ it('handles empty string', () => {
+ expect(escapeHtml('')).toBe('');
+ });
+
+ it('escapes all special characters in one string', () => {
+ const input = ``;
+ const expected =
+ '<div class="test" data-name='foo & bar'>';
+ expect(escapeHtml(input)).toBe(expected);
+ });
+});
diff --git a/test/unit/lib/securityFixes.test.ts b/test/unit/lib/securityFixes.test.ts
new file mode 100644
index 000000000..93e7d8f87
--- /dev/null
+++ b/test/unit/lib/securityFixes.test.ts
@@ -0,0 +1,43 @@
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+import { generateUniqueId, hasStackKeys } from '@/lib/index';
+import { describe, expect, it, vi } from 'vitest';
+
+describe('generateUniqueId', () => {
+ it('returns a 32-character hex string', () => {
+ const id = generateUniqueId();
+ expect(id).toMatch(/^[0-9a-f]{32}$/);
+ });
+
+ it('produces unique values on successive calls', () => {
+ const ids = new Set(Array.from({ length: 100 }, () => generateUniqueId()));
+ expect(ids.size).toBe(100);
+ });
+
+ it('uses crypto.getRandomValues instead of Math.random', () => {
+ const spy = vi.spyOn(crypto, 'getRandomValues');
+ generateUniqueId();
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
+
+describe('hasStackKeys', () => {
+ it('does not reference VITE_STACK_SECRET_SERVER_KEY', () => {
+ // Read the source of hasStackKeys to ensure the secret key is removed
+ const fnSource = hasStackKeys.toString();
+ expect(fnSource).not.toContain('VITE_STACK_SECRET_SERVER_KEY');
+ });
+});
diff --git a/test/unit/store/chatStore-divisionByZero.test.ts b/test/unit/store/chatStore-divisionByZero.test.ts
new file mode 100644
index 000000000..c1727c5d1
--- /dev/null
+++ b/test/unit/store/chatStore-divisionByZero.test.ts
@@ -0,0 +1,63 @@
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+/**
+ * Test that task progress calculation guards against division by zero.
+ *
+ * This test verifies the logic pattern used in chatStore.ts where
+ * taskProgress is calculated as (finishedTask / taskRunning.length) * 100.
+ * When taskRunning is empty, the denominator is 0 and would produce NaN
+ * or Infinity without the guard.
+ */
+import { describe, expect, it } from 'vitest';
+
+function calculateTaskProgress(
+ finishedTask: number | undefined,
+ taskRunningLength: number | undefined
+): number {
+ const denominator = taskRunningLength ?? 0;
+ const taskProgress =
+ denominator === 0
+ ? 0
+ : ((finishedTask || 0) / denominator) * 100;
+ return Number(taskProgress.toFixed(2));
+}
+
+describe('task progress division-by-zero guard', () => {
+ it('returns 0 when taskRunning is empty (denominator = 0)', () => {
+ expect(calculateTaskProgress(0, 0)).toBe(0);
+ });
+
+ it('returns 0 when taskRunning is undefined', () => {
+ expect(calculateTaskProgress(5, undefined)).toBe(0);
+ });
+
+ it('calculates correct percentage for valid inputs', () => {
+ expect(calculateTaskProgress(3, 10)).toBe(30);
+ });
+
+ it('returns 0 when finishedTask is undefined and denominator is 0', () => {
+ expect(calculateTaskProgress(undefined, 0)).toBe(0);
+ });
+
+ it('handles 100% completion', () => {
+ expect(calculateTaskProgress(5, 5)).toBe(100);
+ });
+
+ it('never returns NaN or Infinity', () => {
+ const result = calculateTaskProgress(0, 0);
+ expect(Number.isFinite(result)).toBe(true);
+ expect(Number.isNaN(result)).toBe(false);
+ });
+});