arthrod commited on
Commit
eb90cab
·
1 Parent(s): 1068ad8
Files changed (2) hide show
  1. cicero_jobs/app.py +113 -126
  2. test_main.py +30 -29
cicero_jobs/app.py CHANGED
@@ -24,10 +24,12 @@ def _():
24
  import httpx
25
  import marimo as mo
26
  import mistune
 
27
  #
28
- #from htmltodocxpatch import html4docx as htmlpatch
 
29
 
30
- #HtmlToDocx = htmlpatch.HtmlToDocx
31
  from mistune.plugins.abbr import abbr
32
  from mistune.plugins.def_list import def_list
33
  from mistune.plugins.footnotes import footnotes
@@ -39,7 +41,9 @@ def _():
39
  return (
40
  Agent,
41
  BaseModel,
 
42
  Field,
 
43
  abbr,
44
  def_list,
45
  footnotes,
@@ -82,7 +86,7 @@ def _(mo):
82
  return
83
 
84
 
85
- @app.function
86
  def create_job_application_prompt(resume: str, cover_letter: str, fi: str | None = None) -> str:
87
  return f"""
88
  <JobApplicationGenerationRequest>
@@ -256,70 +260,45 @@ def create_job_application_prompt(resume: str, cover_letter: str, fi: str | None
256
  def _(BaseModel, Field):
257
  class ApplicationMaterials(BaseModel):
258
  """Structured output for resume and cover letter generation."""
259
- agent_has_spoken: bool = Field(..., description="Markdown Richly Formatted resume highlighting relevant experience for the job. 2 pages MAX, aim for 1.")
260
- resume: str = Field(..., description="Markdown Richly Formatted resume highlighting relevant experience for the job. 2 pages MAX, aim for 1.")
261
- cover_letter: str = Field(..., description="Markdown Richly Formatted and Compelling, personalized cover letter (3-4 paragraphs)")
262
- letter_to_recruiter: str = Field(
263
- ...,
264
- description="Markdown Richly Formatted and Concise professional message for LinkedIn/email outreach (5-7 sentences)",
265
- )
266
  tips: str = Field(
267
  ...,
268
- description="Markdown Richly Formatted and 5-7 actionable tips specific to this application, formatted as bullet points. MANDATORY: Must end wishing the candidate good luck and explaining finding a job is a numbers game. Motivate them!"
269
-
270
  )
271
  return (ApplicationMaterials,)
272
 
273
 
274
  @app.cell
275
  def _(mo):
276
- resume_button = mo.ui.file(
277
- kind="area",
278
- filetypes=[".pdf", ".txt", ".doc", ".docx"],
279
- label="Or drop your pdf/doc/docx/txt file résumé here!",
280
- )
281
 
282
- job_posting_area = mo.ui.text_area(placeholder="📋 Drop your job posting here!", full_width=True, rows=27)
283
 
284
- resume_area = mo.ui.text_area(
285
- placeholder="📄 Provide the full content of your résumé. Uploading file in pdf/docx/doc/txt is preferred.",
286
- full_width=True,
287
- rows=20,
288
- )
289
 
290
- job_url = mo.ui.text(
291
- placeholder="https://example.com/job-posting",
292
- label="Alternatively, provide the URL of the job posting",
293
- full_width=True,
294
- )
295
 
296
- continue_button = mo.ui.run_button(
297
- kind="success",
298
- tooltip="Click submit to continue.",
299
- label="Submit All (it takes a little bit, please be patient!)",
300
- keyboard_shortcut="Ctrl+Enter",
301
- full_width=True,
302
- )
303
 
304
  continue_render = mo.vstack(
305
  [
306
  mo.md("# Let's start with your materials!"),
307
  mo.vstack(
308
  [
309
- mo.md("## "),
310
  mo.hstack(
311
- [
312
- mo.vstack([mo.md("### Resume:"), resume_button, resume_area], align="center"),
313
- mo.vstack([mo.md("### Job Posting:"), job_posting_area, job_url], align="center"),
314
- ],
315
- align="center",
316
  ),
317
  continue_button,
318
  ],
319
- align="start",
320
  ),
321
  ],
322
- align="center",
323
  )
324
  return (
325
  continue_button,
@@ -349,11 +328,11 @@ def _(
349
  resume_button,
350
  ):
351
  mo.stop(continue_button.value is not True)
352
- if job_posting_area.value != "":
353
  buceta = 1
354
  job_posting = str(job_posting_area.value)
355
  print(job_posting)
356
- elif job_posting_area.value == "" and job_url.value == "":
357
  buceta = None
358
  job_posting = None
359
  print(job_posting_area.value)
@@ -361,20 +340,24 @@ def _(
361
  buceta = None
362
  job_posting = None
363
  print(job_posting_area.value)
364
- elif job_posting_area.value == "" and job_url.value != "":
365
  buceta = 1
 
366
  url_validation = DocumentUrl(job_url.value)
367
  print(url_validation)
368
- url = f"https://r.jina.ai/{job_url.value}"
369
 
370
- jina_api_key = os.environ.get("JINA_API_KEY", "")
371
- headers = {"Authorization": f"Bearer {jina_api_key}"}
372
  response = httpx.get(url, headers=headers)
373
  job_posting = response.text
374
  print(job_posting)
 
 
 
375
 
376
  try:
377
- if resume_button.contents() is not "" and resume_button.contents() is not None:
378
  resume_contents = resume_button.contents()
379
  try:
380
  from markitdown import MarkItDown as md_instance
@@ -384,7 +367,7 @@ def _(
384
  docx_converted = md.convert(source=forcing_bytes)
385
  resume_complete = str(docx_converted.text_content)
386
  print(resume_complete)
387
- print("0")
388
  except Exception:
389
  if isinstance(resume_contents, (bytes, bytearray)):
390
  from markitdown import MarkItDown as md_instance
@@ -393,16 +376,16 @@ def _(
393
  forcing_bytes = io.BytesIO(resume_contents)
394
  docx_converted = md.convert(forcing_bytes)
395
  resume_complete = str(docx_converted.text_content)
396
- print("1")
397
  else:
398
  resume_complete = resume_contents
399
- print("2")
400
  else:
401
- resume_complete = ""
402
- print("3")
403
  except Exception:
404
- resume_complete = ""
405
- print("4")
406
  return buceta, job_posting, resume_complete
407
 
408
 
@@ -419,39 +402,38 @@ async def _(
419
  mo.stop(continue_button.value is not True)
420
  mo.stop(buceta != 1)
421
  print(resume_complete, job_posting)
422
- final_prompt = create_job_application_prompt(resume_complete, job_posting, fi="uga")
423
 
424
- career_agent = Agent("gemini-2.5-flash", output_type=ApplicationMaterials)
425
  result = await career_agent.run(final_prompt)
426
 
427
 
428
  import json
429
- preview_section = mo.vstack(
430
- [
431
- mo.md("## Preview Your Documents"),
432
- mo.md("---"),
433
- mo.md("### Here is your résumé, how it looks like?"),
434
- mo.md("---"),
435
- mo.md(result.output.resume),
436
- mo.md("---"),
437
- mo.md("---"),
438
- mo.md("### 💼 Here is your Cover Letter"),
439
- mo.md("---"),
440
- mo.md(result.output.cover_letter),
441
- mo.md("---"),
442
- mo.md("---"),
443
- mo.md("### 📧 Here is your message to reach out to the recruiter"),
444
- mo.md("---"),
445
- mo.md(result.output.letter_to_recruiter),
446
- mo.md("---"),
447
- mo.md("---"),
448
- mo.md("### 💡 Here are some additional tips"),
449
- mo.md("---"),
450
- mo.md(result.output.tips),
451
- mo.md("---"),
452
- mo.md("---"),
453
- ]
454
- )
455
  agent_has_spoken = result.output.agent_has_spoken
456
  return agent_has_spoken, preview_section, result
457
 
@@ -489,71 +471,73 @@ def _(
489
  mo.stop(agent_has_spoken is not True)
490
  mo.stop(preview_section is None)
491
 
 
492
  def markdown_to_docx_bytes(markdown_text: str) -> bytes | None:
493
  """Synchronously converts markdown text to DOCX bytes with legal formatting."""
494
  try:
495
- MISTUNE_PLUGINS = [
496
- strikethrough,
497
- footnotes,
498
- table,
499
- task_lists,
500
- insert,
501
- def_list,
502
- abbr,
503
- mark,
504
- subscript,
505
- superscript,
506
- ]
507
- # html_renderer = mistune.HTMLRenderer()
508
  # markdown_instance = mistune.html(plugins=MISTUNE_PLUGINS, renderer=html_renderer)
509
 
510
- html_content = mistune.html(markdown_text, hard_wrap=True)
511
  try:
512
  parser = HtmlToDocx()
513
 
514
  docx_document = parser.parse_html_string(html_content)
515
  # Save to memory
516
 
517
-
518
  out = io.BytesIO()
519
  docx_document.save(out)
520
  doc_bytes = out.getvalue()
521
- print("uga0")
522
  return doc_bytes
523
  except Exception:
524
- print("uga1")
525
  pass
526
 
527
  except Exception:
528
- print("uga2")
529
  pass
530
 
531
- if result.output.resume is not None and result.output.resume is not "":
532
- final_resume = markdown_to_docx_bytes(result.output.resume)
533
- final_cover_letter = markdown_to_docx_bytes(result.output.cover_letter)
534
- final_letter_to_recruiter = markdown_to_docx_bytes(result.output.letter_to_recruiter)
535
-
536
- resume_download = mo.download(data=io.BytesIO(final_resume), filename="resume.docx", label="Résumé", mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
537
 
538
- cover_letter_download = mo.download(data=io.BytesIO(final_cover_letter), filename="cover_letter.docx", label="Cover Letter", mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
539
-
540
- message_download = mo.download(data=io.BytesIO(final_letter_to_recruiter), filename="letter_to_recruiter.docx", label="Recruiter Message", mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
541
-
542
- downloads_section = mo.vstack(
543
- [
544
- mo.md("## 🎉 Your Documents Are Ready!"),
545
- mo.md("Click the buttons below to download your documents:"),
546
- mo.hstack([resume_download, cover_letter_download, message_download], justify="center", gap=2),
547
- mo.md("---"),
548
- mo.callout(mo.md("✅ **Success!** Your documents have been generated and are ready to download."), kind="success"),
549
- ]
550
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  else:
552
  downloads_section = None
553
- return downloads_section
554
-
 
 
555
  @app.cell
556
- def _(mo, buceta, agent_has_spoken, preview_section, downloads_section):
557
  mo.stop(buceta != 1)
558
  mo.stop(agent_has_spoken is not True)
559
  mo.stop(preview_section is None)
@@ -561,7 +545,9 @@ def _(mo, buceta, agent_has_spoken, preview_section, downloads_section):
561
  downloads_section
562
  return
563
 
564
- def _(mo, buceta, agent_has_spoken, preview_section, downloads_section):
 
 
565
  mo.stop(buceta != 1)
566
  mo.stop(agent_has_spoken is not True)
567
  mo.stop(preview_section is None)
@@ -569,5 +555,6 @@ def _(mo, buceta, agent_has_spoken, preview_section, downloads_section):
569
  mo.md("""# <center>Time to do another application! Just add new materials. You got this!</center>""")
570
  return
571
 
 
572
  if __name__ == "__main__":
573
  app.run()
 
24
  import httpx
25
  import marimo as mo
26
  import mistune
27
+
28
  #
29
+ from html4docx import HtmlToDocx
30
+ from pydantic import DocumentUrl
31
 
32
+ # HtmlToDocx = htmlpatch.HtmlToDocx
33
  from mistune.plugins.abbr import abbr
34
  from mistune.plugins.def_list import def_list
35
  from mistune.plugins.footnotes import footnotes
 
41
  return (
42
  Agent,
43
  BaseModel,
44
+ DocumentUrl,
45
  Field,
46
+ HtmlToDocx,
47
  abbr,
48
  def_list,
49
  footnotes,
 
86
  return
87
 
88
 
89
+ @app.function(hide_code=True)
90
  def create_job_application_prompt(resume: str, cover_letter: str, fi: str | None = None) -> str:
91
  return f"""
92
  <JobApplicationGenerationRequest>
 
260
  def _(BaseModel, Field):
261
  class ApplicationMaterials(BaseModel):
262
  """Structured output for resume and cover letter generation."""
263
+
264
+ agent_has_spoken: bool = Field(..., description='Markdown Richly Formatted resume highlighting relevant experience for the job. 2 pages MAX, aim for 1.')
265
+ resume: str = Field(..., description='Markdown Richly Formatted resume highlighting relevant experience for the job. 2 pages MAX, aim for 1.')
266
+ cover_letter: str = Field(..., description='Markdown Richly Formatted and Compelling, personalized cover letter (3-4 paragraphs)')
267
+ letter_to_recruiter: str = Field(..., description='Markdown Richly Formatted and Concise professional message for LinkedIn/email outreach (5-7 sentences)')
 
 
268
  tips: str = Field(
269
  ...,
270
+ description='Markdown Richly Formatted and 5-7 actionable tips specific to this application, formatted as bullet points. MANDATORY: Must end wishing the candidate good luck and explaining finding a job is a numbers game. Motivate them!',
 
271
  )
272
  return (ApplicationMaterials,)
273
 
274
 
275
  @app.cell
276
  def _(mo):
277
+ resume_button = mo.ui.file(kind='area', filetypes=['.pdf', '.txt', '.doc', '.docx'], label='Or drop your pdf/doc/docx/txt file résumé here!')
 
 
 
 
278
 
279
+ job_posting_area = mo.ui.text_area(placeholder='📋 Drop your job posting here!', full_width=True, rows=27)
280
 
281
+ resume_area = mo.ui.text_area(placeholder='📄 Provide the full content of your résumé. Uploading file in pdf/docx/doc/txt is preferred.', full_width=True, rows=20)
 
 
 
 
282
 
283
+ job_url = mo.ui.text(placeholder='https://example.com/job-posting', label='Alternatively, provide the URL of the job posting', full_width=True)
 
 
 
 
284
 
285
+ continue_button = mo.ui.run_button(kind='success', tooltip='Click submit to continue.', label='Submit All (it takes a little bit, please be patient!)', keyboard_shortcut='Ctrl+Enter', full_width=True)
 
 
 
 
 
 
286
 
287
  continue_render = mo.vstack(
288
  [
289
  mo.md("# Let's start with your materials!"),
290
  mo.vstack(
291
  [
292
+ mo.md('## '),
293
  mo.hstack(
294
+ [mo.vstack([mo.md('### Resume:'), resume_button, resume_area], align='center'), mo.vstack([mo.md('### Job Posting:'), job_posting_area, job_url], align='center')], align='center'
 
 
 
 
295
  ),
296
  continue_button,
297
  ],
298
+ align='start',
299
  ),
300
  ],
301
+ align='center',
302
  )
303
  return (
304
  continue_button,
 
328
  resume_button,
329
  ):
330
  mo.stop(continue_button.value is not True)
331
+ if job_posting_area.value != '':
332
  buceta = 1
333
  job_posting = str(job_posting_area.value)
334
  print(job_posting)
335
+ elif job_posting_area.value == '' and job_url.value == '':
336
  buceta = None
337
  job_posting = None
338
  print(job_posting_area.value)
 
340
  buceta = None
341
  job_posting = None
342
  print(job_posting_area.value)
343
+ elif job_posting_area.value is '' and job_url.value is not '':
344
  buceta = 1
345
+
346
  url_validation = DocumentUrl(job_url.value)
347
  print(url_validation)
348
+ url = f'https://r.jina.ai/{job_url.value}'
349
 
350
+ jina_api_key = os.environ.get('JINA_API_KEY', '')
351
+ headers = {'Authorization': f'Bearer {jina_api_key}'}
352
  response = httpx.get(url, headers=headers)
353
  job_posting = response.text
354
  print(job_posting)
355
+ else:
356
+ job_posting = ''
357
+ buceta = None
358
 
359
  try:
360
+ if resume_button.contents() is not '' and resume_button.contents() is not None:
361
  resume_contents = resume_button.contents()
362
  try:
363
  from markitdown import MarkItDown as md_instance
 
367
  docx_converted = md.convert(source=forcing_bytes)
368
  resume_complete = str(docx_converted.text_content)
369
  print(resume_complete)
370
+ print('0')
371
  except Exception:
372
  if isinstance(resume_contents, (bytes, bytearray)):
373
  from markitdown import MarkItDown as md_instance
 
376
  forcing_bytes = io.BytesIO(resume_contents)
377
  docx_converted = md.convert(forcing_bytes)
378
  resume_complete = str(docx_converted.text_content)
379
+ print('1')
380
  else:
381
  resume_complete = resume_contents
382
+ print('2')
383
  else:
384
+ resume_complete = ''
385
+ print('3')
386
  except Exception:
387
+ resume_complete = ''
388
+ print('4')
389
  return buceta, job_posting, resume_complete
390
 
391
 
 
402
  mo.stop(continue_button.value is not True)
403
  mo.stop(buceta != 1)
404
  print(resume_complete, job_posting)
405
+ final_prompt = create_job_application_prompt(resume_complete, job_posting, fi='uga')
406
 
407
+ career_agent = Agent('gemini-2.5-flash', output_type=ApplicationMaterials)
408
  result = await career_agent.run(final_prompt)
409
 
410
 
411
  import json
412
+
413
+ preview_section = mo.vstack([
414
+ mo.md('## Preview Your Documents'),
415
+ mo.md('---'),
416
+ mo.md('### Here is your résumé, how it looks like?'),
417
+ mo.md('---'),
418
+ mo.md(result.output.resume),
419
+ mo.md('---'),
420
+ mo.md('---'),
421
+ mo.md('### 💼 Here is your Cover Letter'),
422
+ mo.md('---'),
423
+ mo.md(result.output.cover_letter),
424
+ mo.md('---'),
425
+ mo.md('---'),
426
+ mo.md('### 📧 Here is your message to reach out to the recruiter'),
427
+ mo.md('---'),
428
+ mo.md(result.output.letter_to_recruiter),
429
+ mo.md('---'),
430
+ mo.md('---'),
431
+ mo.md('### 💡 Here are some additional tips'),
432
+ mo.md('---'),
433
+ mo.md(result.output.tips),
434
+ mo.md('---'),
435
+ mo.md('---'),
436
+ ])
 
437
  agent_has_spoken = result.output.agent_has_spoken
438
  return agent_has_spoken, preview_section, result
439
 
 
471
  mo.stop(agent_has_spoken is not True)
472
  mo.stop(preview_section is None)
473
 
474
+
475
  def markdown_to_docx_bytes(markdown_text: str) -> bytes | None:
476
  """Synchronously converts markdown text to DOCX bytes with legal formatting."""
477
  try:
478
+ MISTUNE_PLUGINS = [strikethrough, footnotes, table, task_lists, insert, def_list, abbr, mark, subscript, superscript]
479
+ html_renderer = mistune.HTMLRenderer()
 
 
 
 
 
 
 
 
 
 
 
480
  # markdown_instance = mistune.html(plugins=MISTUNE_PLUGINS, renderer=html_renderer)
481
 
482
+ html_content = mistune.html(markdown_text)
483
  try:
484
  parser = HtmlToDocx()
485
 
486
  docx_document = parser.parse_html_string(html_content)
487
  # Save to memory
488
 
 
489
  out = io.BytesIO()
490
  docx_document.save(out)
491
  doc_bytes = out.getvalue()
492
+ print('uga0')
493
  return doc_bytes
494
  except Exception:
495
+ print('uga1')
496
  pass
497
 
498
  except Exception:
499
+ print('uga2')
500
  pass
501
 
 
 
 
 
 
 
502
 
503
+ if result.output.resume is not None:
504
+ if result.output.resume is not '':
505
+ final_resume = markdown_to_docx_bytes(result.output.resume)
506
+ final_cover_letter = markdown_to_docx_bytes(result.output.cover_letter)
507
+ final_letter_to_recruiter = markdown_to_docx_bytes(result.output.letter_to_recruiter)
508
+ if final_resume is not None:
509
+ resume_download = mo.download(data=(final_resume), filename='resume.docx', label='Résumé', mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document')
510
+ if final_cover_letter is not None:
511
+ cover_letter_download = mo.download(
512
+ data=(final_cover_letter), filename='cover_letter.docx', label='Cover Letter', mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
513
+ )
514
+ if final_letter_to_recruiter is not None:
515
+ message_download = mo.download(
516
+ data=(final_letter_to_recruiter), filename='letter_to_recruiter.docx', label='Recruiter Message', mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
517
+ )
518
+
519
+ downloads_section = mo.vstack([
520
+ mo.md('## 🎉 Your Documents Are Ready!'),
521
+ mo.md('Click the buttons below to download your documents:'),
522
+ mo.hstack([resume_download, cover_letter_download, message_download], justify='center', gap=2),
523
+ mo.md('---'),
524
+ mo.callout(mo.md('✅ **Success!** Your documents have been generated and are ready to download.'), kind='success'),
525
+ ])
526
+ print('we have downloads-section')
527
+ else:
528
+ downloads_section = None
529
+ print('we dont have downloads-section')
530
+ else:
531
+ downloads_section = None
532
+ print('we dont have downloads-section')
533
  else:
534
  downloads_section = None
535
+ print('we dont have downloads-section')
536
+ return (downloads_section,)
537
+
538
+
539
  @app.cell
540
+ def _(agent_has_spoken, buceta, downloads_section, mo, preview_section):
541
  mo.stop(buceta != 1)
542
  mo.stop(agent_has_spoken is not True)
543
  mo.stop(preview_section is None)
 
545
  downloads_section
546
  return
547
 
548
+
549
+ @app.cell
550
+ def _(agent_has_spoken, buceta, downloads_section, mo, preview_section):
551
  mo.stop(buceta != 1)
552
  mo.stop(agent_has_spoken is not True)
553
  mo.stop(preview_section is None)
 
555
  mo.md("""# <center>Time to do another application! Just add new materials. You got this!</center>""")
556
  return
557
 
558
+
559
  if __name__ == "__main__":
560
  app.run()
test_main.py CHANGED
@@ -4,89 +4,90 @@ Unit tests for the main FastAPI application.
4
 
5
  import pytest
6
  from fastapi.testclient import TestClient
 
7
  from _server.main import app
8
 
9
 
10
  class TestRootEndpoint:
11
  """Test cases for the root endpoint."""
12
-
13
  @pytest.fixture
14
  def client(self):
15
  """Create a test client for the FastAPI app."""
16
  return TestClient(app)
17
-
18
  def test_root_redirects_to_cicero_jobs_app(self, client):
19
  """Test that the root endpoint "/" redirects to "/cicero_jobs/app"."""
20
- response = client.get("/", follow_redirects=False)
21
-
22
  # Assert it's a redirect response
23
  assert response.status_code == 307
24
-
25
  # Assert the location header points to the correct URL
26
- assert response.headers["location"] == "/cicero_jobs/app"
27
-
28
  def test_root_redirect_response_type(self, client):
29
  """Test that the root endpoint returns a RedirectResponse."""
30
  response = client.get("/", follow_redirects=False)
31
-
32
  # Check status code for temporary redirect
33
  assert response.status_code == 307
34
-
35
  # Verify it's a proper redirect response
36
  assert "location" in response.headers
37
-
38
  def test_root_redirect_follows_correctly(self, client):
39
  """Test that following the redirect leads to the expected path."""
40
  # First, get the redirect response
41
- redirect_response = client.get("/", follow_redirects=False)
42
- redirect_url = redirect_response.headers["location"]
43
-
44
  # Then try to access the redirected URL
45
- # Note: This might fail if the marimo app isn't fully set up,
46
  # but we're testing the redirect behavior
47
  final_response = client.get(redirect_url, follow_redirects=False)
48
-
49
  # The redirect URL should be accessible (even if it returns an error due to marimo setup)
50
  # We're mainly testing that the redirect target exists in our app
51
- assert redirect_url == "/cicero_jobs/app"
52
-
53
  def test_root_redirect_preserves_method(self, client):
54
  """Test that GET method is preserved in redirect."""
55
  response = client.get("/", follow_redirects=False)
56
-
57
  assert response.status_code == 307 # Temporary redirect preserves method
58
- assert response.headers["location"] == "/cicero_jobs/app"
59
 
60
 
61
  # Additional test class for edge cases
62
  class TestRootEndpointEdgeCases:
63
  """Test edge cases for the root endpoint."""
64
-
65
  @pytest.fixture
66
  def client(self):
67
  """Create a test client for the FastAPI app."""
68
  return TestClient(app)
69
-
70
  def test_root_with_query_parameters(self, client):
71
  """Test root endpoint behavior with query parameters."""
72
- response = client.get("/?param=value", follow_redirects=False)
73
-
74
  # Should still redirect regardless of query parameters
75
  assert response.status_code == 307
76
  assert response.headers["location"] == "/cicero_jobs/app"
77
-
78
  def test_root_post_method(self, client):
79
  """Test that POST to root also redirects (if supported)."""
80
- response = client.post("/", follow_redirects=False)
81
-
82
  # FastAPI should either redirect or return method not allowed
83
  # Since we only defined GET, this should return 405 Method Not Allowed
84
  assert response.status_code == 405
85
-
86
  def test_root_with_trailing_slash_consistency(self, client):
87
  """Test that root endpoint handles trailing slashes consistently."""
88
- response1 = client.get("/", follow_redirects=False)
89
-
90
  # Both should behave the same way
91
  assert response1.status_code == 307
92
  assert response1.headers["location"] == "/cicero_jobs/app"
 
4
 
5
  import pytest
6
  from fastapi.testclient import TestClient
7
+
8
  from _server.main import app
9
 
10
 
11
  class TestRootEndpoint:
12
  """Test cases for the root endpoint."""
13
+
14
  @pytest.fixture
15
  def client(self):
16
  """Create a test client for the FastAPI app."""
17
  return TestClient(app)
18
+
19
  def test_root_redirects_to_cicero_jobs_app(self, client):
20
  """Test that the root endpoint "/" redirects to "/cicero_jobs/app"."""
21
+ response = client.get('/', follow_redirects=False)
22
+
23
  # Assert it's a redirect response
24
  assert response.status_code == 307
25
+
26
  # Assert the location header points to the correct URL
27
+ assert response.headers['location'] == '/cicero_jobs/app'
28
+
29
  def test_root_redirect_response_type(self, client):
30
  """Test that the root endpoint returns a RedirectResponse."""
31
  response = client.get("/", follow_redirects=False)
32
+
33
  # Check status code for temporary redirect
34
  assert response.status_code == 307
35
+
36
  # Verify it's a proper redirect response
37
  assert "location" in response.headers
38
+
39
  def test_root_redirect_follows_correctly(self, client):
40
  """Test that following the redirect leads to the expected path."""
41
  # First, get the redirect response
42
+ redirect_response = client.get('/', follow_redirects=False)
43
+ redirect_url = redirect_response.headers['location']
44
+
45
  # Then try to access the redirected URL
46
+ # Note: This might fail if the marimo app isn't fully set up,
47
  # but we're testing the redirect behavior
48
  final_response = client.get(redirect_url, follow_redirects=False)
49
+
50
  # The redirect URL should be accessible (even if it returns an error due to marimo setup)
51
  # We're mainly testing that the redirect target exists in our app
52
+ assert redirect_url == '/cicero_jobs/app'
53
+
54
  def test_root_redirect_preserves_method(self, client):
55
  """Test that GET method is preserved in redirect."""
56
  response = client.get("/", follow_redirects=False)
57
+
58
  assert response.status_code == 307 # Temporary redirect preserves method
59
+ assert response.headers['location'] == '/cicero_jobs/app'
60
 
61
 
62
  # Additional test class for edge cases
63
  class TestRootEndpointEdgeCases:
64
  """Test edge cases for the root endpoint."""
65
+
66
  @pytest.fixture
67
  def client(self):
68
  """Create a test client for the FastAPI app."""
69
  return TestClient(app)
70
+
71
  def test_root_with_query_parameters(self, client):
72
  """Test root endpoint behavior with query parameters."""
73
+ response = client.get('/?param=value', follow_redirects=False)
74
+
75
  # Should still redirect regardless of query parameters
76
  assert response.status_code == 307
77
  assert response.headers["location"] == "/cicero_jobs/app"
78
+
79
  def test_root_post_method(self, client):
80
  """Test that POST to root also redirects (if supported)."""
81
+ response = client.post('/', follow_redirects=False)
82
+
83
  # FastAPI should either redirect or return method not allowed
84
  # Since we only defined GET, this should return 405 Method Not Allowed
85
  assert response.status_code == 405
86
+
87
  def test_root_with_trailing_slash_consistency(self, client):
88
  """Test that root endpoint handles trailing slashes consistently."""
89
+ response1 = client.get('/', follow_redirects=False)
90
+
91
  # Both should behave the same way
92
  assert response1.status_code == 307
93
  assert response1.headers["location"] == "/cicero_jobs/app"