Skip to content

Commit b9082e9

Browse files
committed
bugfix-309-setup-crash: Improve CLI error handling by ensuring early exit when no valid PERSONAL_ACCESS_TOKEN is provided. Update tests to verify correct logging and exit behavior based on the presence of a .env file, enhancing user guidance during setup.
1 parent 6325e66 commit b9082e9

5 files changed

Lines changed: 121 additions & 1 deletion

File tree

build/cli/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47664,6 +47664,7 @@ program
4766447664
(0, logger_1.logInfo)(' • Or create a .env file in this repo with: PERSONAL_ACCESS_TOKEN=your_github_token');
4766547665
}
4766647666
process.exit(1);
47667+
return;
4766747668
}
4766847669
(0, logger_1.logInfo)('⚙️ Running initial setup (labels, issue types, access)...');
4766947670
const params = {

src/__tests__/cli.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ jest.mock('../data/repository/ai_repository', () => ({
3434
})),
3535
}));
3636

37-
jest.mock('../utils/setup_files', () => jest.requireActual<typeof import('../utils/setup_files')>('../utils/setup_files'));
37+
const mockGetSetupToken = jest.fn();
38+
const mockSetupEnvFileExists = jest.fn();
39+
jest.mock('../utils/setup_files', () => {
40+
const actual = jest.requireActual<typeof import('../utils/setup_files')>('../utils/setup_files');
41+
return {
42+
...actual,
43+
getSetupToken: (...args: unknown[]) => mockGetSetupToken(...args),
44+
setupEnvFileExists: (...args: unknown[]) => mockSetupEnvFileExists(...args),
45+
};
46+
});
3847

