test: add tests for optional repo across all changed modules
- detect-env: test GitHub/GitLab HTTPS/SSH remote extraction, unknown hosts returning null, missing remote, and non-git directories - repo-validation: test the anchored regex accepts owner/repo, rejects empty, lone slash, missing segments, whitespace, and nested paths - config-validation: test SCM/tracker inference skipped when repo is missing or has no slash, and inferred correctly with owner/repo - scm-webhooks: test eventMatchesProject returns false when project has no repo configured - orchestrator-prompt: existing test covers repo:undefined → "not configured" - prompt-builder: existing test covers BASE_AGENT_PROMPT_NO_REPO selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
536e38b7e6
commit
72e3620b8d
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
git: vi.fn(),
|
||||
gh: vi.fn(),
|
||||
execSilent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/git-utils.js", () => ({
|
||||
detectDefaultBranch: vi.fn().mockResolvedValue("main"),
|
||||
}));
|
||||
|
||||
import { detectEnvironment } from "../../src/lib/detect-env.js";
|
||||
import { git, execSilent } from "../../src/lib/shell.js";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(execSilent).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe("detectEnvironment", () => {
|
||||
describe("ownerRepo extraction", () => {
|
||||
it("extracts owner/repo from GitHub HTTPS remote", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git") // rev-parse
|
||||
.mockResolvedValueOnce("https://github.com/acme/my-app.git") // remote
|
||||
.mockResolvedValueOnce("main"); // branch
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBe("acme/my-app");
|
||||
});
|
||||
|
||||
it("extracts owner/repo from GitHub SSH remote", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git")
|
||||
.mockResolvedValueOnce("git@github.com:acme/my-app.git")
|
||||
.mockResolvedValueOnce("main");
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBe("acme/my-app");
|
||||
});
|
||||
|
||||
it("extracts owner/repo from GitLab HTTPS remote", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git")
|
||||
.mockResolvedValueOnce("https://gitlab.com/org/repo.git")
|
||||
.mockResolvedValueOnce("main");
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBe("org/repo");
|
||||
});
|
||||
|
||||
it("extracts owner/repo from GitLab SSH remote", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git")
|
||||
.mockResolvedValueOnce("git@gitlab.com:org/repo.git")
|
||||
.mockResolvedValueOnce("main");
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBe("org/repo");
|
||||
});
|
||||
|
||||
it("returns null for self-hosted / unknown git hosts", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git")
|
||||
.mockResolvedValueOnce("git@git.corp.com:team/project.git")
|
||||
.mockResolvedValueOnce("main");
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when no remote is configured", async () => {
|
||||
vi.mocked(git)
|
||||
.mockResolvedValueOnce(".git")
|
||||
.mockResolvedValueOnce(null) // no remote
|
||||
.mockResolvedValueOnce("main");
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.ownerRepo).toBeNull();
|
||||
expect(env.gitRemote).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when not a git repo", async () => {
|
||||
vi.mocked(git).mockResolvedValueOnce(null); // not a git repo
|
||||
|
||||
const env = await detectEnvironment("/tmp/test");
|
||||
expect(env.isGitRepo).toBe(false);
|
||||
expect(env.ownerRepo).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
/**
|
||||
* Tests for the repo validation regex used in autoCreateConfig and addProjectToConfig.
|
||||
* The regex is inlined in start.ts — this test validates the pattern independently.
|
||||
*/
|
||||
const REPO_REGEX = /^[^\s/]+\/[^\s/]+$/;
|
||||
|
||||
describe("repo validation regex", () => {
|
||||
it("accepts valid owner/repo", () => {
|
||||
expect(REPO_REGEX.test("acme/my-app")).toBe(true);
|
||||
expect(REPO_REGEX.test("ComposioHQ/agent-orchestrator")).toBe(true);
|
||||
expect(REPO_REGEX.test("org/repo")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects empty string", () => {
|
||||
expect(REPO_REGEX.test("")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects lone slash", () => {
|
||||
expect(REPO_REGEX.test("/")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing owner", () => {
|
||||
expect(REPO_REGEX.test("/repo")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing repo name", () => {
|
||||
expect(REPO_REGEX.test("owner/")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects strings with whitespace", () => {
|
||||
expect(REPO_REGEX.test("acme/repo extra")).toBe(false);
|
||||
expect(REPO_REGEX.test("acme /repo")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects nested paths (more than one slash)", () => {
|
||||
expect(REPO_REGEX.test("acme/repo/extra")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects strings without a slash", () => {
|
||||
expect(REPO_REGEX.test("notaslash")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects strings with spaces in segments", () => {
|
||||
expect(REPO_REGEX.test("my org/repo")).toBe(false);
|
||||
expect(REPO_REGEX.test("org/my repo")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -379,6 +379,55 @@ describe("Config Schema Validation", () => {
|
|||
expect(() => validateConfig(missingBranch)).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not infer SCM or tracker when repo is missing", () => {
|
||||
const config = {
|
||||
projects: {
|
||||
proj1: {
|
||||
path: "/repos/test",
|
||||
defaultBranch: "main",
|
||||
// No repo — SCM and tracker should not be inferred
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validated = validateConfig(config);
|
||||
expect(validated.projects.proj1.repo).toBeUndefined();
|
||||
expect(validated.projects.proj1.scm).toBeUndefined();
|
||||
expect(validated.projects.proj1.tracker).toBeUndefined();
|
||||
});
|
||||
|
||||
it("infers SCM and tracker when repo has owner/repo format", () => {
|
||||
const config = {
|
||||
projects: {
|
||||
proj1: {
|
||||
path: "/repos/test",
|
||||
repo: "org/test",
|
||||
defaultBranch: "main",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validated = validateConfig(config);
|
||||
expect(validated.projects.proj1.scm).toEqual({ plugin: "github" });
|
||||
expect(validated.projects.proj1.tracker).toEqual({ plugin: "github" });
|
||||
});
|
||||
|
||||
it("does not infer SCM or tracker when repo has no slash", () => {
|
||||
const config = {
|
||||
projects: {
|
||||
proj1: {
|
||||
path: "/repos/test",
|
||||
repo: "notaslash",
|
||||
defaultBranch: "main",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validated = validateConfig(config);
|
||||
expect(validated.projects.proj1.scm).toBeUndefined();
|
||||
expect(validated.projects.proj1.tracker).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sessionPrefix is optional", () => {
|
||||
const config = {
|
||||
projects: {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,20 @@ describe("eventMatchesProject", () => {
|
|||
|
||||
expect(eventMatchesProject(event, project)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when project has no repo configured", () => {
|
||||
const noRepoProject: ProjectConfig = { ...project, repo: undefined };
|
||||
const event: SCMWebhookEvent = {
|
||||
provider: "github",
|
||||
kind: "pull_request",
|
||||
action: "opened",
|
||||
rawEventType: "pull_request",
|
||||
repository: { owner: "acme", name: "my-app" },
|
||||
data: {},
|
||||
};
|
||||
|
||||
expect(eventMatchesProject(event, noRepoProject)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAffectedSessions", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue