fix(cli): detect API key from env instead of checking repo secrets (#1116)
Co-authored-by: Letta <noreply@letta.com> Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: Charles Packer <cpacker@users.noreply.github.com>
This commit is contained in:
@@ -3,10 +3,19 @@ import {
|
||||
buildInstallPrBody,
|
||||
generateLettaWorkflowYaml,
|
||||
getDefaultWorkflowPath,
|
||||
type InstallGithubAppResult,
|
||||
parseGitHubRepoFromRemote,
|
||||
parseScopesFromGhAuthStatus,
|
||||
validateRepoSlug,
|
||||
} from "../../cli/commands/install-github-app";
|
||||
import {
|
||||
buildProgress,
|
||||
buildProgressSteps,
|
||||
} from "../../cli/components/InstallGithubAppFlow";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("install-github-app helpers", () => {
|
||||
test("validateRepoSlug accepts owner/repo and rejects invalid forms", () => {
|
||||
@@ -92,3 +101,275 @@ describe("install-github-app helpers", () => {
|
||||
expect(body).toContain("letta-code-action");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wizard progress steps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("buildProgressSteps", () => {
|
||||
test("includes all standard steps for current agent mode", () => {
|
||||
const steps = buildProgressSteps("current", null);
|
||||
const labels = steps.map((s) => s.label);
|
||||
|
||||
expect(labels).toEqual([
|
||||
"Getting repository information",
|
||||
"Creating branch",
|
||||
"Creating workflow files",
|
||||
"Setting up LETTA_API_KEY secret",
|
||||
"Configuring agent",
|
||||
"Opening pull request page",
|
||||
]);
|
||||
});
|
||||
|
||||
test("includes all standard steps for existing agent mode", () => {
|
||||
const steps = buildProgressSteps("existing", null);
|
||||
const labels = steps.map((s) => s.label);
|
||||
|
||||
expect(labels).toContain("Configuring agent");
|
||||
expect(labels).toContain("Setting up LETTA_API_KEY secret");
|
||||
expect(labels).not.toContainEqual(
|
||||
expect.stringContaining("Creating agent"),
|
||||
);
|
||||
});
|
||||
|
||||
test("includes agent creation step in create mode", () => {
|
||||
const steps = buildProgressSteps("create", "My Bot");
|
||||
const labels = steps.map((s) => s.label);
|
||||
|
||||
expect(labels).toContain("Creating agent My Bot");
|
||||
});
|
||||
|
||||
test("omits 'Configuring agent' in create mode", () => {
|
||||
const steps = buildProgressSteps("create", "My Bot");
|
||||
const labels = steps.map((s) => s.label);
|
||||
|
||||
expect(labels).not.toContain("Configuring agent");
|
||||
});
|
||||
|
||||
test("always includes LETTA_API_KEY secret step regardless of mode", () => {
|
||||
for (const mode of ["current", "existing", "create"] as const) {
|
||||
const steps = buildProgressSteps(mode, mode === "create" ? "Bot" : null);
|
||||
const labels = steps.map((s) => s.label);
|
||||
expect(labels).toContain("Setting up LETTA_API_KEY secret");
|
||||
}
|
||||
});
|
||||
|
||||
test("steps are in the correct order", () => {
|
||||
const steps = buildProgressSteps("current", null);
|
||||
const labels = steps.map((s) => s.label);
|
||||
|
||||
const repoIdx = labels.indexOf("Getting repository information");
|
||||
const branchIdx = labels.indexOf("Creating branch");
|
||||
const workflowIdx = labels.indexOf("Creating workflow files");
|
||||
const secretIdx = labels.indexOf("Setting up LETTA_API_KEY secret");
|
||||
const agentIdx = labels.indexOf("Configuring agent");
|
||||
const prIdx = labels.indexOf("Opening pull request page");
|
||||
|
||||
expect(repoIdx).toBeLessThan(branchIdx);
|
||||
expect(branchIdx).toBeLessThan(workflowIdx);
|
||||
expect(workflowIdx).toBeLessThan(secretIdx);
|
||||
expect(secretIdx).toBeLessThan(agentIdx);
|
||||
expect(agentIdx).toBeLessThan(prIdx);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progress state machine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("buildProgress", () => {
|
||||
test("marks first step as active when status matches it", () => {
|
||||
const items = buildProgress(
|
||||
"Getting repository information",
|
||||
"current",
|
||||
null,
|
||||
);
|
||||
|
||||
expect(items[0]?.active).toBe(true);
|
||||
expect(items[0]?.done).toBe(false);
|
||||
// All subsequent steps should be inactive and not done
|
||||
for (const item of items.slice(1)) {
|
||||
expect(item.active).toBe(false);
|
||||
expect(item.done).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("marks prior steps as done and current step as active", () => {
|
||||
const items = buildProgress("Creating workflow files", "current", null);
|
||||
const labels = items.map((i) => i.label);
|
||||
const workflowIdx = labels.indexOf("Creating workflow files");
|
||||
|
||||
// All steps before should be done
|
||||
for (let i = 0; i < workflowIdx; i++) {
|
||||
expect(items[i]?.done).toBe(true);
|
||||
expect(items[i]?.active).toBe(false);
|
||||
}
|
||||
// Current step should be active
|
||||
expect(items[workflowIdx]?.active).toBe(true);
|
||||
expect(items[workflowIdx]?.done).toBe(false);
|
||||
// Steps after should be neither done nor active
|
||||
for (let i = workflowIdx + 1; i < items.length; i++) {
|
||||
expect(items[i]?.done).toBe(false);
|
||||
expect(items[i]?.active).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("marks all prior steps done when on last step", () => {
|
||||
const items = buildProgress("Opening pull request page", "current", null);
|
||||
const lastIdx = items.length - 1;
|
||||
|
||||
for (let i = 0; i < lastIdx; i++) {
|
||||
expect(items[i]?.done).toBe(true);
|
||||
}
|
||||
expect(items[lastIdx]?.active).toBe(true);
|
||||
expect(items[lastIdx]?.done).toBe(false);
|
||||
});
|
||||
|
||||
test("no step is active when status doesn't match any step", () => {
|
||||
const items = buildProgress("Preparing setup...", "current", null);
|
||||
|
||||
for (const item of items) {
|
||||
expect(item.active).toBe(false);
|
||||
expect(item.done).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("matches status case-insensitively", () => {
|
||||
const items = buildProgress(
|
||||
"SETTING UP LETTA_API_KEY SECRET",
|
||||
"current",
|
||||
null,
|
||||
);
|
||||
const secretItem = items.find(
|
||||
(i) => i.label === "Setting up LETTA_API_KEY secret",
|
||||
);
|
||||
|
||||
expect(secretItem?.active).toBe(true);
|
||||
});
|
||||
|
||||
test("includes agent creation step in create mode progress", () => {
|
||||
const items = buildProgress("Creating agent My Bot", "create", "My Bot");
|
||||
const agentItem = items.find((i) => i.label === "Creating agent My Bot");
|
||||
|
||||
expect(agentItem).toBeDefined();
|
||||
expect(agentItem?.active).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Success screen / InstallGithubAppResult content
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("success screen content", () => {
|
||||
const baseResult: InstallGithubAppResult = {
|
||||
repo: "letta-ai/letta-code",
|
||||
workflowPath: ".github/workflows/letta.yml",
|
||||
branchName: "letta/install-github-app-abc123",
|
||||
pullRequestUrl: "https://github.com/letta-ai/letta-code/pull/42",
|
||||
pullRequestCreateMode: "created",
|
||||
committed: true,
|
||||
secretAction: "set",
|
||||
agentId: "agent-aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
||||
agentUrl:
|
||||
"https://app.letta.com/agents/agent-aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
||||
};
|
||||
|
||||
test("agentUrl points to app.letta.com ADE", () => {
|
||||
expect(baseResult.agentUrl).toBe(
|
||||
`https://app.letta.com/agents/${baseResult.agentId}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("agentUrl is null when agentId is null", () => {
|
||||
const noAgent: InstallGithubAppResult = {
|
||||
...baseResult,
|
||||
agentId: null,
|
||||
agentUrl: null,
|
||||
};
|
||||
expect(noAgent.agentUrl).toBeNull();
|
||||
});
|
||||
|
||||
test("secretAction is always 'set' after the API key fix", () => {
|
||||
// After removing reuseExistingSecret, the secret is always set
|
||||
expect(baseResult.secretAction).toBe("set");
|
||||
});
|
||||
|
||||
test("pullRequestUrl is present when workflow was committed", () => {
|
||||
expect(baseResult.committed).toBe(true);
|
||||
expect(baseResult.pullRequestUrl).toContain("github.com");
|
||||
expect(baseResult.pullRequestUrl).toContain("/pull/");
|
||||
});
|
||||
|
||||
test("pullRequestUrl is null when workflow was unchanged", () => {
|
||||
const unchanged: InstallGithubAppResult = {
|
||||
...baseResult,
|
||||
committed: false,
|
||||
branchName: null,
|
||||
pullRequestUrl: null,
|
||||
};
|
||||
expect(unchanged.pullRequestUrl).toBeNull();
|
||||
expect(unchanged.branchName).toBeNull();
|
||||
});
|
||||
|
||||
// Simulate the success lines built by the component to verify content
|
||||
test("success lines include expected content", () => {
|
||||
const result = baseResult;
|
||||
const successLines: string[] = [
|
||||
"✓ GitHub Actions workflow created!",
|
||||
"",
|
||||
"✓ API key saved as LETTA_API_KEY secret",
|
||||
];
|
||||
|
||||
if (result.agentId) {
|
||||
successLines.push("");
|
||||
successLines.push(`✓ Agent configured: ${result.agentId}`);
|
||||
}
|
||||
|
||||
successLines.push("");
|
||||
successLines.push("Next steps:");
|
||||
successLines.push("1. A pre-filled PR page has been created");
|
||||
successLines.push("2. Merge the PR to enable Letta Code PR assistance");
|
||||
successLines.push("3. Mention @letta-code in an issue or PR to test");
|
||||
|
||||
// Verify all expected content is present
|
||||
const allText = successLines.join("\n");
|
||||
expect(allText).toContain("GitHub Actions workflow created");
|
||||
expect(allText).toContain("API key saved as LETTA_API_KEY secret");
|
||||
expect(allText).toContain(`Agent configured: ${result.agentId}`);
|
||||
expect(allText).toContain("Merge the PR");
|
||||
expect(allText).toContain("@letta-code");
|
||||
});
|
||||
|
||||
test("success lines omit agent line when no agent configured", () => {
|
||||
const result: InstallGithubAppResult = {
|
||||
...baseResult,
|
||||
agentId: null,
|
||||
agentUrl: null,
|
||||
};
|
||||
const successLines: string[] = [
|
||||
"✓ GitHub Actions workflow created!",
|
||||
"",
|
||||
"✓ API key saved as LETTA_API_KEY secret",
|
||||
];
|
||||
|
||||
if (result.agentId) {
|
||||
successLines.push("");
|
||||
successLines.push(`✓ Agent configured: ${result.agentId}`);
|
||||
}
|
||||
|
||||
const allText = successLines.join("\n");
|
||||
expect(allText).not.toContain("Agent configured");
|
||||
});
|
||||
|
||||
test("agent URL uses correct ADE format for any agent ID", () => {
|
||||
const agentId = "agent-12345678-abcd-efgh-ijkl-123456789012";
|
||||
const expectedUrl = `https://app.letta.com/agents/${agentId}`;
|
||||
|
||||
// This mirrors the logic in installGithubApp
|
||||
const agentUrl = agentId ? `https://app.letta.com/agents/${agentId}` : null;
|
||||
|
||||
expect(agentUrl).toBe(expectedUrl);
|
||||
expect(agentUrl).toContain("app.letta.com/agents/");
|
||||
expect(agentUrl).toContain(agentId);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user