From 9ef20a7667328177268e1dd3230745c1eaa2cc58 Mon Sep 17 00:00:00 2001 From: someone <2+someone@noreply.localhost> Date: Fri, 24 Apr 2026 23:45:04 +0000 Subject: [PATCH] Sync workflow/WorkflowTest.gs --- workflow/WorkflowTest.gs | 761 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 workflow/WorkflowTest.gs diff --git a/workflow/WorkflowTest.gs b/workflow/WorkflowTest.gs new file mode 100644 index 0000000..b696845 --- /dev/null +++ b/workflow/WorkflowTest.gs @@ -0,0 +1,761 @@ +/** + * ๐Ÿงช WORKFLOW INTEGRATION TESTS + * ============================================================================= + * This file contains manual test helpers to verify the connection between + * SchoolHub and external workflow tools (Vikunja & Gitea). + * + * INSTRUCTIONS: + * 1. Select the desired test function from the toolbar dropdown. + * 2. Click "Run". + * 3. Check the "Execution Log" for the results. + * ============================================================================= + */ + +/** + * Test 1: Verify Script Properties are readable. + * Goal: Ensure VIKUNJA_BASE_URL, VIKUNJA_TOKEN, etc., are set. + */ +function testWorkflowConfig() { + console.log('--- Starting Test: Workflow Config ---'); + try { + const vikunja = getVikunjaConfig(); + const gitea = getGiteaConfig(); + + const results = { + vikunjaUrl: !!vikunja.baseUrl, + vikunjaToken: !!vikunja.token, + giteaUrl: !!gitea.baseUrl, + giteaToken: !!gitea.token, + giteaOwner: !!gitea.owner, + giteaRepo: !!gitea.repo + }; + + const allPassed = Object.values(results).every(val => val === true); + + if (allPassed) { + console.log('โœ… PASS: All required Script Properties are present.'); + } else { + console.error('โŒ FAIL: Some properties are missing:'); + console.log(JSON.stringify(results, null, 2)); + } + return { ok: allPassed, data: results }; + } catch (e) { + console.error('โŒ ERROR: ' + e.message); + return { ok: false, message: e.message }; + } +} + +/** + * Test 2: Verify Gitea Connectivity. + * Goal: Perform a read-only request to the repository root. + */ +function testGiteaConnection() { + console.log('--- Starting Test: Gitea Connection ---'); + const res = getGiteaRepo(); + + if (res.ok) { + console.log('โœ… PASS: Gitea authenticated successfully.'); + console.log('Repo Name: ' + res.data.name); + return { ok: true, data: res.data }; + } else { + console.error('โŒ FAIL: Gitea connection failed: ' + res.message); + return { ok: false, message: res.message }; + } +} + +/** + * Test 3: Verify Vikunja Connectivity using a Task ID. + * @param {string} taskId - Replace with a valid Task ID from your Vikunja instance. + */ +function testVikunjaRead() { + const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID'; + if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') { + console.warn('โš ๏ธ SKIP: Please provide a valid taskId in the code first.'); + return { ok: false, message: 'No Task ID provided' }; + } + console.log(`--- Starting Test: Vikunja Read (Task: ${taskId}) ---`); + const res = getVikunjaTask(taskId); + + if (res.ok) { + console.log('โœ… PASS: Vikunja authenticated and task retrieved.'); + console.log('Task Title: ' + (res.data.title || 'No Title')); + return { ok: true, data: res.data }; + } else { + console.error('โŒ FAIL: Vikunja read failed: ' + res.message); + if (res.rawBody) console.log('Raw Response: ' + res.rawBody); + return { ok: false, message: res.message }; + } +} + +/** + * Test 4: Verify Vikunja Write Operation. + * Goal: Add a clearly marked test comment. + * @param {string} taskId - Replace with a valid Task ID. + */ +function testVikunjaComment() { + const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID'; + + if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') { + console.warn('โš ๏ธ SKIP: Please provide a valid taskId in the code first.'); + return { ok: false, message: 'No Task ID provided' }; + } + console.log(`--- Starting Test: Vikunja Write (Task: ${taskId}) ---`); + const testMessage = `[TEST] Integration check from SchoolHub at ${new Date().toISOString()}`; + const res = createVikunjaComment(taskId, testMessage); + + if (res.ok) { + console.log('โœ… PASS: Test comment successfully created.'); + return { ok: true }; + } else { + console.error('โŒ FAIL: Vikunja write failed: ' + res.message); + return { ok: false, message: res.message }; + } +} +/* + Test 6: Automated Vikunja Lifecycle Test + Goal: Create -> Read -> Comment -> Hook -> Delete (in Test Project) + */ +function testVikunjaLifecycle() { + console.log('--- Starting Test: Vikunja Lifecycle (Auto) ---'); + + const config = getTestProjectConfig(); + const testProjectId = config.testProjectId; + + if (!testProjectId) { + console.warn('โš ๏ธ SKIP: VIKUNJA_TEST_PROJECT_ID not found in Script Properties.'); + return { ok: false, message: 'Missing test project ID' }; + } + + let createdTaskId = null; + const steps = []; + let cleanedUp = false; + + try { + console.log('Step 1: Creating temporary task...'); + const createData = { + title: `[TEST][AUTO][SchoolHub] Lifecycle Test ${new Date().getTime()}`, + projectId: testProjectId + }; + + const createRes = createVikunjaTask(createData); + if (!createRes.ok || !createRes.data || !createRes.data.id) { + throw new Error(`Task Creation Failed: ${createRes.message || 'No ID returned'}. Raw: ${createRes.rawBody || 'N/A'}`); + } + + createdTaskId = createRes.data.id; + steps.push('TASK_CREATED'); + console.log(`โœ… Task created with ID: ${createdTaskId}`); + + console.log('Step 2: Reading task back...'); + const readRes = getVikunjaTask(createdTaskId); + if (!readRes.ok) throw new Error(`Read failed: ${readRes.message}`); + + steps.push('TASK_READ'); + console.log(`โœ… Task verified: ${readRes.data.title}`); + + console.log('Step 3: Adding test comment...'); + const commentRes = createVikunjaComment(createdTaskId, '[TEST] Automated lifecycle comment check.'); + if (!commentRes.ok) { + throw new Error(`Comment failed (Code: ${commentRes.code}): ${commentRes.message}. Raw: ${commentRes.rawBody}`); + } + + steps.push('COMMENT_ADDED'); + console.log('โœ… Comment added successfully'); + + console.log('Step 4: Running workflow hook...'); + const hookRes = workflowStartFeature('TEST-AUTO', createdTaskId); + if (!hookRes.ok) throw new Error(`Hook failed: ${hookRes.message}`); + + steps.push('HOOK_EXECUTED'); + console.log('โœ… Workflow hook executed successfully'); + + return { + ok: true, + createdTaskId, + cleanedUp: cleanedUp, + steps, + message: 'Full lifecycle completed successfully' + }; + } catch (e) { + console.error(`โŒ Lifecycle Test Failed at step ${steps.length + 1}: ${e.message}`); + return { + ok: false, + createdTaskId, + cleanedUp: cleanedUp, + steps, + message: e.message + }; + } finally { + if (createdTaskId) { + console.log('Final Step: Cleaning up temporary task...'); + const delRes = deleteVikunjaTask(createdTaskId); + + if (delRes.ok) { + console.log('โœ… Temporary task deleted.'); + cleanedUp = true; + } else { + console.error(`โŒ Cleanup failed for task ${createdTaskId}: ${delRes.message}`); + cleanedUp = false; + } + } + } +} + +function testMarkWorkflowTaskDone() { + console.log('--- Starting Test: markWorkflowTaskDone ---'); + + const featureMeta = { + featureCode: `DONE-TEST-${Date.now()}`, + title: 'Automated Done Status Test', + type: 'feature', + module: 'general' + }; + + try { + // 1. Ensure task exists + const ensureRes = ensureWorkflowTask(featureMeta); + const taskId = ensureRes.taskId; + console.log(`Step 1: Task created/resolved: ${taskId}`); + + // 2. Mark as done + markWorkflowTaskDone_(featureMeta, true); + console.log('Step 2: markWorkflowTaskDone_ executed'); + + // 3. Verify status + const taskRes = getVikunjaTask(taskId); + if (!taskRes.ok) throw new Error(`Read failed: ${taskRes.message}`); + + const task = taskRes.data; + assertEqual_('Task is marked done', task.done, true); + + // done_at is optional depending on server-side trigger/version + if (task.done_at) { + assertTrue_('Task has done_at timestamp', !!task.done_at); + } else { + console.log('โ„น๏ธ Note: Server did not return done_at, but task.done is true.'); + } + + console.log('--- Test Passed: markWorkflowTaskDone ---'); + + // Cleanup + deleteVikunjaTask(taskId); + } catch (e) { + console.error(`โŒ Test Failed: ${e.message}`); + throw e; + } +} + +function testVikunjaCreateOnly() { + const config = getTestProjectConfig(); + if (!config.testProjectId) return { ok: false, message: 'No test project ID' }; + + const res = createVikunjaTask({ + title: `[DEBUG] Create Test ${new Date().getTime()}`, + projectId: config.testProjectId, + priority: 4 + }); + + console.log(res.ok ? `โœ… Created: ${res.data && res.data.id}` : `โŒ Failed: ${res.message}`); + return res; +} + +/** + * ASSERTION HELPERS + */ +function assertEqual_(name, actual, expected) { + if (actual !== expected) { + throw new Error(`[ASSERT FAILED] ${name}: Expected ${expected}, but got ${actual}`); + } + console.log(`โœ… ${name}: ${actual} == ${expected}`); +} + +function assertArrayEqual_(name, actual, expected) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) { + throw new Error(`[ASSERT FAILED] ${name}: Expected ${e}, but got ${a}`); + } + console.log(`โœ… ${name}: ${a} == ${e}`); +} + +function assertTrue_(name, condition) { + if (!condition) { + throw new Error(`[ASSERT FAILED] ${name}: Expected true, but got false`); + } + console.log(`โœ… ${name}: true`); +} + +/** + * Test: Workflow Priority Rules + * Goal: Validate that the priority mapping (5..1) follows business rules. + */ +function testWorkflowPriorityRules() { + console.log('--- Starting Test: Workflow Priority Rules ---'); + + const testCases = [ + { + name: "Critical severity overrides all", + input: { type: 'feature', module: 'general', severity: 'critical' }, + expected: 5 + }, + { + name: "Auth module gets highest priority", + input: { type: 'feature', module: 'auth', severity: 'medium' }, + expected: 5 + }, + { + name: "Bugfix gets priority 4", + input: { type: 'bugfix', module: 'student', severity: 'medium' }, + expected: 4 + }, + { + name: "High severity gets priority 4", + input: { type: 'feature', module: 'student', severity: 'high' }, + expected: 4 + }, + { + name: "Feature gets priority 3", + input: { type: 'feature', module: 'student', severity: 'medium' }, + expected: 3 + }, + { + name: "Improvement gets priority 3", + input: { type: 'improvement', module: 'teacher', severity: 'medium' }, + expected: 3 + }, + { + name: "Refactor gets priority 2", + input: { type: 'refactor', module: 'general', severity: 'medium' }, + expected: 2 + }, + { + name: "Docs gets priority 2", + input: { type: 'docs', module: 'general', severity: 'medium' }, + expected: 2 + }, + { + name: "Low severity gets priority 2", + input: { type: 'chore', module: 'general', severity: 'low' }, + expected: 2 + }, + { + name: "Fallback gets priority 1", + input: { type: 'chore', module: 'general', severity: 'medium' }, + expected: 1 + }, + ]; + + testCases.forEach(tc => { + const actual = deriveWorkflowPriority_(tc.input); + assertEqual_(tc.name, actual, tc.expected); + }); + + console.log('--- Test Passed: Workflow Priority Rules ---'); +} + +/** + * Test: Workflow Label Rules + * Goal: Validate that labels are correctly derived from metadata. + */ +function testWorkflowLabelRules() { + console.log('--- Starting Test: Workflow Label Rules ---'); + + const fullInput = { + type: 'bugfix', + module: 'auth', + impactArea: 'student', + severity: 'high' + }; + const expectedFull = ['type:bugfix', 'mod:auth', 'impact:student', 'sev:high']; + assertArrayEqual_('Full metadata labels', deriveWorkflowLabels_(fullInput), expectedFull); + + const minimalInput = { type: 'feature' }; + const expectedMinimal = ['type:feature']; + assertArrayEqual_('Minimal metadata labels', deriveWorkflowLabels_(minimalInput), expectedMinimal); + + console.log('--- Test Passed: Workflow Label Rules ---'); +} + +/** + * Test: Ensure Workflow Task Integration + * Goal: Verify the full cycle: ensure task -> verify in Vikunja -> delete. + */ +function testEnsureWorkflowTask() { + console.log('--- Starting Test: Ensure Workflow Task ---'); + + const featureMeta = { + featureCode: `TEST-AUTO-PRIO-${Date.now()}`, + title: 'Automated ensureWorkflowTask priority and label check', + type: 'bugfix', + module: 'auth', + impactArea: 'student', + severity: 'critical', + requestedBy: 'Automated Test', + source: 'WorkflowTest' + }; + + let createdTaskId = null; + + try { + const ensureRes = ensureWorkflowTask(featureMeta); + console.log('Ensure Result: ' + JSON.stringify(ensureRes)); + + assertTrue_('Ensure task ok', ensureRes.ok); + assertTrue_('Task ID exists', !!ensureRes.taskId); + assertTrue_('Task state is valid', ['CREATED', 'EXISTING'].includes(ensureRes.state)); + + createdTaskId = ensureRes.taskId; + + const taskRes = getVikunjaTask(createdTaskId); + assertTrue_('Task fetch ok', taskRes.ok); + assertTrue_('Task data exists', !!taskRes.data); + + const task = taskRes.data; + assertTrue_('Title contains feature code', task.title.includes(`[${featureMeta.featureCode}]`)); + assertEqual_('Task priority matches rule', task.priority, deriveWorkflowPriority_(featureMeta)); + + const expectedLabels = deriveWorkflowLabels_(featureMeta); + if (Array.isArray(task.labels)) { + const actualLabels = task.labels.map(l => l.title || l.name); + expectedLabels.forEach(el => { + assertTrue_(`Label ${el} present`, actualLabels.includes(el)); + }); + } else { + console.log('โ„น๏ธ Labels not returned as array; verification skipped for this instance.'); + } + + console.log('--- Test Passed: Ensure Workflow Task ---'); + } catch (e) { + console.error(`โŒ Test Failed: ${e.message}`); + throw e; + } finally { + if (createdTaskId) { + const delRes = deleteVikunjaTask(createdTaskId); + if (delRes.ok) console.log('โœ… Cleanup: Temporary task deleted.'); + else console.warn('โš ๏ธ Cleanup: Failed to delete temporary task.'); + } + } +} + +/** + * Test 5: Workflow Hook Dry Run. + * Goal: Verify the orchestrator can call the client. + * @param {string} taskId - Replace with a valid Task ID. + */ +function testWorkflowHooksDryRun() { + const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID'; + + if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') { + console.warn('โš ๏ธ SKIP: Please provide a valid taskId in the code first.'); + return { ok: false, message: 'No Task ID provided' }; + } + + console.log(`--- Starting Test: Workflow Hook Dry Run (Task: ${taskId}) ---`); + const res = workflowStartFeature('TEST-001', taskId); + + if (res.ok) { + console.log('โœ… PASS: workflowStartFeature executed successfully.'); + return { ok: true }; + } else { + console.error(`โŒ FAIL: Workflow hook failed: ${res.message}`); + return { ok: false, message: res.message }; + } +} + +/* + * ๐Ÿงช GITEA SYNC INTEGRATION TESTS + * ============================================================================= + */ + +function testGiteaPathMapping() { + console.log('--- Starting Test: Gitea Path Mapping ---'); + + const testCases = [ + { input: { name: 'appsscript', type: 'JSON' }, expected: 'appsscript.json' }, + { input: { name: 'Code', type: 'SERVER_JS' }, expected: 'app/Code.gs' }, + { input: { name: 'Index', type: 'HTML' }, expected: 'app/Index.html' }, + { input: { name: 'styles', type: 'HTML' }, expected: 'app/styles.html' }, + { input: { name: 'Readme', type: 'SERVER_JS' }, expected: 'docs/Readme.gs' }, + { input: { name: 'WorkflowConfig', type: 'SERVER_JS' }, expected: 'workflow/WorkflowConfig.gs' }, + { input: { name: 'Unknown', type: 'OTHER' }, expected: null } + ]; + + testCases.forEach(function(tc) { + const actual = mapAppsScriptFileToRepoPath(tc.input); + assertEqual_( + `Path mapping for ${tc.input.name} (${tc.input.type})`, + actual, + tc.expected + ); + }); + + console.log('--- Test Passed: Gitea Path Mapping ---'); +} + +function testGiteaBranchNaming() { + console.log('--- Starting Test: Gitea Branch Naming ---'); + + const testCases = [ + { code: 'feat/login', expectedBase: 'feat-login' }, + { code: 'BUG #123', expectedBase: 'bug-123' }, + { code: '---cool---feature---', expectedBase: 'cool-feature' }, + { code: null, expectedBase: 'manual' }, + { code: '', expectedBase: 'manual' } + ]; + + testCases.forEach(function(tc) { + const branch = buildSyncBranchName(tc.code); + + assertTrue_( + `Branch starts with sync/ for input: ${tc.code}`, + branch.indexOf('sync/') === 0 + ); + + assertTrue_( + `Branch contains sanitized base "${tc.expectedBase}"`, + branch.indexOf(tc.expectedBase) !== -1 + ); + + assertTrue_( + `Branch ends with timestamp pattern for input: ${tc.code}`, + /\d{8}-\d{4}$/.test(branch) + ); + + assertTrue_( + `Branch has no duplicate hyphens for input: ${tc.code}`, + !/--/.test(branch) + ); + }); + + console.log('--- Test Passed: Gitea Branch Naming ---'); +} +/** + * Convenience runner for the Workflow Rules Suite. + * Runs priority, label, and task assurance tests. + */ +function testWorkflowRulesSuite() { + console.log('=== Starting Workflow Rules Suite ==='); + + try { + testWorkflowPriorityRules(); + testWorkflowLabelRules(); + testEnsureWorkflowTask(); + console.log('=== Workflow Rules Suite Passed ==='); + } catch (e) { + console.error('=== Workflow Rules Suite FAILED ==='); + console.error(e.message); + throw e; + } +} + +function testPrepareGitSyncPayload() { + console.log('--- Starting Test: Prepare Git Sync Payload ---'); + + const mockFiles = [ + { name: 'appsscript', type: 'JSON', source: '{ "timeZone": "UTC" }' }, + { name: 'Code', type: 'SERVER_JS', source: 'function test() {}' }, + { name: 'Index', type: 'HTML', source: '' }, + { name: 'InvalidFile', type: 'UNKNOWN', source: 'junk' }, + { name: 'MissingSource', type: 'SERVER_JS' }, + null + ]; + + const payload = prepareGitSyncPayload('test-feature', mockFiles); + + assertTrue_('Base branch is set', !!payload.baseBranch); + assertTrue_('Sync branch starts with sync/', payload.syncBranch.indexOf('sync/') === 0); + assertTrue_('Sync branch contains feature code', payload.syncBranch.indexOf('test-feature') !== -1); + + assertEqual_('Valid files mapped correctly', payload.files.length, 4); + assertEqual_('JSON mapped to appsscript.json', payload.files[0].repoPath, 'appsscript.json'); + assertEqual_('SERVER_JS mapped to app/Code.gs', payload.files[1].repoPath, 'app/Code.gs'); + assertEqual_('HTML mapped to app/Index.html', payload.files[2].repoPath, 'app/Index.html'); + assertEqual_('SERVER_JS missing source mapped to workflow/MissingSource.gs', payload.files[3].repoPath, 'workflow/MissingSource.gs'); + + console.log('--- Test Passed: Prepare Git Sync Payload ---'); +} + + +function testGiteaSyncHelpers() { + console.log('--- Starting Test: Gitea Sync Helpers ---'); + + const url = getGiteaSyncApiUrl('/test/path'); + assertTrue_('URL ends with /api/v1/test/path', url.indexOf('/api/v1/test/path') !== -1); + + const opts = getGiteaSyncRequestOptions('POST', { key: 'value' }); + assertEqual_('Method is POST', opts.method, 'POST'); + assertTrue_('Has Authorization header', !!opts.headers['Authorization']); + assertEqual_('contentType is application/json', opts.contentType, 'application/json'); + assertEqual_('Payload is stringified', opts.payload, '{"key":"value"}'); + + console.log('--- Test Passed: Gitea Sync Helpers ---'); +} + +function testGiteaBranchCreationPayload() { + console.log('--- Starting Test: Gitea Branch Creation Payload ---'); + + const baseBranch = 'main'; + const newBranch = 'sync/feat-test'; + const payload = { + new_branch_name: newBranch, + old_ref_name: baseBranch + }; + + assertEqual_('Payload uses old_ref_name instead of old_branch_name', payload.old_ref_name, 'main'); + assertTrue_('old_branch_name is undefined', payload.old_branch_name === undefined); + + console.log('--- Test Passed: Gitea Branch Creation Payload ---'); +} + +function testSyncSummaryFormat() { + console.log('--- Starting Test: Sync Summary Format ---'); + + const mockResults = { + created: 2, + updated: 1, + errors: 0, + details: [ + { file: "app/Code.gs", ok: true, message: "" }, + { file: "appsscript.json", ok: true, message: "" }, + { file: "sync-manifest.json", ok: true, message: "" } + ] + }; + + assertTrue_('Summary has created count', mockResults.created === 2); + assertTrue_('Summary has updated count', mockResults.updated === 1); + assertTrue_('Summary has errors count', mockResults.errors === 0); + assertEqual_('Summary details length is correct', mockResults.details.length, 3); + + const mockFinalResult = { + ok: true, + branch: 'sync/manual', + summary: mockResults, + pullRequest: { url: 'https://gitea.example.com/pr/1', existed: true } + }; + + assertTrue_('Final result has branch info', !!mockFinalResult.branch); + assertTrue_('Final result has pullRequest info', !!mockFinalResult.pullRequest); + assertTrue_('Final result pullRequest has url', !!mockFinalResult.pullRequest.url); + assertEqual_('Final result pullRequest existed flag is true', mockFinalResult.pullRequest.existed, true); + + console.log('--- Test Passed: Sync Summary Format ---'); +} + + +function testGetAppsScriptProjectContentErrorFormat() { + console.log('--- Starting Test: Get Apps Script Project Content Error Format ---'); + + const mockErrorCode = 500; + const result = { ok: false, message: "Failed to fetch project content. Code: " + mockErrorCode }; + + assertEqual_('Result ok is false', result.ok, false); + assertEqual_('Message formatting correct', result.message, "Failed to fetch project content. Code: 500"); + + const result403 = { + ok: false, + message: "HTTP 403 Forbidden. Ensure Google Apps Script API is enabled in user settings (https://script.google.com/home/usersettings) and OAuth scopes are correctly configured.", + code: 403, + rawBody: "Mock raw body content" + }; + + assertEqual_('403 Result ok is false', result403.ok, false); + assertEqual_('403 Result code is 403', result403.code, 403); + assertTrue_('Message contains HTTP 403 Forbidden', result403.message.indexOf("HTTP 403 Forbidden") !== -1); + assertEqual_('rawBody is preserved', result403.rawBody, "Mock raw body content"); + + console.log('--- Test Passed: Get Apps Script Project Content Error Format ---'); +} + +function testScriptIdFallback() { + console.log('--- Starting Test: Script ID Fallback ---'); + + const currentScriptId = ScriptApp.getScriptId(); + const config = getSyncConfig(); + + assertTrue_('getSyncConfig provides a scriptId', !!config.scriptId); + console.log('Current ScriptApp ID: ' + currentScriptId); + console.log('Config Script ID: ' + config.scriptId); + + if (config.scriptId === currentScriptId) { + console.log('โœ… Fallback working or explicitly matched.'); + } else { + console.log('โ„น๏ธ Configured scriptId is different from current ScriptApp ID.'); + } + + console.log('--- Test Passed: Script ID Fallback ---'); +} + +function testGiteaPullRequestPayload() { + console.log('--- Starting Test: Gitea Pull Request Payload ---'); + + const payload = { + title: 'Sync Feature: test-feat', + body: 'Auto sync', + base: 'main', + head: 'sync/test-feat' + }; + + assertEqual_('PR title is set', payload.title, 'Sync Feature: test-feat'); + assertEqual_('PR base is main', payload.base, 'main'); + assertEqual_('PR head is correct', payload.head, 'sync/test-feat'); + + console.log('--- Test Passed: Gitea Pull Request Payload ---'); +} + +function testCheckGiteaPullRequestExistsLogic() { + console.log('--- Starting Test: Check Gitea PR Exists Logic ---'); + + const mockPrs = [ + { base: { ref: 'main' }, head: { ref: 'sync/feat-1' } }, + { base: { ref: 'main' }, head: { ref: 'sync/feat-2' } } + ]; + + const baseBranch = 'main'; + const headBranch = 'sync/feat-2'; + + const existing = mockPrs.find(function(pr) { + return pr.base && pr.head && pr.base.ref === baseBranch && pr.head.ref === headBranch; + }); + + assertTrue_('Finds existing PR', !!existing); + assertEqual_('Matches correct head', existing.head.ref, 'sync/feat-2'); + + const notExisting = mockPrs.find(function(pr) { + return pr.base && pr.head && pr.base.ref === baseBranch && pr.head.ref === 'sync/feat-3'; + }); + + assertTrue_('Does not find missing PR', !notExisting); + + console.log('--- Test Passed: Check Gitea PR Exists Logic ---'); +} + +function testEmptyRepoErrorDetection() { + console.log('--- Starting Test: Empty Repo Error Detection ---'); + + const mockCreateRes = { + ok: false, + message: "Branch creation failed: Git Repository is empty.", + code: 409 + }; + + const isRepoEmptyError = !mockCreateRes.ok && mockCreateRes.message && mockCreateRes.message.toLowerCase().indexOf('empty') !== -1; + assertTrue_('Detects empty repository error message', isRepoEmptyError); + + const mockSuccessRes = { + ok: true, + data: { name: "sync/feat-test" } + }; + + const isSuccessEmptyError = !mockSuccessRes.ok && mockSuccessRes.message && mockSuccessRes.message.toLowerCase().indexOf('empty') !== -1; + assertEqual_('Does not falsely detect empty repo on success', isSuccessEmptyError, false); + + const mockBootstrapFail = { + ok: false, + message: 'Git Repository is empty and bootstrap failed: 404 Not Found. Harap inisialisasi repositori secara manual (misal: centang "Initialize Repository" saat membuat repo).' + }; + const detectsManualInit = !mockBootstrapFail.ok && mockBootstrapFail.message.toLowerCase().indexOf('manual') !== -1; + assertTrue_('Detects manual initialization instruction on bootstrap failure', detectsManualInit); + + console.log('--- Test Passed: Empty Repo Error Detection ---'); +} +