diff --git a/src/phoenix/test/test-git-detection.js b/src/phoenix/test/test-git-detection.js
new file mode 100644
index 000000000..976ca48e2
--- /dev/null
+++ b/src/phoenix/test/test-git-detection.js
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { JSDOM } from 'jsdom';
+
+// Function to test
+async function hasGitDirectory(items) {
+ // Case 1: Single Puter path
+ if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) {
+ const stat = await global.puter.fs.stat(items);
+ if (stat.is_dir) {
+ const files = await global.puter.fs.readdir(items);
+ return files.some(file => file.name === '.git' && file.is_dir);
+ }
+ return false;
+ }
+
+ // Case 2: Array of Puter items
+ if (Array.isArray(items) && items[0]?.uid) {
+ return items.some(item => item.name === '.git' && item.is_dir);
+ }
+
+ // Case 3: Local items (DataTransferItems)
+ if (Array.isArray(items)) {
+ for (let item of items) {
+ if (item.fullPath?.includes('/.git/') ||
+ item.path?.includes('/.git/') ||
+ item.filepath?.includes('/.git/')) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+describe('hasGitDirectory', () => {
+ // Mock puter.fs for testing
+ const mockPuterFS = {
+ stat: async (path) => ({
+ is_dir: path.endsWith('dir')
+ }),
+ readdir: async (path) => {
+ if (path === '/path/to/git/dir') {
+ return [
+ { name: '.git', is_dir: true },
+ { name: 'src', is_dir: true }
+ ];
+ }
+ return [
+ { name: 'src', is_dir: true },
+ { name: 'test', is_dir: true }
+ ];
+ }
+ };
+
+ beforeEach(() => {
+ // Set up global puter object before each test
+ global.puter = { fs: mockPuterFS };
+ });
+
+ afterEach(() => {
+ // Clean up global puter object after each test
+ delete global.puter;
+ });
+
+ describe('Case 1: Single Puter path', () => {
+ it('should return true for directory containing .git', async () => {
+ const result = await hasGitDirectory('/path/to/git/dir');
+ assert.strictEqual(result, true);
+ });
+
+ it('should return false for directory without .git', async () => {
+ const result = await hasGitDirectory('/path/to/normal/dir');
+ assert.strictEqual(result, false);
+ });
+
+ it('should return false for non-directory path', async () => {
+ const result = await hasGitDirectory('/path/to/file');
+ assert.strictEqual(result, false);
+ });
+ });
+
+ describe('Case 2: Array of Puter items', () => {
+ it('should return true when .git directory is present', async () => {
+ const items = [
+ { uid: '1', name: 'src', is_dir: true },
+ { uid: '2', name: '.git', is_dir: true },
+ { uid: '3', name: 'test', is_dir: true }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, true);
+ });
+
+ it('should return false when no .git directory is present', async () => {
+ const items = [
+ { uid: '1', name: 'src', is_dir: true },
+ { uid: '2', name: 'test', is_dir: true }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, false);
+ });
+ });
+
+ describe('Case 3: Local items (DataTransferItems)', () => {
+ it('should return true when path contains .git directory', async () => {
+ const items = [
+ { fullPath: '/project/.git/config' },
+ { fullPath: '/project/src/index.js' }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, true);
+ });
+
+ it('should return true when using alternative path properties', async () => {
+ const items = [
+ { path: '/project/.git/config' },
+ { filepath: '/project/src/index.js' }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, true);
+ });
+
+ it('should return false when no .git directory is present', async () => {
+ const items = [
+ { fullPath: '/project/src/index.js' },
+ { fullPath: '/project/test/test.js' }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, false);
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle empty array input', async () => {
+ const result = await hasGitDirectory([]);
+ assert.strictEqual(result, false);
+ });
+
+ it('should handle invalid input', async () => {
+ const result = await hasGitDirectory(null);
+ assert.strictEqual(result, false);
+ });
+
+ it('should handle mixed path formats', async () => {
+ const items = [
+ { fullPath: '/project/src/index.js' },
+ { path: '/project/.git/config' },
+ { filepath: '/project/test/test.js' }
+ ];
+ const result = await hasGitDirectory(items);
+ assert.strictEqual(result, true);
+ });
+ });
+});
+
+
diff --git a/src/phoenix/test/test-git-warning-dialog.js b/src/phoenix/test/test-git-warning-dialog.js
new file mode 100644
index 000000000..b0afde3df
--- /dev/null
+++ b/src/phoenix/test/test-git-warning-dialog.js
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { JSDOM } from 'jsdom';
+
+// Function to test
+async function showGitWarningDialog() {
+ try {
+ // Check if the user has chosen to skip the warning
+ const skipWarning = await global.puter.kv.get('skip-git-warning');
+
+ // Log retrieved value for debugging
+ console.log('Retrieved skip-git-warning:', skipWarning);
+
+ // If the user opted to skip the warning, proceed without showing it
+ if (skipWarning === true) {
+ return true;
+ }
+ } catch (error) {
+ console.error('Error accessing KV store:', error);
+ // If KV store access fails, fall back to showing the dialog
+ }
+
+ // Create the modal dialog
+ const modal = document.createElement('div');
+ modal.innerHTML = `
+
+
Warning: Git Repository Detected
+
A .git directory was found in your deployment files. Deploying .git directories may:
+
+
Expose sensitive information like commit history and configuration
+
Significantly increase deployment size
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+
+ return new Promise((resolve) => {
+ // Handle "Continue Deployment"
+ document.getElementById('continue-deployment').addEventListener('click', async () => {
+ try {
+ const skipChecked = document.getElementById('skip-git-warning')?.checked;
+ if (skipChecked) {
+ console.log("Saving 'skip-git-warning' preference as true");
+ await global.puter.kv.set('skip-git-warning', true);
+ }
+ } catch (error) {
+ console.error('Error saving user preference to KV store:', error);
+ } finally {
+ document.body.removeChild(modal);
+ resolve(true); // Continue deployment
+ }
+ });
+
+ // Handle "Cancel Deployment"
+ document.getElementById('cancel-deployment').addEventListener('click', () => {
+ document.body.removeChild(modal);
+ resolve(false); // Cancel deployment
+ });
+ });
+}
+
+describe('showGitWarningDialog', () => {
+ let dom;
+ let consoleLogs = [];
+ let consoleErrors = [];
+
+ // Mock console methods
+ const mockConsole = {
+ log: (msg) => consoleLogs.push(msg),
+ error: (msg) => consoleErrors.push(msg)
+ };
+
+ // Mock puter.kv
+ const mockPuterKV = {
+ get: async () => false,
+ set: async () => {}
+ };
+
+ beforeEach(() => {
+ // Set up JSDOM with all required features
+ dom = new JSDOM(``, {
+ url: 'http://localhost',
+ runScripts: 'dangerously',
+ resources: 'usable',
+ pretendToBeVisual: true
+ });
+
+ // Set up global objects
+ global.document = dom.window.document;
+ global.window = dom.window;
+ global.HTMLElement = dom.window.HTMLElement;
+ global.Element = dom.window.Element;
+ global.Node = dom.window.Node;
+ global.navigator = dom.window.navigator;
+ global.console = { ...console, ...mockConsole };
+ global.puter = { kv: mockPuterKV };
+
+ // Reset console logs
+ consoleLogs = [];
+ consoleErrors = [];
+ });
+
+ afterEach(() => {
+ // Clean up
+ dom.window.document.body.innerHTML = '';
+ });
+
+ describe('Skip Warning Behavior', () => {
+ it('should skip dialog if warning is disabled', async () => {
+ global.puter.kv.get = async () => true;
+
+ const result = await showGitWarningDialog();
+
+ assert.strictEqual(result, true);
+ assert.strictEqual(dom.window.document.body.children.length, 0);
+ assert.ok(consoleLogs.some(log => log.includes('Retrieved skip-git-warning')));
+ });
+
+ it('should show dialog if warning is not disabled', async () => {
+ global.puter.kv.get = async () => false;
+
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ // Check if dialog is shown
+ assert.strictEqual(dom.window.document.body.children.length, 1);
+ assert.ok(dom.window.document.querySelector('h3').textContent.includes('Git Repository Detected'));
+
+ // Simulate clicking "Cancel" to resolve the promise
+ dom.window.document.getElementById('cancel-deployment').click();
+ const result = await dialogPromise;
+ assert.strictEqual(result, false);
+ });
+
+ it('should handle KV store error gracefully', async () => {
+ global.puter.kv.get = async () => {
+ throw new Error('KV store error');
+ };
+
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ // Check if dialog is shown
+ assert.strictEqual(dom.window.document.body.children.length, 1);
+ assert.ok(consoleErrors.some(error => error.includes('Error accessing KV store')));
+
+ // Cleanup
+ dom.window.document.getElementById('cancel-deployment').click();
+ await dialogPromise;
+ });
+ });
+
+ describe('User Interaction', () => {
+ it('should save preference when checkbox is checked', async () => {
+ let kvSetCalled = false;
+ global.puter.kv.set = async (key, value) => {
+ assert.strictEqual(key, 'skip-git-warning');
+ assert.strictEqual(value, true);
+ kvSetCalled = true;
+ };
+
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ const checkbox = dom.window.document.getElementById('skip-git-warning');
+ checkbox.checked = true;
+
+ dom.window.document.getElementById('continue-deployment').click();
+ const result = await dialogPromise;
+
+ assert.strictEqual(result, true);
+ assert.ok(kvSetCalled);
+ assert.ok(consoleLogs.some(log => log.includes("Saving 'skip-git-warning' preference")));
+ });
+
+ it('should not save preference when checkbox is unchecked', async () => {
+ let kvSetCalled = false;
+ global.puter.kv.set = async () => {
+ kvSetCalled = true;
+ };
+
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ dom.window.document.getElementById('continue-deployment').click();
+ await dialogPromise;
+
+ assert.strictEqual(kvSetCalled, false);
+ });
+
+ it('should handle KV set error gracefully', async () => {
+ global.puter.kv.set = async () => {
+ throw new Error('KV set error');
+ };
+
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ const checkbox = dom.window.document.getElementById('skip-git-warning');
+ checkbox.checked = true;
+
+ dom.window.document.getElementById('continue-deployment').click();
+ const result = await dialogPromise;
+
+ assert.strictEqual(result, true);
+ assert.ok(consoleErrors.some(error => error.includes('Error saving user preference')));
+ });
+ });
+
+ describe('Dialog UI', () => {
+ it('should create dialog with all required elements', async () => {
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ assert.ok(dom.window.document.querySelector('h3'));
+ assert.ok(dom.window.document.querySelector('ul'));
+ assert.ok(dom.window.document.getElementById('skip-git-warning'));
+ assert.ok(dom.window.document.getElementById('cancel-deployment'));
+ assert.ok(dom.window.document.getElementById('continue-deployment'));
+
+ // Cleanup
+ dom.window.document.getElementById('cancel-deployment').click();
+ await dialogPromise;
+ });
+
+ it('should remove dialog after interaction', async () => {
+ const dialogPromise = showGitWarningDialog();
+
+ // Wait for the next tick to allow DOM updates
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ assert.strictEqual(dom.window.document.body.children.length, 1);
+
+ dom.window.document.getElementById('continue-deployment').click();
+ await dialogPromise;
+
+ assert.strictEqual(dom.window.document.body.children.length, 0);
+ });
+ });
+});