refactor git provider and site structures

This commit is contained in:
Krrish Ghimire
2026-07-05 23:40:37 +05:45
parent bfc61ed660
commit 874eb33678
17 changed files with 631 additions and 679 deletions

View File

@@ -72,9 +72,7 @@ class LLMControllerTest {
.pages(List.of(
SiteStructure.PageStructure.builder()
.title("Home").slug("home")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("hero").build()
))
.rawHtml("<h1>Home</h1>")
.build()
))
.build();
@@ -101,9 +99,7 @@ class LLMControllerTest {
.pages(List.of(
SiteStructure.PageStructure.builder()
.title("Home").slug("home")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("hero").build()
))
.rawHtml("<h1>Home</h1>")
.build()
))
.build();
@@ -215,9 +211,7 @@ class LLMControllerTest {
.pages(List.of(
SiteStructure.PageStructure.builder()
.title("Old").slug("old")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("text").build()
))
.rawHtml("<p>Old</p>")
.build()
))
.build();
@@ -225,9 +219,7 @@ class LLMControllerTest {
.pages(List.of(
SiteStructure.PageStructure.builder()
.title("Refined").slug("refined")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("hero").build()
))
.rawHtml("<h1>Refined</h1>")
.build()
))
.build();
@@ -354,9 +346,7 @@ class LLMControllerTest {
.pages(List.of(
SiteStructure.PageStructure.builder()
.title("Restored").slug("restored")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("hero").build()
))
.rawHtml("<h1>Restored</h1>")
.build()
))
.build();

View File

