Skip to content

Commit 49baa37

Browse files
committed
Make a failed run say so, and stop shipping a file people fill with secrets
Three problems that all showed up as "it does nothing". A run that could not start printed nothing and exited 0. loguru begins with no sinks, src/logging.py removes them again and only adds one if LOG_TO_FILE or LOG_TO_CONSOLE is set, and both were False. main.py catches every exception, hands it to that silent logger and falls off the end of the function. Measured with a required file removed: empty stdout, empty stderr, exit 0, and the shell reporting success. Every other first-run problem went the same way, a wrong key, no Chrome, an unreachable posting. Console logging is on and the handlers exit 1. data_folder held the file the setup instructions tell you to put your API key in, and it was tracked. Filling it in staged a live credential on the next commit, which is exactly how one got in here. Same for plain_text_resume.yaml, which wants your address and phone number. Those three files are untracked and ignored now, and main.py creates them from data_folder_example on first run and tells you what to edit, so the manual copy step is gone as well. The error message pointed at Auto_Jobs_Applier_AIHawk, a name this repository has not had for a while. Tests for all of it, each checked against the failure it describes: reverting the console flag turns both visibility tests red, and the placeholder check now refuses six credential prefixes rather than one, verified against four.
1 parent 6ac213a commit 49baa37

9 files changed

Lines changed: 291 additions & 424 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,7 @@ venv.bak/
161161
# Mono Auto Generated Files
162162
mono_crash.*
163163

