Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ def compose_turn_system_prompt(
tz_name = tz_name or "UTC"
now_local = datetime.now(ZoneInfo(tz_name))
tz_abbr = now_local.strftime("%Z") or tz_name
# No `%-d` / `%-I`: those are glibc extensions. On Windows strftime raises ValueError on
# them, the except below swallowed it, and the agent shipped without a clock on that OS.
hour = now_local.strftime("%I:%M %p").lstrip("0")
time_ctx = (
"<current_time>\n"
f"Today is {now_local.strftime('%A, %B %-d, %Y')}.\n"
f"Local time: {now_local.strftime('%-I:%M %p')} {tz_abbr} ({tz_name}).\n"
f"Today is {now_local.strftime('%A, %B')} {now_local.day}, {now_local.year}.\n"
f"Local time: {hour} {tz_abbr} ({tz_name}).\n"
"Use this as ground truth for any date/time/day-of-week question. The timezone also "
"gives the user's coarse region; when they say 'here' or 'near me' without a place, "
"infer the likely city from it (say you inferred it) instead of claiming you can't know.\n"
Expand Down
2 changes: 1 addition & 1 deletion backend/apps/outputs/publish_capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def p_api_callers(root: str) -> List[str]:
continue
with open(full, "r", encoding="utf-8", errors="replace") as fh:
if P_API_CALL.search(fh.read()):
hits.append(os.path.relpath(full, root))
hits.append(os.path.relpath(full, root).replace(os.sep, "/"))
except OSError:
continue
return sorted(hits)
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/skills/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,8 @@ async def list_skill_files(skill_id: str):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for n in sorted(names):
path = os.path.join(root, n)
rel = os.path.relpath(path, base_abs)
# API paths are posix on every OS: the frontend joins them with "/" and the SKILL.md-first sort compares them as strings.
rel = os.path.relpath(path, base_abs).replace(os.sep, "/")
if n.startswith(".") or os.path.getsize(path) > 512_000:
continue
try:
Expand Down
4 changes: 3 additions & 1 deletion backend/apps/swarm/entities/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def p_read_supporting_files(skill_dir: str) -> dict[str, bytes]:
for root, p_dirs, names in os.walk(skill_dir):
for n in names:
full = os.path.join(root, n)
rel = os.path.relpath(full, skill_dir)
# Forward slashes: these keys travel inside .swarm bundles, and the importer joins them
# onto the destination on whatever OS opens the bundle.
rel = os.path.relpath(full, skill_dir).replace(os.sep, "/")
if rel == "SKILL.md" or n.startswith("."):
continue
try:
Expand Down
7 changes: 4 additions & 3 deletions backend/tests/test_app_export_no_stale_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def p_make_app(ws_root, *, workspace: bool) -> str:
if wsid:
folder = ws_root / wsid
folder.mkdir()
(folder / "app.py").write_text("print('v1')\n")
(folder / "app.py").write_text("print('v1')\n", newline="\n")
o = Output(
name="Demo", description="", icon="view_quilt",
input_schema={"type": "object", "properties": {}, "required": []},
Expand All @@ -50,8 +50,9 @@ def test_workspace_app_export_omits_stale_inline_files(p_ws_root):
o = workspace_io.load_output(oid)
folder = p_ws_root / o.workspace_id
# agent edit -> v2 on disk: modify app.py + add new.py (output.files stays v1)
(folder / "app.py").write_text("print('v2 EDITED')\n")
(folder / "new.py").write_text("print('v2 NEW')\n")
# newline="\n": the export is compared byte-for-byte below, and text mode would write CRLF on Windows.
(folder / "app.py").write_text("print('v2 EDITED')\n", newline="\n")
(folder / "new.py").write_text("print('v2 NEW')\n", newline="\n")

exp = AppExportable.load(oid)
payload = exp.serialize(P_Ctx())
Expand Down
4 changes: 3 additions & 1 deletion backend/tests/test_browser_cookies_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ def test_truncated_ciphertext_degrades(on_windows):
assert bc.decrypt_cookie_value(b"v10" + NONCE, KEY) is None


def test_the_mac_branch_does_not_try_gcm():
def test_the_mac_branch_does_not_try_gcm(monkeypatch):
"""Same bytes, no Windows flag: the CBC branch must not accidentally accept a GCM blob."""
# Pin the flag rather than the host: on a Windows runner IS_WIN is naturally True and the GCM branch would (correctly) decrypt.
monkeypatch.setattr(bc, "IS_WIN", False)
assert bc.decrypt_cookie_value(p_win_blob(b"sessionid=abc123"), KEY) is None


Expand Down
7 changes: 5 additions & 2 deletions backend/tests/test_browser_login_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ def test_first_seen_preserved_last_login_advances():
assert second["last_login"] >= first["last_login"]


def test_record_login_is_fail_open(monkeypatch):
def test_record_login_is_fail_open(monkeypatch, tmp_path):
# an unwritable path must not raise; the run just treats it as a fresh sign-in next time
monkeypatch.setattr(h, "P_STORE_PATH", "/nonexistent-dir-xyz/authenticated_domains.json")
# (a path UNDER a regular file is unwritable on every OS; a bare "/nonexistent-dir" is creatable on Windows)
blocker = tmp_path / "not-a-dir"
blocker.write_text("x", encoding="utf-8")
monkeypatch.setattr(h, "P_STORE_PATH", str(blocker / "authenticated_domains.json"))
h.record_login("x.com") # no exception
assert h.is_authenticated("x.com") is False

Expand Down
7 changes: 4 additions & 3 deletions backend/tests/test_browser_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def test_task_secrets_are_scrubbed_from_tasks_jsonl(tmp_path, monkeypatch):
line = open(p_os.path.join(str(tmp_path), "tasks.jsonl")).read()
assert "hunter2" not in line and "sk-abc" not in line
assert "password [redacted]" in line
# owner-only file perms
mode = p_os.stat(p_os.path.join(str(tmp_path), "tasks.jsonl")).st_mode & 0o777
assert mode == 0o600
# owner-only file perms (POSIX mode bits; Windows has no equivalent to assert on)
if p_os.name == "posix":
mode = p_os.stat(p_os.path.join(str(tmp_path), "tasks.jsonl")).st_mode & 0o777
assert mode == 0o600
4 changes: 2 additions & 2 deletions backend/tests/test_browser_provider_agnostic.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
def p_model_ids_in_code(path: pathlib.Path):
"""Every concrete model id appearing in a STRING literal, with its line number."""
hits = []
with path.open() as f:
with path.open(encoding="utf-8") as f:
for tok in tokenize.generate_tokens(f.readline):
if tok.type != tokenize.STRING:
continue
Expand Down Expand Up @@ -74,7 +74,7 @@ def test_every_aux_call_asks_for_a_tier():
call_re = re.compile(r"preferred_tier\s*=\s*\"([^\"]+)\"")
bad = []
for path in sorted(P_BROWSER_DIR.glob("*.py")):
for i, line in enumerate(path.read_text().splitlines(), 1):
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
for m in call_re.finditer(line):
if m.group(1) not in ("haiku", "sonnet", "opus"):
bad.append(f"{path.name}:{i} -> {m.group(1)}")
Expand Down
5 changes: 4 additions & 1 deletion backend/tests/test_bundled_extracted_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ def test_warm_cache_is_complete_requires_the_sentinel(tmp_path):
assert vt.warm_cache_is_complete(str(nm)) is False
bindir = nm / ".bin"
bindir.mkdir()
(bindir / "vite").symlink_to("../vite/bin/vite.js")
# A plain file, not a symlink: on Windows a relative symlink target with forward slashes does not
# resolve (exists() is False), and npm writes .bin shims as files there anyway. The check under test
# is os.path.exists, which the two are equivalent for.
(bindir / "vite").write_text("#!/usr/bin/env node\n")
assert vt.warm_cache_is_complete(str(nm)) is False
(tmp_path / ".install-complete").write_text("digest")
assert vt.warm_cache_is_complete(str(nm)) is True
Expand Down
1 change: 1 addition & 0 deletions backend/tests/test_credential_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async def test_restoring_a_refresh_token_round_trips(p_router):


@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits; Windows ACLs do not surface through st_mode")
async def test_written_db_is_owner_only(p_router):
await store.apply_to_connection("conn-1", changes={}, drop=["refreshToken"])
mode = stat.S_IMODE(os.stat(store.db_path()).st_mode)
Expand Down
8 changes: 5 additions & 3 deletions backend/tests/test_disk_resilience.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ def tracking_replace(src, dst):


def test_atomic_write_fsyncs_directory_after_rename(tmp_path, monkeypatch):
# POSIX only: the rename is only durable once the parent dir is fsync'd.
if not hasattr(os, "O_RDONLY"):
pytest.skip("directory fsync not applicable on this platform")
# POSIX only: the rename is only durable once the parent dir is fsync'd. Windows exposes
# os.O_RDONLY but cannot open a directory with it, and json_store skips the dir fsync there
# by design, so the old hasattr guard never fired where it was needed.
if os.name == "nt":
pytest.skip("directory fsync is not possible on Windows; json_store skips it there")
from backend.config import json_store

fsync_targets = []
Expand Down
3 changes: 3 additions & 0 deletions backend/tests/test_reap_ghost_runtimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import os
from unittest.mock import patch

import pytest

from backend.apps.outputs import reap_ghost_runtimes as mod


Expand Down Expand Up @@ -163,6 +165,7 @@ class P_Out:
assert rg.find_ghost_runtime_pids() == [], "a live backend's own app runtime must never be killed"


@pytest.mark.skipif(os.name == "nt", reason="SIGSTOP/SIGCONT freezing is POSIX-only; the reaper never pauses runtimes on Windows")
def test_a_frozen_ghost_is_thawed_before_being_signalled(monkeypatch):
"""Idle app runtimes are parked with SIGSTOP, and a STOPPED process never handles SIGTERM: it
queues it and lives forever. Found live as a frozen `bash run.sh` that had survived every reap
Expand Down
7 changes: 7 additions & 0 deletions backend/tests/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,16 @@ def patch_settings(tmp_path):
}))
import backend.apps.settings.store as settings_mod
old = settings_mod.SETTINGS_FILE
old_dir = settings_mod.DATA_DIR
settings_mod.SETTINGS_FILE = str(sf)
# The atomic writer stages its temp file in DATA_DIR and os.replace()s it onto SETTINGS_FILE, so
# the two must live together as they do in production. Left at the real data dir, the temp file
# lands in the repo's backend/data and, on a runner where that is another drive than tmp_path,
# the replace fails with WinError 17 (and the writer's caller quietly falls back).
settings_mod.DATA_DIR = str(tmp_path)
yield
settings_mod.SETTINGS_FILE = old
settings_mod.DATA_DIR = old_dir


@pytest.fixture(autouse=True)
Expand Down
7 changes: 7 additions & 0 deletions backend/tests/test_shutdown_fuse_disarms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
skip: a green-looking run that never executed.
"""

import os

import pytest

from backend.apps.service.shutdown_fuse import arm_shutdown_fuse, disarm_shutdown_fuse, fuse_armed

# The fuse walks the process tree with pgrep and is deliberately a no-op on Windows (arm returns without a timer), so there is nothing to stand down there.
pytestmark = pytest.mark.skipif(os.name == "nt", reason="the shutdown fuse is POSIX-only; arm is a no-op on Windows")


def test_arming_then_disarming_leaves_no_live_timer() -> None:
arm_shutdown_fuse()
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_skill_replay_vs_send_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_the_agent_uses_the_predicate_rather_than_reinventing_the_condition():
"""The whole point of extracting it. If someone re-inlines the check, these tests would keep
passing while the live path regressed, which is exactly how this bug survived the first time."""
src = (bs.__file__).replace("browser_skills.py", "browser_agent.py")
with open(src) as f:
with open(src, encoding="utf-8") as f:
text = f.read()
assert "browser_skills.replay_owns_nav(" in text
assert "p_skip_prestage_for_skill = bool(" not in text, "the condition was re-inlined"
6 changes: 6 additions & 0 deletions backend/tests/test_system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
App Builder skill. The context builders are mocked to None so the test is deterministic and
doesn't depend on dashboard/tool disk state."""

import re

import pytest
from unittest.mock import patch

Expand All @@ -26,6 +28,10 @@ def test_base_composition_includes_default_and_time_pin():
out = p_compose(session)
assert "You are a helpful agent." in out
assert "<current_time>" in out # the wall-clock pin is always appended
# The pin carries a real date and time on every OS (the block is built with portable strftime
# codes; a formatting error would be swallowed and the whole pin would vanish).
assert re.search(r"Today is [A-Z][a-z]+, [A-Z][a-z]+ [1-9]\d?, \d{4}\.", out), out
assert re.search(r"Local time: [1-9]\d?:\d{2} [AP]M ", out), out


def test_view_builder_appends_live_skill_block():
Expand Down