From 3e1057d9510b2e62937387297891150df7481cc2 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 20 Oct 2026 20:05:01 +0200 Subject: [PATCH 1/2] fix(ci): preserve portable fixtures and complete confirmation tab coverage --- .../2026-10-20-browser-fixture-and-focus-contracts.md | 9 +++++++++ job-tracker-ui/e2e/helpers/seed-operation.py | 6 +++--- job-tracker-ui/e2e/named-controls-modal-state.spec.ts | 5 +++++ job-tracker-ui/e2e/phase7-discovery.spec.ts | 4 ++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docs/infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md diff --git a/docs/infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md b/docs/infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md new file mode 100644 index 0000000..9a70299 --- /dev/null +++ b/docs/infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md @@ -0,0 +1,9 @@ +# Browser fixture portability and confirmation focus contracts + +Run832 at main `d1fcd2308755dfc112a2bd6a736dd763bbc895f9` completed its331-case browser suite:314 passed,16 failed, one flaky Rules draft case. Deployment was skipped. The Playwright runtime-corruption signature did not recur in this completed run. These outcomes do not establish production readiness or resolve the existing intermittent Rules observation. + +Eleven failures came from `seed-operation.py` calling Python3.12's `Path.is_junction` on the Debian runner's Python3.11.2 PosixPath. This was independently reproduced by running the unchanged helper in an isolated, network-disabled `node:20-bookworm` container on the runner host; host Python3.12.3 passed. Junction checks now execute only on Windows, where the existing Python3.12 guard is preserved. Absolute/direct-temp-root, symlink, hardlink, exact owner/schema and insert-only SQL protections remain unchanged. All29 self-checks pass both on local Windows and in that same Python3.11 container after the correction. No existing application data or production container is mounted into the test container. + +Five failures were stale keyboard expectations after the verified shared confirmation scroll-body accessibility correction. The focusable named body now belongs in the native tab cycle. The tests explicitly verify Cancel → reverse Tab to named body → reverse Tab to Confirm → forward Tab to body → forward Tab to Cancel, followed by the original cancellation/focus/draft/action proofs. No focus assertion, axe rule, timeout or application behavior is relaxed. All five affected native cases pass with zero skips/flaky/retries,20October18:03:21–18:04:21UTC. + +The bounded repair changes only the offline fixture and those two browser test consumers. Full release CI and deployed identity still require a new successful run. Production readiness remains NO-GO independently of the owner's authorization to publish main. The Qwen3.5 adapter preparation is separately tested; model activation and P5-13 qualification remain separate. diff --git a/job-tracker-ui/e2e/helpers/seed-operation.py b/job-tracker-ui/e2e/helpers/seed-operation.py index a672f23..e4feefe 100644 --- a/job-tracker-ui/e2e/helpers/seed-operation.py +++ b/job-tracker-ui/e2e/helpers/seed-operation.py @@ -69,11 +69,11 @@ def seed_operation(payload: object) -> str: except OSError as error: raise SeedDenied("The isolated dataRoot must already exist") from error if (root != requested or root.parent != temporary or not root.is_dir() - or requested.is_symlink() or requested.is_junction() + or requested.is_symlink() or (sys.platform == "win32" and requested.is_junction()) or not re.fullmatch(r"jobtracker-e2e-[A-Za-z0-9][A-Za-z0-9_-]*", root.name)): raise SeedDenied("Only an unlinked jobtracker-e2e-* direct child of system temp is allowed") database = root / "jobtracker.db" - if (not database.is_file() or database.is_symlink() or database.is_junction() + if (not database.is_file() or database.is_symlink() or (sys.platform == "win32" and database.is_junction()) or database.resolve(strict=True) != database or database.stat().st_nlink != 1): raise SeedDenied("The unlinked existing direct jobtracker.db file is required") # EF's SQLite Guid converter writes uppercase canonical text. @@ -110,7 +110,7 @@ def temporary_fixture(): yield root finally: assert root.parent == Path(tempfile.gettempdir()).resolve() - assert root.name.startswith("jobtracker-e2e-operation-selftest-") and not root.is_symlink() and not root.is_junction() + assert root.name.startswith("jobtracker-e2e-operation-selftest-") and not root.is_symlink() and not (sys.platform == "win32" and root.is_junction()) def self_test() -> dict: diff --git a/job-tracker-ui/e2e/named-controls-modal-state.spec.ts b/job-tracker-ui/e2e/named-controls-modal-state.spec.ts index a7f333d..7e2ba5d 100644 --- a/job-tracker-ui/e2e/named-controls-modal-state.spec.ts +++ b/job-tracker-ui/e2e/named-controls-modal-state.spec.ts @@ -171,8 +171,13 @@ for (const [mode, language, width] of [["light", "en", 1440], ["dark", "nb", 144 await expect(cancel).toBeFocused(); await expect(confirm).toHaveAccessibleDescription(t.attachmentsDeleteConfirm.replace("{name}", "Reviewed æøå.txt")); await cancel.press("Shift+Tab"); + const messageRegion = confirm.getByRole("region", { name: t.attachmentsDeleteTitle, exact: true }); + await expect(messageRegion).toBeFocused(); + await messageRegion.press("Shift+Tab"); await expect(confirm.getByRole("button", { name: t.attachmentsDelete, exact: true })).toBeFocused(); await page.keyboard.press("Tab"); + await expect(messageRegion).toBeFocused(); + await page.keyboard.press("Tab"); await expect(cancel).toBeFocused(); await scan(page, info, "confirm", true); await capture(page, info, `confirm-${suffix}`); diff --git a/job-tracker-ui/e2e/phase7-discovery.spec.ts b/job-tracker-ui/e2e/phase7-discovery.spec.ts index e08a921..3ced984 100644 --- a/job-tracker-ui/e2e/phase7-discovery.spec.ts +++ b/job-tracker-ui/e2e/phase7-discovery.spec.ts @@ -366,7 +366,11 @@ test('[P7-05 Discovery] keyboard native create and alert updates persist; delete const cancel = dialog.getByRole('button', { name: t.cancel, exact: true }); await expect(cancel).toBeFocused(); await page.keyboard.press('Shift+Tab'); + const messageRegion = dialog.getByRole('region', { name: await dialog.getByRole('heading').innerText(), exact: true }); + await expect(messageRegion).toBeFocused(); + await messageRegion.press('Shift+Tab'); await expect(dialog.getByRole('button', { name: t.adminUsersDelete, exact: true })).toBeFocused(); + await page.keyboard.press('Tab'); await expect(messageRegion).toBeFocused(); await page.keyboard.press('Tab'); await expect(cancel).toBeFocused(); await page.keyboard.press('Enter'); await expect(dialog).toHaveCount(0); await expect(deleteButton).toBeFocused(); From f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 20 Oct 2026 15:33:07 +0200 Subject: [PATCH 2/2] fix(ai): request final responses from Qwen3.5 --- .../2026-10-20-qwen35-compatibility.md | 9 ++++ tools/summarizer/app.py | 2 + tools/summarizer/tests/test_app.py | 50 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docs/infrastructure/2026-10-20-qwen35-compatibility.md diff --git a/docs/infrastructure/2026-10-20-qwen35-compatibility.md b/docs/infrastructure/2026-10-20-qwen35-compatibility.md new file mode 100644 index 0000000..d450996 --- /dev/null +++ b/docs/infrastructure/2026-10-20-qwen35-compatibility.md @@ -0,0 +1,9 @@ +# Qwen3.5 compatibility preparation + +The owner requested `qwen3.5:9b-q4_K_M` after live deployment. The model is downloaded, but the active sidecar still uses the previous model. This change prepares the existing adapter; it does not activate a model or qualify the withheld P5-13 capability. + +The existing adapter consumes Ollama's final `response`. A synthetic local probe found an empty response with the Qwen3.5 thinking default; a20October probe with `think:false` returned the requested synthetic JSON and no thinking text. The adapter now sets that request field only when the configured base family is exactly `qwen3.5`. Existing temperature, JSON mode, timeouts, response parsing, other model payloads and external-provider consent/fallback remain unchanged. Custom namespace aliases are outside this exact family match. + +Seven new offline cases verify both text/JSON for the requested tag and bare family, and unchanged payloads for qwen2.5, qwen3 and a lookalike family. Four Qwen3.5 cases failed ordinarily before the two-line change; three compatibility cases passed. The full sidecar suite then passed **56/56**, zero failures/errors/skips,8.45seconds,20October13:32UTC. The prior49-case baseline grows by seven; no model calls are part of these tests. Raw RED/GREEN logs and JUnit are retained under `.artifacts/release-20261020/model-compat/`. + +Live activation, deployed adapter identity, GPU residency and final-response smoke remain pending behind the release. P5-13's deployed-identity/366-call/independent-human-review/UI requirements remain unmet and its review feature stays disabled. Production readiness remains NO-GO independently of the owner's release authorization. diff --git a/tools/summarizer/app.py b/tools/summarizer/app.py index 9409640..85c2947 100644 --- a/tools/summarizer/app.py +++ b/tools/summarizer/app.py @@ -646,6 +646,8 @@ def _ollama_generate(prompt: str, *, json_mode: bool, temperature: float, timeou "stream": False, "options": {"temperature": temperature}, } + if OLLAMA_MODEL.split(":", 1)[0] == "qwen3.5": + payload["think"] = False if json_mode: payload["format"] = "json" body = _http_post_json(f"{OLLAMA_BASE_URL}/api/generate", payload, {}, timeout) diff --git a/tools/summarizer/tests/test_app.py b/tools/summarizer/tests/test_app.py index 866d683..5729c2e 100644 --- a/tools/summarizer/tests/test_app.py +++ b/tools/summarizer/tests/test_app.py @@ -8,6 +8,7 @@ import textwrap import zipfile from pathlib import Path +import pytest from docx import Document from fastapi.testclient import TestClient from PIL import Image @@ -253,6 +254,55 @@ def test_provider_defaults_to_ollama_and_is_unchanged(monkeypatch): assert captured["body"]["options"]["temperature"] == 0.1 +@pytest.mark.parametrize("ollama_model", ["qwen3.5:9b-q4_K_M", "qwen3.5"]) +@pytest.mark.parametrize("json_mode", [False, True]) +def test_qwen35_requests_nonthinking_output_without_consuming_thinking(monkeypatch, ollama_model, json_mode): + module = load_app_module(monkeypatch, ollama_model=ollama_model) + captured = {} + output = '{"score": 7}' if json_mode else "Rewritten CV text" + + def fake_urlopen(req, timeout=None): + captured["body"] = json.loads(req.data.decode("utf-8")) + captured["timeout"] = timeout + captured["url"] = req.full_url + return _FakeResponse({ + "response": output if captured["body"].get("think") is False else "", + "thinking": "Private reasoning must never become candidate text.", + }) + + monkeypatch.setattr(module.urllib_request, "urlopen", fake_urlopen) + result = module._ollama_generate_json("synthetic prompt") if json_mode else module._ollama_generate_text("synthetic prompt") + + assert result == ({"score": 7} if json_mode else output) + expected = { + "model": ollama_model, + "prompt": "synthetic prompt", + "stream": False, + "options": {"temperature": 0.1 if json_mode else 0.2}, + "think": False, + } + if json_mode: + expected["format"] = "json" + assert captured["body"] == expected + assert captured["url"] == f"{module.OLLAMA_BASE_URL}/api/generate" + assert captured["timeout"] == (120 if json_mode else 180) + + +@pytest.mark.parametrize("ollama_model", ["qwen2.5:7b", "qwen3:8b", "qwen3.5-plus:9b"]) +def test_other_ollama_model_payloads_remain_unchanged(monkeypatch, ollama_model): + module = load_app_module(monkeypatch, ollama_model=ollama_model) + captured = {} + _install_fake_urlopen(monkeypatch, module, {"response": " Candidate text ", "thinking": "Reasoning"}, captured) + + assert module._ollama_generate_text("synthetic prompt") == "Candidate text" + assert captured["body"] == { + "model": ollama_model, + "prompt": "synthetic prompt", + "stream": False, + "options": {"temperature": 0.2}, + } + + def test_local_success_wins_even_when_external_fallback_is_permitted(monkeypatch): monkeypatch.setenv("AI_PROVIDER", "gemini") monkeypatch.setenv("GEMINI_API_KEY", "test-key")