164-
job_applications/
164+
job_applications/
165+
# Your own key, your own CV, your own address. Created from data_folder_example
166+
# on first run. Was tracked until it leaked an API key.
167+
data_folder/*.yaml

README.md

Lines changed: 153 additions & 238 deletions
Large diffs are not rendered by default.

config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
LOG_LEVEL = 'ERROR'
77
LOG_SELENIUM_LEVEL = ERROR
88
LOG_TO_FILE = False
9-
LOG_TO_CONSOLE = False
9+
# Errors go through loguru, and loguru starts with every sink removed. With this
10+
# False the program was silent about everything: a missing config file, a bad API
11+
# key, an unreachable posting. It exited 0 and printed nothing.
12+
LOG_TO_CONSOLE = True
1013

1114
MINIMUM_WAIT_TIME_IN_SECONDS = 60
1215

data_folder/plain_text_resume.yaml

Lines changed: 0 additions & 130 deletions
This file was deleted.

data_folder/secrets.yaml

Lines changed: 0 additions & 1 deletion
This file was deleted.

data_folder/work_preferences.yaml

Lines changed: 0 additions & 47 deletions
This file was deleted.

main.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import shutil
23
import sys
34
from pathlib import Path
45
import traceback
@@ -186,6 +187,28 @@ class FileManager:
186187

187188
REQUIRED_FILES = [SECRETS_YAML, WORK_PREFERENCES_YAML, PLAIN_TEXT_RESUME_YAML]
188189

190+
@staticmethod
191+
def bootstrap_data_folder(app_data_folder: Path, example_folder: Path) -> List[str]:
192+
"""Create the data folder from the example on a fresh clone.
193+
194+
data_folder holds your API key, your CV and your address, so it is not in
195+
git. That used to mean a fresh clone had nothing there and the run died on
196+
a missing file. Copying it is a step nobody should have to read about.
197+
198+
Returns the names of the files it had to create, empty if there was
199+
nothing to do.
200+
"""
201+
created = []
202+
app_data_folder.mkdir(parents=True, exist_ok=True)
203+
for name in FileManager.REQUIRED_FILES:
204+
target = app_data_folder / name
205+
source = example_folder / name
206+
if target.exists() or not source.exists():
207+
continue
208+
shutil.copyfile(source, target)
209+
created.append(name)
210+
return created
211+
189212
@staticmethod
190213
def validate_data_folder(app_data_folder: Path) -> Tuple[Path, Path, Path, Path]:
191214
"""Validate the existence of the data folder and required files."""
@@ -529,6 +552,17 @@ def main():
529552
try:
530553
# Define and validate the data folder
531554
data_folder = Path("data_folder")
555+
created = FileManager.bootstrap_data_folder(data_folder, Path("data_folder_example"))
556+
if created:
557+
logger.error(
558+
f"Created {', '.join(created)} in {data_folder}/ from the example. "
559+
"Open them and put your own details in: plain_text_resume.yaml is "
560+
"your experience, work_preferences.yaml is what you are looking "
561+
"for, and secrets.yaml takes your OpenAI API key. Then run this "
562+
"again."
563+
)
564+
sys.exit(1)
565+
532566
secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)
533567

534568
# Validate configuration and secrets
@@ -549,16 +583,20 @@ def main():
549583
logger.error(f"Configuration error: {ce}")
550584
logger.error(
551585
"Refer to the configuration guide for troubleshooting: "
552-
"https://github.com/feder-cr/Auto_Jobs_Applier_AIHawk?tab=readme-ov-file#configuration"
586+
"https://github.com/feder-cr/Jobs_Applier_AI_Agent_AIHawk?tab=readme-ov-file#configuration"
553587
)
588+
sys.exit(1)
554589
except FileNotFoundError as fnf:
555590
logger.error(f"File not found: {fnf}")
556591
logger.error("Ensure all required files are present in the data folder.")
592+
sys.exit(1)
557593
except RuntimeError as re:
558594
logger.error(f"Runtime error: {re}")
559595
logger.debug(traceback.format_exc())
596+
sys.exit(1)
560597
except Exception as e:
561598
logger.exception(f"An unexpected error occurred: {e}")
599+
sys.exit(1)
562600

563601

564602
if __name__ == "__main__":

tests/test_failures_are_visible.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""A run that cannot proceed has to say so, on the terminal and in the exit code.
2+
3+
It did neither. loguru starts with no sinks, src/logging.py removes them again
4+
and only adds one if LOG_TO_FILE or LOG_TO_CONSOLE is set, and config.py had both
5+
False. main.py catches every exception, hands it to that sink-less logger and
6+
falls off the end of the function. Measured before the fix: a run with a required
7+
config file missing printed nothing at all on stdout or stderr and exited 0. The
8+
shell reported success.
9+
10+
Every other first-run problem went the same way: a wrong API key, no Chrome, an
11+
unreachable posting.
12+
"""
13+
14+
import os
15+
import shutil
16+
import subprocess
17+
import sys
18+
from pathlib import Path
19+
20+
ROOT = Path(__file__).resolve().parent.parent
21+
22+
23+
def run_in(folder: Path):
24+
env = {**os.environ, "PYTHONPATH": str(ROOT), "PYTHONIOENCODING": "utf-8"}
25+
return subprocess.run(
26+
[sys.executable, str(ROOT / "main.py")],
27+
cwd=folder,
28+
env=env,
29+
capture_output=True,
30+
text=True,
31+
timeout=120,
32+
stdin=subprocess.DEVNULL,
33+
)
34+
35+
36+
def prepare(folder: Path):
37+
"""Give the run a copy of the shipped example to work from."""
38+
shutil.copytree(ROOT / "data_folder_example", folder / "data_folder_example")
39+
return folder
40+
41+
42+
def test_a_fresh_clone_creates_the_data_folder_and_says_what_to_edit(tmp_path):
43+
prepare(tmp_path)
44+
45+
done = run_in(tmp_path)
46+
combined = done.stdout + done.stderr
47+
48+
for name in ("secrets.yaml", "work_preferences.yaml", "plain_text_resume.yaml"):
49+
assert (tmp_path / "data_folder" / name).exists(), f"{name} was not created"
50+
assert done.returncode != 0, "the first run has nothing to work with yet"
51+
assert "secrets.yaml" in combined, (
52+
"the run has to name what it created and ask for real values. Got:\n"
53+
f"stdout={done.stdout!r}\nstderr={done.stderr!r}"
54+
)
55+
56+
57+
def test_a_broken_config_is_reported_rather_than_swallowed(tmp_path):
58+
"""The bootstrap above cannot help with a file that exists and is wrong, which
59+
is the case that used to exit 0 in silence."""
60+
prepare(tmp_path)
61+
data = tmp_path / "data_folder"
62+
shutil.copytree(ROOT / "data_folder_example", data)
63+
prefs = data / "work_preferences.yaml"
64+
prefs.write_text(
65+
prefs.read_text(encoding="utf-8").replace("distance:", "distance_typo:"),
66+
encoding="utf-8",
67+
)
68+
69+
done = run_in(tmp_path)
70+
combined = done.stdout + done.stderr
71+
72+
assert done.returncode != 0, "an invalid config must not report success"
73+
assert "distance" in combined.lower(), (
74+
"the run has to name what is wrong with the config. Got:\n"
75+
f"stdout={done.stdout!r}\nstderr={done.stderr!r}"
76+
)

tests/test_shipped_config.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
import main
1515

16-
SHIPPED = ["data_folder", "data_folder_example"]
16+
# data_folder is no longer tracked: it holds the user's key and CV, and it is
17+
# created from this example on first run. The example is the only shipped copy.
18+
SHIPPED = ["data_folder_example"]
1719

1820

1921
@pytest.mark.parametrize("folder", SHIPPED)
@@ -29,13 +31,21 @@ def test_shipped_secrets_provide_a_key(folder):
2931
assert main.ConfigValidator.validate_secrets(Path(folder) / "secrets.yaml")
3032

3133

34+
# Prefixes the common providers put on their keys. sk- covers OpenAI and
35+
# Anthropic, AIza is Google, hf_ is HuggingFace, gsk_ is Groq. The project only
36+
# calls OpenAI today, but somebody pasting any of these into the shipped file is
37+
# the accident this guards.
38+
KEY_PREFIXES = ("sk-", "AIza", "hf_", "gsk_", "xai-", "pplx-")
39+
40+
3241
@pytest.mark.parametrize("folder", SHIPPED)
3342
def test_shipped_secrets_are_a_placeholder_and_not_a_real_key(folder):
3443
key = main.ConfigValidator.validate_secrets(Path(folder) / "secrets.yaml")
35-
assert not key.startswith("sk-"), (
36-
f"{folder}/secrets.yaml looks like a real API key rather than a placeholder. "
37-
"Credentials must never be committed here: put yours in the file locally and "
38-
"keep it out of the diff."
44+
looks_real = [p for p in KEY_PREFIXES if key.startswith(p)]
45+
assert not looks_real, (
46+
f"{folder}/secrets.yaml starts with {looks_real[0]!r}, which is what a real "
47+
"API key looks like rather than a placeholder. Credentials must never be "
48+
"committed here: put yours in the file locally and keep it out of the diff."
3949
)
4050

4151

0 commit comments

Comments
 (0)