3948
describe('CLI', () => {
4049
let exitSpy: jest.SpyInstance;
@@ -49,6 +58,11 @@ describe('CLI', () => {
4958
mockIsIssue.mockResolvedValue(true);
5059
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
5160
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
61+
// Default: return override token when present (so setup --token ... still works)
62+
mockGetSetupToken.mockImplementation((_cwd: string, override?: string) =>
63+
override?.trim() && override.trim().length >= 20 ? override.trim() : undefined
64+
);
65+
mockSetupEnvFileExists.mockReturnValue(false);
5266
});
5367

5468
afterEach(() => {
@@ -330,6 +344,34 @@ describe('CLI', () => {
330344
const ranWithValidRepo = runCalls.length > 0 && runCalls[0][0]?.repo?.owner && runCalls[0][0]?.repo?.repo;
331345
expect(ranWithValidRepo).not.toBe(true);
332346
});
347+
348+
it('exits when no valid token and suggests creating .env when .env does not exist', async () => {
349+
mockGetSetupToken.mockReturnValue(undefined);
350+
mockSetupEnvFileExists.mockReturnValue(false);
351+
const { logError, logInfo } = require('../utils/logger');
352+
(runLocalAction as jest.Mock).mockClear();
353+
354+
await program.parseAsync(['node', 'cli', 'setup']);
355+
356+
expect(logError).toHaveBeenCalledWith(expect.stringContaining('Setup requires PERSONAL_ACCESS_TOKEN'));
357+
expect(logInfo).toHaveBeenCalledWith(expect.stringContaining('create a .env file'));
358+
expect(runLocalAction).not.toHaveBeenCalled();
359+
expect(exitSpy).toHaveBeenCalledWith(1);
360+
});
361+
362+
it('exits when no valid token and suggests adding to existing .env when .env exists', async () => {
363+
mockGetSetupToken.mockReturnValue(undefined);
364+
mockSetupEnvFileExists.mockReturnValue(true);
365+
const { logError, logInfo } = require('../utils/logger');
366+
(runLocalAction as jest.Mock).mockClear();
367+
368+
await program.parseAsync(['node', 'cli', 'setup']);
369+
370+
expect(logError).toHaveBeenCalledWith(expect.stringContaining('Setup requires PERSONAL_ACCESS_TOKEN'));
371+
expect(logInfo).toHaveBeenCalledWith(expect.stringContaining('existing .env file'));
372+
expect(runLocalAction).not.toHaveBeenCalled();
373+
expect(exitSpy).toHaveBeenCalledWith(1);
374+
});
333375
});
334376

335377
describe('detect-potential-problems', () => {

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ program
456456
logInfo(' • Or create a .env file in this repo with: PERSONAL_ACCESS_TOKEN=your_github_token');
457457
}
458458
process.exit(1);
459+
return;
459460
}
460461

461462
logInfo('⚙️ Running initial setup (labels, issue types, access)...');

src/data/repository/__tests__/project_repository.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,15 +804,22 @@ describe("ProjectRepository.setTaskPriority", () => {
804804
});
805805

806806
it("throws when content id not found for issue (issue/PR not in repo)", async () => {
807+
const { logError } = require("../../../utils/logger");
808+
(logError as jest.Mock).mockClear();
807809
mockGraphql.mockResolvedValueOnce({
808810
repository: { issueOrPullRequest: null },
809811
});
810812
await expect(
811813
repo.setTaskPriority(project, "owner", "repo", 999, "High", "token")
812814
).rejects.toThrow("Content ID not found");
815+
expect(logError).toHaveBeenCalledWith(
816+
expect.stringContaining("999 not found in repository")
817+
);
813818
});
814819

815820
it("throws when project node is null (invalid project ID)", async () => {
821+
const { logError } = require("../../../utils/logger");
822+
(logError as jest.Mock).mockClear();
816823
mockGraphql
817824
.mockResolvedValueOnce({
818825
repository: { issueOrPullRequest: { id: "I_issue1" } },
@@ -821,9 +828,14 @@ describe("ProjectRepository.setTaskPriority", () => {
821828
await expect(
822829
repo.setTaskPriority(project, "owner", "repo", 1, "High", "token")
823830
).rejects.toThrow("Project not found or invalid project ID");
831+
expect(logError).toHaveBeenCalledWith(
832+
expect.stringContaining("Project not found for ID")
833+
);
824834
});
825835

826836
it("throws when issue/PR not in project yet", async () => {
837+
const { logError } = require("../../../utils/logger");
838+
(logError as jest.Mock).mockClear();
827839
mockGraphql
828840
.mockResolvedValueOnce({
829841
repository: { issueOrPullRequest: { id: "I_issue1" } },
@@ -839,5 +851,59 @@ describe("ProjectRepository.setTaskPriority", () => {
839851
await expect(
840852
repo.setTaskPriority(project, "owner", "repo", 1, "High", "token")
841853
).rejects.toThrow("not in the project yet");
854+
expect(logError).toHaveBeenCalledWith(
855+
expect.stringContaining("not found in project after checking")
856+
);
857+
});
858+
859+
it("logs error when hasNextPage is true but endCursor is null", async () => {
860+
const { logError } = require("../../../utils/logger");
861+
(logError as jest.Mock).mockClear();
862+
mockGraphql
863+
.mockResolvedValueOnce({
864+
repository: { issueOrPullRequest: { id: "I_issue1" } },
865+
})
866+
.mockResolvedValueOnce({
867+
node: {
868+
items: {
869+
nodes: [{ id: "other", content: { id: "I_other" } }],
870+
pageInfo: { hasNextPage: true, endCursor: null },
871+
},
872+
},
873+
});
874+
await expect(
875+
repo.setTaskPriority(project, "owner", "repo", 1, "High", "token")
876+
).rejects.toThrow("not in the project yet");
877+
expect(logError).toHaveBeenCalledWith(
878+
expect.stringContaining("hasNextPage is true but endCursor is null")
879+
);
880+
});
881+
882+
it("stops after maxPages and throws when item not found", async () => {
883+
const { logError } = require("../../../utils/logger");
884+
(logError as jest.Mock).mockClear();
885+
const pageWithNext = {
886+
node: {
887+
items: {
888+
nodes: Array.from({ length: 100 }, (_, i) => ({
889+
id: `item_${i}`,
890+
content: { id: `I_other_${i}` },
891+
})),
892+
pageInfo: { hasNextPage: true, endCursor: "next" },
893+
},
894+
},
895+
};
896+
mockGraphql.mockResolvedValueOnce({
897+
repository: { issueOrPullRequest: { id: "I_issue1" } },
898+
});
899+
for (let p = 0; p < 100; p++) {
900+
mockGraphql.mockResolvedValueOnce(pageWithNext);
901+
}
902+
await expect(
903+
repo.setTaskPriority(project, "owner", "repo", 1, "High", "token")
904+
).rejects.toThrow("not in the project yet");
905+
expect(logError).toHaveBeenCalledWith(
906+
expect.stringContaining("Stopped after 100 pages")
907+
);
842908
});
843909
});

src/utils/__tests__/setup_files.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ describe('setup_files', () => {
8787
expect(fs.readFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), 'utf8')).toBe('# PR template');
8888
});
8989

90+
it('skips pull_request_template.md when destination already exists', () => {
91+
fs.mkdirSync(path.join(tmpDir, 'setup'), { recursive: true });
92+
fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true });
93+
fs.writeFileSync(path.join(tmpDir, 'setup', 'pull_request_template.md'), '# from setup');
94+
fs.writeFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), '# existing');
95+
const result = copySetupFiles(tmpDir, setupDir());
96+
expect(result.skipped).toBe(1);
97+
expect(fs.readFileSync(path.join(tmpDir, '.github', 'pull_request_template.md'), 'utf8')).toBe('# existing');
98+
});
99+
90100
it('does not create .env when no token in env and no .env (only suggests via log)', () => {
91101
const saved = process.env[ENV_TOKEN_KEY];
92102
delete process.env[ENV_TOKEN_KEY];

0 commit comments

Comments
 (0)