Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions services/app-builder/src/_integration_tests/git-test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, readdirSyn
import { tmpdir } from 'os';
import { join } from 'path';

// --- Configuration ---

export const APP_BUILDER_URL = process.env.APP_BUILDER_URL || 'http://localhost:8790';
export const AUTH_TOKEN = process.env.AUTH_TOKEN || 'dev-token-change-this-in-production';

// --- Types ---

export type TokenPermission = 'full' | 'ro';

export type InitSuccessResponse = {
Expand All @@ -32,8 +28,6 @@ export type TokenResponse = {
permission: TokenPermission;
};

// --- Logging ---

export function log(message: string, data?: unknown) {
console.log(`[TEST] ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
Expand All @@ -50,8 +44,6 @@ export function logFailure(message: string) {
console.error(`[✗] ${message}`);
}

// --- API helpers ---

export async function initProject(projectId: string): Promise<InitSuccessResponse> {
const endpoint = `${APP_BUILDER_URL}/apps/${encodeURIComponent(projectId)}/init`;

Expand Down Expand Up @@ -94,8 +86,6 @@ export async function generateGitToken(
return response.json();
}

// --- Git CLI helpers ---

export function buildGitUrlWithToken(gitUrl: string, token: string): string {
const url = new URL(gitUrl);
url.username = 'x-access-token';
Expand Down Expand Up @@ -172,8 +162,6 @@ export function runGitCommandSafe(
}
}

// --- High-level helpers ---

/**
* Clone a repository into a subdirectory of `parentDir`.
* Returns the absolute path to the cloned directory.
Expand Down Expand Up @@ -212,8 +200,6 @@ export function push(dir: string, branch = 'main') {
runGitCommand(dir, `git push origin ${branch}`);
}

// --- Temp dir management ---

export function createTempDir(prefix = 'app-builder-test-'): string {
return mkdtempSync(join(tmpdir(), prefix));
}
Expand All @@ -226,8 +212,6 @@ export function removeTempDir(dir: string) {
}
}

// --- Assertions ---

export class AssertionError extends Error {
constructor(message: string) {
super(message);
Expand Down Expand Up @@ -274,20 +258,16 @@ export function assertFileExists(dir: string, relativePath: string, label: strin
* Runs multiple independent checks.
*/
export function assertNotDetachedHead(dir: string, expectedBranch = 'main') {
// Check 1: git branch --show-current (empty in detached HEAD)
const branchShowCurrent = runGitCommandSafe(dir, 'git branch --show-current');
assertEqual(branchShowCurrent.stdout, expectedBranch, 'git branch --show-current');

// Check 2: git symbolic-ref HEAD (errors in detached HEAD)
const symbolicRef = runGitCommandSafe(dir, 'git symbolic-ref HEAD');
assertEqual(symbolicRef.exitCode, 0, 'git symbolic-ref HEAD exit code');
assertEqual(symbolicRef.stdout, `refs/heads/${expectedBranch}`, 'git symbolic-ref HEAD');

// Check 3: git rev-parse --abbrev-ref HEAD (returns "HEAD" if detached)
const abbrevRef = runGitCommandSafe(dir, 'git rev-parse --abbrev-ref HEAD');
assertEqual(abbrevRef.stdout, expectedBranch, 'git rev-parse --abbrev-ref HEAD');

// Check 4: git status should say "On branch main", not "HEAD detached"
const status = runGitCommandSafe(dir, 'git status');
assertIncludes(status.stdout, `On branch ${expectedBranch}`, 'git status branch line');
}
Expand All @@ -311,8 +291,6 @@ export function getCommitMessages(dir: string): string[] {
.filter(line => line.length > 0);
}

// --- Test runner ---

export type TestFn = () => Promise<void>;

/**
Expand Down Expand Up @@ -342,7 +320,6 @@ export async function runTestSuite(
}
}

// Summary
console.log(`\n${'='.repeat(60)}`);
console.log(` Results: ${suiteName}`);
console.log('='.repeat(60));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ function uniqueId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}

// =========================================================================
// Test 1: Multiple local commits then push, clone verifies all history
// =========================================================================
async function testMultipleCommitsThenPush() {
const testId = uniqueId('multi-commit');
const tempDir = createTempDir();
Expand All @@ -56,29 +53,23 @@ async function testMultipleCommitsThenPush() {
log(`Project: ${testId}`);
const { git_url: gitUrl } = await initProject(testId);

// Clone
const dir1 = await cloneRepo(testId, gitUrl, tempDir, 'work');
configureGitUser(dir1);

// First local commit: create file A
const fileAContent = 'File A — first commit';
writeFileSync(join(dir1, 'fileA.txt'), fileAContent);
commitAll(dir1, 'Add file A');

// Second local commit: modify A + create B
const fileAUpdated = 'File A — updated in second commit';
const fileBContent = 'File B — second commit';
writeFileSync(join(dir1, 'fileA.txt'), fileAUpdated);
writeFileSync(join(dir1, 'fileB.txt'), fileBContent);
commitAll(dir1, 'Update A, add B');

// Push both commits at once
push(dir1);

// Clone into a fresh folder
const dir2 = await cloneRepo(testId, gitUrl, tempDir, 'verify');

// Assertions
assertNotDetachedHead(dir2, 'main');

// 3 commits: initial template + "Add file A" + "Update A, add B"
Expand All @@ -93,7 +84,6 @@ async function testMultipleCommitsThenPush() {
assertFileContent(dir2, 'fileA.txt', fileAUpdated, 'fileA.txt content');
assertFileContent(dir2, 'fileB.txt', fileBContent, 'fileB.txt content');

// git status should be clean
const status = runGitCommand(dir2, 'git status --porcelain');
assertEqual(status.trim(), '', 'git status clean');

Expand All @@ -103,9 +93,6 @@ async function testMultipleCommitsThenPush() {
}
}

// =========================================================================
// Test 2: Incremental pushes — push, then push again, then clone
// =========================================================================
async function testIncrementalPushes() {
const testId = uniqueId('incr-push');
const tempDir = createTempDir();
Expand All @@ -114,24 +101,19 @@ async function testIncrementalPushes() {
log(`Project: ${testId}`);
const { git_url: gitUrl } = await initProject(testId);

// Clone
const dir1 = await cloneRepo(testId, gitUrl, tempDir, 'work');
configureGitUser(dir1);

// First commit + push
writeFileSync(join(dir1, 'first.txt'), 'first push content');
commitAll(dir1, 'First push');
push(dir1);

// Second commit + push (incremental)
writeFileSync(join(dir1, 'second.txt'), 'second push content');
commitAll(dir1, 'Second push');
push(dir1);

// Clone into fresh folder
const dir2 = await cloneRepo(testId, gitUrl, tempDir, 'verify');

// Assertions
assertNotDetachedHead(dir2, 'main');

const commitCount = countCommits(dir2);
Expand All @@ -150,9 +132,6 @@ async function testIncrementalPushes() {
}
}

// =========================================================================
// Test 3: Bidirectional — clone in dir1 push, clone in dir2 push, verify
// =========================================================================
async function testBidirectionalPushes() {
const testId = uniqueId('bidir');
const tempDir = createTempDir();
Expand All @@ -161,27 +140,22 @@ async function testBidirectionalPushes() {
log(`Project: ${testId}`);
const { git_url: gitUrl } = await initProject(testId);

// Clone into dir1, make changes, push
const dir1 = await cloneRepo(testId, gitUrl, tempDir, 'dir1');
configureGitUser(dir1);
writeFileSync(join(dir1, 'from-dir1.txt'), 'created in dir1');
commitAll(dir1, 'Commit from dir1');
push(dir1);

// Clone into dir2 (simulates cloud-agent picking up the repo)
const dir2 = await cloneRepo(testId, gitUrl, tempDir, 'dir2');
assertNotDetachedHead(dir2, 'main');
configureGitUser(dir2);

// Verify dir2 has dir1's file
assertFileContent(dir2, 'from-dir1.txt', 'created in dir1', 'dir1 file in dir2');

// Make changes in dir2, push
writeFileSync(join(dir2, 'from-dir2.txt'), 'created in dir2');
commitAll(dir2, 'Commit from dir2');
push(dir2);

// Clone into dir3 (final verification)
const dir3 = await cloneRepo(testId, gitUrl, tempDir, 'dir3');

assertNotDetachedHead(dir3, 'main');
Expand All @@ -202,9 +176,6 @@ async function testBidirectionalPushes() {
}
}

// =========================================================================
// Test 4: Detached HEAD regression — thorough checks after every clone
// =========================================================================
async function testDetachedHeadRegression() {
const testId = uniqueId('detached');
const tempDir = createTempDir();
Expand All @@ -213,37 +184,31 @@ async function testDetachedHeadRegression() {
log(`Project: ${testId}`);
const { git_url: gitUrl } = await initProject(testId);

// Clone immediately after init (only initial commit exists)
log('Cloning immediately after init...');
const dir1 = await cloneRepo(testId, gitUrl, tempDir, 'clone-after-init');
assertNotDetachedHead(dir1, 'main');
logSuccess('Clone after init: NOT detached HEAD');

// Push a commit
configureGitUser(dir1);
writeFileSync(join(dir1, 'change.txt'), 'some change');
commitAll(dir1, 'Add change');
push(dir1);

// Clone after first push
log('Cloning after first push...');
const dir2 = await cloneRepo(testId, gitUrl, tempDir, 'clone-after-push1');
assertNotDetachedHead(dir2, 'main');
logSuccess('Clone after 1st push: NOT detached HEAD');

// Push another commit
configureGitUser(dir2);
writeFileSync(join(dir2, 'change2.txt'), 'another change');
commitAll(dir2, 'Add change2');
push(dir2);

// Clone after second push
log('Cloning after second push...');
const dir3 = await cloneRepo(testId, gitUrl, tempDir, 'clone-after-push2');
assertNotDetachedHead(dir3, 'main');
logSuccess('Clone after 2nd push: NOT detached HEAD');

// Verify the full chain is intact
assertEqual(countCommits(dir3), 3, 'final commit count');

logSuccess('No detached HEAD detected at any stage');
Expand All @@ -252,9 +217,6 @@ async function testDetachedHeadRegression() {
}
}

// =========================================================================
// Test 5: Clone of fresh repo (only template initial commit)
// =========================================================================
async function testCloneFreshRepo() {
const testId = uniqueId('fresh');
const tempDir = createTempDir();
Expand All @@ -263,26 +225,20 @@ async function testCloneFreshRepo() {
log(`Project: ${testId}`);
const { git_url: gitUrl } = await initProject(testId);

// Clone immediately — no pushes have occurred yet
const dir1 = await cloneRepo(testId, gitUrl, tempDir, 'fresh-clone');

// Should be on main, not detached
assertNotDetachedHead(dir1, 'main');

// Should have exactly 1 commit (the initial template commit)
assertEqual(countCommits(dir1), 1, 'commit count');
assertEqual(getCommitMessages(dir1)[0], 'Initial commit', 'initial commit message');

// Template files should exist (at minimum package.json for nextjs-starter)
assertFileExists(dir1, 'package.json', 'template package.json');

// Should be able to make changes and push
configureGitUser(dir1);
writeFileSync(join(dir1, 'new-file.txt'), 'new content');
commitAll(dir1, 'First user commit');
push(dir1);

// Verify the push took
const dir2 = await cloneRepo(testId, gitUrl, tempDir, 'verify');
assertNotDetachedHead(dir2, 'main');
assertEqual(countCommits(dir2), 2, 'commit count after push');
Expand All @@ -294,10 +250,6 @@ async function testCloneFreshRepo() {
}
}

// =========================================================================
// Main
// =========================================================================

async function main() {
log('Git HEAD & History Integration Tests', { appBuilderUrl: APP_BUILDER_URL });

Expand Down
Loading