@@ -0,0 +1,364 @@
package com.krrishg.service;
import com.krrishg.config.GitConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class GitServiceTest {
@TempDir
Path tempDir;
private Path remoteDir;
private GitService gitService;
@BeforeEach
void setUp() throws Exception {
remoteDir = tempDir.resolve("remote");
Files.createDirectories(remoteDir);
GitConfig config = new GitConfig();
config.setAuthorName("Test Author");
config.setAuthorEmail("test@example.com");
gitService = new GitService(config, null, null, null);
}
@Test
void initialPublishCreatesBranchOnRemote() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> files = new HashMap<>();
files.put("index.html", "<h1>Hello</h1>".getBytes());
files.put("style.css", "body { color: red; }".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), files, "Deploy site");
assertBranchExists("gh-pages");
assertRemoteFileContent("gh-pages", "index.html", "<h1>Hello</h1>");
assertRemoteFileContent("gh-pages", "style.css", "body { color: red; }");
assertRemoteHasNojekyll("gh-pages");
}
@Test
void initialPublishForGitLabCreatesPublicDirectory() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir.resolve("public");
Files.createDirectories(contentRoot);
Map<String, byte[]> files = new HashMap<>();
files.put("index.html", "<h1>GitLab</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gl-pages",
contentRoot, new GitLabPagesProvider(), files, "Deploy site");
assertBranchExists("gl-pages");
assertRemoteFileContent("gl-pages", "public/index.html", "<h1>GitLab</h1>");
assertRemoteHasGitlabCi("gl-pages");
}
@Test
void secondPublishPreservesHistory() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> firstFiles = new HashMap<>();
firstFiles.put("index.html", "<h1>First</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), firstFiles, "Deploy site");
Path workDir2 = tempDir.resolve("work2");
Files.createDirectories(workDir2);
Path contentRoot2 = workDir2;
Map<String, byte[]> secondFiles = new HashMap<>();
secondFiles.put("index.html", "<h1>Second</h1>".getBytes());
gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages",
contentRoot2, new GitHubPagesProvider(), secondFiles, "Deploy site");
assertRemoteFileContent("gh-pages", "index.html", "<h1>Second</h1>");
int commitCount = countCommits("gh-pages");
assertEquals(2, commitCount);
}
@Test
void divergenceUsesLocalContentOnConflict() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> initialFiles = new HashMap<>();
initialFiles.put("index.html", "<h1>Original</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), initialFiles, "Deploy site");
addFileToRemote("gh-pages", "index.html", "<h1>Remote change</h1>");
addFileToRemote("gh-pages", "remote-only.html", "<p>Remote only</p>");
Path workDir2 = tempDir.resolve("work2");
Files.createDirectories(workDir2);
Path contentRoot2 = workDir2;
Map<String, byte[]> localFiles = new HashMap<>();
localFiles.put("index.html", "<h1>Local new version</h1>".getBytes());
localFiles.put("local-only.html", "<p>Local only</p>".getBytes());
gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages",
contentRoot2, new GitHubPagesProvider(), localFiles, "Deploy site");
assertRemoteFileContent("gh-pages", "index.html", "<h1>Local new version</h1>");
assertRemoteFileContent("gh-pages", "local-only.html", "<p>Local only</p>");
int commitCount = countCommits("gh-pages");
assertEquals(4, commitCount, "Should preserve history from both sides");
}
@Test
void publishWithoutNojekyllForGitHub() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> files = new HashMap<>();
files.put("index.html", "<h1>Test</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), files, "Deploy site");
assertRemoteFileContent("gh-pages", "index.html", "<h1>Test</h1>");
}
@Test
void unpublishReplacesContentWithRemovalPage() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> siteFiles = new HashMap<>();
siteFiles.put("index.html", "<h1>My Site</h1>".getBytes());
siteFiles.put("about.html", "<h1>About</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), siteFiles, "Deploy site");
Path workDir2 = tempDir.resolve("work2");
Files.createDirectories(workDir2);
Path contentRoot2 = workDir2;
Map<String, byte[]> removalFiles = new HashMap<>();
removalFiles.put("index.html", "<!DOCTYPE html>".getBytes());
gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages",
contentRoot2, new GitHubPagesProvider(), removalFiles, "Unpublish site");
assertRemoteFileContentStart("gh-pages", "index.html", "<!DOCTYPE html>");
assertRemoteFileMissing("gh-pages", "about.html");
}
@Test
void unpublishPreservesHistory() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir;
Map<String, byte[]> siteFiles = new HashMap<>();
siteFiles.put("index.html", "<h1>My Site</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages",
contentRoot, new GitHubPagesProvider(), siteFiles, "Deploy site");
Path workDir2 = tempDir.resolve("work2");
Files.createDirectories(workDir2);
Path contentRoot2 = workDir2;
Map<String, byte[]> removalFiles = new HashMap<>();
removalFiles.put("index.html", "<!DOCTYPE html>".getBytes());
gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages",
contentRoot2, new GitHubPagesProvider(), removalFiles, "Unpublish site");
int commitCount = countCommits("gh-pages");
assertEquals(2, commitCount, "Unpublish should add a commit, not replace history");
}
@Test
void unpublishForGitLabUsesPublicDirectory() throws Exception {
initRemote();
Path workDir = tempDir.resolve("work");
Files.createDirectories(workDir);
Path contentRoot = workDir.resolve("public");
Files.createDirectories(contentRoot);
Map<String, byte[]> siteFiles = new HashMap<>();
siteFiles.put("index.html", "<h1>GitLab Site</h1>".getBytes());
gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gl-pages",
contentRoot, new GitLabPagesProvider(), siteFiles, "Deploy site");
Path workDir2 = tempDir.resolve("work2");
Files.createDirectories(workDir2);
Path contentRoot2 = workDir2.resolve("public");
Files.createDirectories(contentRoot2);
Map<String, byte[]> removalFiles = new HashMap<>();
removalFiles.put("index.html", "<!DOCTYPE html>".getBytes());
gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gl-pages",
contentRoot2, new GitLabPagesProvider(), removalFiles, "Unpublish site");
assertRemoteFileContentStart("gl-pages", "public/index.html", "<!DOCTYPE html>");
assertRemoteHasGitlabCi("gl-pages");
}
private void assertRemoteFileContentStart(String branch, String path, String expectedPrefix) throws Exception {
Path cloneDir = tempDir.resolve("prefix-" + path.hashCode());
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch,
"git checkout -b " + branch + " origin/" + branch
);
Path filePath = cloneDir.resolve(path);
assertTrue(Files.exists(filePath), "File " + path + " should exist in " + branch);
String actual = Files.readString(filePath);
assertTrue(actual.startsWith(expectedPrefix),
"Expected content starting with \"" + expectedPrefix + "\" but got: " + actual);
}
private void assertRemoteFileMissing(String branch, String path) throws Exception {
Path cloneDir = tempDir.resolve("missing-" + path.hashCode());
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch,
"git checkout -b " + branch + " origin/" + branch
);
Path filePath = cloneDir.resolve(path);
assertFalse(Files.exists(filePath), "File " + path + " should NOT exist in " + branch);
}
private void initRemote() throws Exception {
runGit(remoteDir, "git init --bare");
}
private void assertBranchExists(String branch) throws Exception {
ProcessBuilder pb = new ProcessBuilder("sh", "-c",
"git ls-remote --exit-code file://" + remoteDir.toString() + " " + branch);
pb.directory(tempDir.toFile());
assertEquals(0, pb.start().waitFor(), "Branch " + branch + " should exist on remote");
}
private void assertRemoteFileContent(String branch, String path, String expectedContent) throws Exception {
Path cloneDir = tempDir.resolve("assert-" + path.hashCode());
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch,
"git checkout -b " + branch + " origin/" + branch
);
Path filePath = cloneDir.resolve(path);
assertTrue(Files.exists(filePath), "File " + path + " should exist in " + branch);
String actual = Files.readString(filePath);
assertEquals(expectedContent, actual);
}
private void assertRemoteHasNojekyll(String branch) throws Exception {
try {
assertRemoteFileContent(branch, ".nojekyll", "");
} catch (AssertionError e) {
Path cloneDir = tempDir.resolve("nojekyll-check");
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch,
"git checkout -b " + branch + " origin/" + branch
);
assertTrue(Files.exists(cloneDir.resolve(".nojekyll")),
".nojekyll file should exist on " + branch);
}
}
private void assertRemoteHasGitlabCi(String branch) throws Exception {
assertRemoteFileContent(branch, ".gitlab-ci.yml",
"pages:\n" +
" stage: deploy\n" +
" script:\n" +
" - echo \"Deploying GitLab Pages\"\n" +
" artifacts:\n" +
" paths:\n" +
" - public\n" +
" rules:\n" +
" - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n");
}
private void addFileToRemote(String branch, String path, String content) throws Exception {
Path cloneDir = tempDir.resolve("remote-add-" + path.hashCode());
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch,
"git checkout -b " + branch + " origin/" + branch
);
Path filePath = cloneDir.resolve(path);
Files.createDirectories(filePath.getParent());
Files.writeString(filePath, content);
runGit(cloneDir,
"git add -A",
"git -c user.name='Remote' -c user.email='remote@example.com' commit -m 'Remote change: " + path + "'",
"git push origin " + branch
);
}
private int countCommits(String branch) throws Exception {
Path cloneDir = tempDir.resolve("count-commits");
Files.createDirectories(cloneDir);
runGit(cloneDir,
"git init",
"git remote add origin file://" + remoteDir.toString(),
"git fetch origin " + branch
);
ProcessBuilder pb = new ProcessBuilder("sh", "-c",
"git rev-list --count origin/" + branch);
pb.directory(cloneDir.toFile());
pb.redirectErrorStream(true);
Process process = pb.start();
String output = new String(process.getInputStream().readAllBytes()).trim();
int exitCode = process.waitFor();
if (exitCode != 0) return 0;
return Integer.parseInt(output);
}
private void runGit(Path workDir, String... commands) throws Exception {
for (String cmd : commands) {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
pb.directory(workDir.toFile());
pb.redirectErrorStream(true);
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
String output = new String(process.getInputStream().readAllBytes());
throw new RuntimeException("git command failed (exit " + exitCode + "): " + cmd + "\n" + output);
}
}
}
}

