/** * ๐Ÿงช 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 ---'); }