View File

@@ -95,9 +95,7 @@ class LLMResponseParserTest {
{
"title": "Home",
"slug": "home",
"components": [
{"type": "hero", "orderIndex": 0, "config": {"title": "Welcome"}}
]
"rawHtml": "<h1>Welcome</h1>"
}
]
}
@@ -111,8 +109,7 @@ class LLMResponseParserTest {
assertEquals(1, structure.getPages().size());
assertEquals("Home", structure.getPages().get(0).getTitle());
assertEquals("home", structure.getPages().get(0).getSlug());
assertEquals(1, structure.getPages().get(0).getComponents().size());
assertEquals("hero", structure.getPages().get(0).getComponents().get(0).getType());
assertEquals("<h1>Welcome</h1>", structure.getPages().get(0).getRawHtml());
}
@Test
@@ -120,7 +117,7 @@ class LLMResponseParserTest {
String json = """
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "text"}]}
{"title": "Home", "slug": "home", "rawHtml": "<p>Hello</p>"}
]
}
""";
@@ -147,7 +144,7 @@ class LLMResponseParserTest {
String json = """
{
"pages": [
{"slug": "home", "components": [{"type": "hero"}]}
{"slug": "home", "rawHtml": "<p>test</p>"}
]
}
""";
@@ -159,19 +156,7 @@ class LLMResponseParserTest {
String json = """
{
"pages": [
{"title": "Home", "components": [{"type": "hero"}]}
]
}
""";
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
}
@Test
void throwsOnComponentWithoutType() {
String json = """
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"orderIndex": 0}]}
{"title": "Home", "rawHtml": "<p>test</p>"}
]
}
""";
@@ -183,7 +168,7 @@ class LLMResponseParserTest {
String json = """
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
{"title": "Home", "slug": "home", "rawHtml": "<h1>Hello</h1>"}
]
}
""";
@@ -192,20 +177,6 @@ class LLMResponseParserTest {
assertNotNull(structure.getPages().get(0).getId());
}
@Test
void assignsGeneratedIdWhenComponentIdMissing() {
String json = """
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
]
}
""";
SiteStructure structure = parser.parse(json);
assertNotNull(structure.getPages().get(0).getComponents().get(0).getId());
}
@Test
void parsesPageWithAllOptionalFields() {
String json = """
@@ -218,15 +189,7 @@ class LLMResponseParserTest {
"seoTitle": "About Us",
"seoDescription": "Learn about our company",
"orderIndex": 1,
"components": [
{
"id": "comp-1",
"type": "text",
"orderIndex": 0,
"config": {"body": "Hello"},
"styles": {"color": "red"}
}
]
"rawHtml": "<section><h2>About</h2><p>Our story</p></section>"
}
]
}
@@ -240,13 +203,7 @@ class LLMResponseParserTest {
assertEquals("About Us", page.getSeoTitle());
assertEquals("Learn about our company", page.getSeoDescription());
assertEquals(1, page.getOrderIndex());
SiteStructure.ComponentStructure comp = page.getComponents().get(0);
assertEquals("comp-1", comp.getId());
assertEquals("text", comp.getType());
assertEquals(0, comp.getOrderIndex());
assertEquals("Hello", comp.getConfig().get("body"));
assertEquals("red", comp.getStyles().get("color"));
assertEquals("<section><h2>About</h2><p>Our story</p></section>", page.getRawHtml());
}
@Test
@@ -255,7 +212,7 @@ class LLMResponseParserTest {
{
"unknownField": true,
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero", "extra": 1}]}
{"title": "Home", "slug": "home", "rawHtml": "<p>test</p>", "extra": 1}
]
}
""";
@@ -268,5 +225,19 @@ class LLMResponseParserTest {
String json = "{invalid json here}";
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
}
@Test
void parsesPageWithoutRawHtml() {
String json = """
{
"pages": [
{"title": "Empty", "slug": "empty"}
]
}
""";
SiteStructure structure = parser.parse(json);
assertNull(structure.getPages().get(0).getRawHtml());
}
}
}

View File

@@ -78,7 +78,7 @@ class LLMServiceTest {
activeProvider.withContent("""
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
{"title": "Home", "slug": "home", "rawHtml": "<h1>Welcome</h1>"}
]
}
""");
@@ -97,7 +97,7 @@ class LLMServiceTest {
activeProvider.withContent("""
{
"pages": [
{"title": "About", "slug": "about", "components": [{"type": "text"}]}
{"title": "About", "slug": "about", "rawHtml": "<p>About</p>"}
]
}
""");
@@ -122,7 +122,7 @@ class LLMServiceTest {
activeProvider.withContent("""
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
{"title": "Home", "slug": "home", "rawHtml": "<p>Hero</p>"}
]
}
""");
@@ -147,7 +147,7 @@ class LLMServiceTest {
activeProvider.withContent("""
{
"pages": [
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
{"title": "Home", "slug": "home", "rawHtml": "<p>Hero</p>"}
]
}
""");
@@ -174,7 +174,7 @@ class LLMServiceTest {
fallbackProvider.withContent("""
{
"pages": [
{"title": "Fallback", "slug": "fallback", "components": [{"type": "hero"}]}
{"title": "Fallback", "slug": "fallback", "rawHtml": "<p>Hero</p>"}
]
}
""");
@@ -204,9 +204,7 @@ class LLMServiceTest {
SiteStructure.PageStructure.builder()
.title("Old")
.slug("old")
.components(List.of(
SiteStructure.ComponentStructure.builder().type("text").build()
))
.rawHtml("<p>Old content</p>")
.build()
))
.build();
@@ -214,7 +212,7 @@ class LLMServiceTest {
activeProvider.withContent("""
{
"pages": [
{"title": "Refined", "slug": "refined", "components": [{"type": "hero"}]}
{"title": "Refined", "slug": "refined", "rawHtml": "<p>Hero</p>"}
]
}
""");
@@ -258,7 +256,7 @@ class LLMServiceTest {
GenerationVersion version = GenerationVersion.builder()
.id("v1")
.fullStructure("""
{"pages": [{"title": "Restored", "slug": "restored", "components": [{"type": "hero"}]}]}
{"pages": [{"title": "Restored", "slug": "restored", "rawHtml": "<p>Hero</p>"}]}
""")
.build();