Skip to content
Merged
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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"

- name: Install dependencies
run: pip install -e ".[test]"

- name: Run tests
run: pytest --cov=scriptrunner --cov-report=term-missing
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ dependencies = [

[project.scripts]
scriptrunner = "scriptrunner.cli:app"

[project.optional-dependencies]
test = ["pytest>=8.0", "httpx>=0.27", "pytest-cov>=5.0"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v"
14 changes: 7 additions & 7 deletions scriptrunner/server/mission_engine.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Auto-checks and completes missions based on current game state."""

from datetime import datetime
from datetime import datetime, timezone
from typing import List

from sqlmodel import Session, select
Expand All @@ -15,18 +15,18 @@ def _complete(mission: Mission, state: GameState, session: Session, completed: L
if mission.completed:
return
mission.completed = True
mission.completed_at = datetime.utcnow()
mission.completed_at = datetime.now(timezone.utc).replace(tzinfo=None)
state.cycles += mission.reward_cycles * state.cycle_multiplier
state.synth += mission.reward_synth
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(mission)
completed.append(mission.slug)


def _promote_tier(state: GameState, target: int) -> None:
if state.tier < target:
state.tier = target
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)


def check_missions(state: GameState, session: Session) -> List[str]:
Expand Down Expand Up @@ -55,9 +55,9 @@ def check_missions(state: GameState, session: Session) -> List[str]:
if "patience" in missions:
m = missions["patience"]
if state.patience_first_mine_at is None and state.mines_total >= 1:
state.patience_first_mine_at = datetime.utcnow()
state.patience_first_mine_at = datetime.now(timezone.utc).replace(tzinfo=None)
state.patience_first_entropy = state.entropy
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
elif (
state.patience_first_mine_at is not None
and state.entropy < 2.0
Expand All @@ -78,7 +78,7 @@ def check_missions(state: GameState, session: Session) -> List[str]:
m = missions["the_compressor"]
if state.entropy > 70:
state.compressor_saw_high = True
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
if state.compressor_saw_high and state.entropy < 30:
_complete(m, state, session, completed)

Expand Down
10 changes: 5 additions & 5 deletions scriptrunner/server/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from sqlmodel import Field, SQLModel
Expand Down Expand Up @@ -42,8 +42,8 @@ class GameState(SQLModel, table=True):
dark_ops_unlocked: bool = Field(default=False)
# Dark ops: HMAC key shards collected (bitmask 0b11111 = all 5)
hmac_shards: int = Field(default=0)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None))


class CallLog(SQLModel, table=True):
Expand All @@ -52,14 +52,14 @@ class CallLog(SQLModel, table=True):
method: str = Field(default="POST")
status_code: int
result_json: str
timestamp: datetime = Field(default_factory=datetime.utcnow)
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None))


class Automation(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True)
interval_sec: int = Field(default=10)
registered_at: datetime = Field(default_factory=datetime.utcnow)
registered_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None))
active: bool = Field(default=True)


Expand Down
6 changes: 3 additions & 3 deletions scriptrunner/server/routes/automate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""POST /automate — register a named automation for passive +0.5 cycles/sec."""

import json
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException
Expand Down Expand Up @@ -39,7 +39,7 @@ def post_automate(body: AutomateRequest, session: Session = Depends(get_session)
if existing:
existing.active = True
existing.interval_sec = body.interval_sec
existing.registered_at = datetime.utcnow()
existing.registered_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(existing)
msg = f"automation '{body.name}' reactivated"
else:
Expand All @@ -56,7 +56,7 @@ def post_automate(body: AutomateRequest, session: Session = Depends(get_session)
session.add(state)
session.add(CallLog(
endpoint="/automate", method="POST", status_code=200,
result_json=json.dumps(result), timestamp=datetime.utcnow()
result_json=json.dumps(result), timestamp=datetime.now(timezone.utc).replace(tzinfo=None)
))
session.commit()
return result
Expand Down
6 changes: 3 additions & 3 deletions scriptrunner/server/routes/compress.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""POST /compress — spend 100 cycles to reduce entropy by 20."""

import json
from datetime import datetime
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
Expand Down Expand Up @@ -34,7 +34,7 @@ def post_compress(session: Session = Depends(get_session)):

state.cycles -= COMPRESS_COST
state.entropy = max(0.0, state.entropy - COMPRESS_REDUCTION)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

newly_completed = check_missions(state, session)

Expand All @@ -49,7 +49,7 @@ def post_compress(session: Session = Depends(get_session)):
update_blob(state, "/compress")
session.add(CallLog(
endpoint="/compress", method="POST", status_code=200,
result_json=json.dumps(result), timestamp=datetime.utcnow()
result_json=json.dumps(result), timestamp=datetime.now(timezone.utc).replace(tzinfo=None)
))
session.add(state)
session.commit()
Expand Down
24 changes: 12 additions & 12 deletions scriptrunner/server/routes/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import random
import time
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
Expand All @@ -27,7 +27,7 @@ def _get_state(session: Session) -> GameState:

def _track_scheduler(state: GameState) -> None:
"""Track timing for The Scheduler mission: mine every 2s ±200ms for 60s."""
now = datetime.utcnow()
now = datetime.now(timezone.utc).replace(tzinfo=None)
if not state.scheduler_active:
state.scheduler_active = True
state.scheduler_mines = 1
Expand Down Expand Up @@ -61,7 +61,7 @@ def _log_call(endpoint: str, method: str, status_code: int, result: dict, sessio
def get_status(session: Session = Depends(get_session)):
state = _get_state(session)
state.status_calls += 1
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

newly_completed = check_missions(state, session)

Expand Down Expand Up @@ -121,7 +121,7 @@ def post_mine(session: Session = Depends(get_session)):

# Track overclock mines for Overclock Runner mission
if state.overclock_active and state.overclock_ends_at:
if datetime.utcnow() < state.overclock_ends_at:
if datetime.now(timezone.utc).replace(tzinfo=None) < state.overclock_ends_at:
state.overclock_mines += 1
gain *= 2 # doubled during overclock
else:
Expand All @@ -133,7 +133,7 @@ def post_mine(session: Session = Depends(get_session)):
if loss_event:
state.cycles = max(0, state.cycles - 50)
state.entropy = min(100, state.entropy + 0.5)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
newly_completed = check_missions(state, session)
result = {
"cycles": round(state.cycles, 2),
Expand All @@ -151,7 +151,7 @@ def post_mine(session: Session = Depends(get_session)):

state.cycles += gain
state.entropy = min(100, state.entropy + 0.5)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

newly_completed = check_missions(state, session)

Expand Down Expand Up @@ -180,15 +180,15 @@ def post_overclock(session: Session = Depends(get_session)):
if state.tier < 3:
raise HTTPException(status_code=403, detail="endpoint locked — reach Tier 3 first")

if state.overclock_active and state.overclock_ends_at and datetime.utcnow() < state.overclock_ends_at:
remaining = (state.overclock_ends_at - datetime.utcnow()).seconds
if state.overclock_active and state.overclock_ends_at and datetime.now(timezone.utc).replace(tzinfo=None) < state.overclock_ends_at:
remaining = (state.overclock_ends_at - datetime.now(timezone.utc).replace(tzinfo=None)).seconds
raise HTTPException(status_code=400, detail=f"overclock already active — {remaining}s remaining")

state.overclock_active = True
state.overclock_mines = 0
state.overclock_ends_at = datetime.utcnow() + timedelta(seconds=30)
state.overclock_ends_at = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(seconds=30)
state.entropy = min(100, state.entropy + 30)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

result = {
"message": "overclock active — mine yield doubled for 30 seconds",
Expand All @@ -214,7 +214,7 @@ def post_exploit(session: Session = Depends(get_session)):
if success:
gain = 500.0 * state.cycle_multiplier
state.cycles += gain
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
result = {
"outcome": "success",
"gained": round(gain, 2),
Expand All @@ -223,7 +223,7 @@ def post_exploit(session: Session = Depends(get_session)):
}
else:
state.entropy = min(100, state.entropy + 40)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
result = {
"outcome": "failure",
"entropy": round(state.entropy, 2),
Expand Down
24 changes: 12 additions & 12 deletions scriptrunner/server/routes/dark_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import random
import time
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException
Expand Down Expand Up @@ -89,7 +89,7 @@ def get_hint(shard_id: int, session: Session = Depends(get_session)):
bit = 1 << (shard_id - 1)
if not (state.hmac_shards & bit):
state.hmac_shards |= bit
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(state)
session.commit()

Expand Down Expand Up @@ -120,7 +120,7 @@ def post_spoof(body: SpoofRequest, session: Session = Depends(get_session)):
if not hmac.compare_digest(expected, body.signature.lower()):
# Penalty: entropy spike
state.entropy = min(100, state.entropy + 20)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(state)
session.commit()
raise HTTPException(
Expand All @@ -130,7 +130,7 @@ def post_spoof(body: SpoofRequest, session: Session = Depends(get_session)):

gain = 200.0 * state.cycle_multiplier
state.cycles += gain
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

result = {
"outcome": "spoof accepted",
Expand All @@ -141,7 +141,7 @@ def post_spoof(body: SpoofRequest, session: Session = Depends(get_session)):
update_blob(state, "/dark-ops/spoof")
session.add(CallLog(
endpoint="/dark-ops/spoof", method="POST", status_code=200,
result_json=json.dumps(result), timestamp=datetime.utcnow()
result_json=json.dumps(result), timestamp=datetime.now(timezone.utc).replace(tzinfo=None)
))
session.add(state)
session.commit()
Expand All @@ -159,7 +159,7 @@ def post_inject(session: Session = Depends(get_session)):
# Critical failure
state.entropy = min(100, state.entropy + 50)
state.cycles = max(0, state.cycles - 200)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
result = {
"outcome": "critical_failure",
"entropy": round(state.entropy, 2),
Expand All @@ -170,7 +170,7 @@ def post_inject(session: Session = Depends(get_session)):
gain = 100.0 * state.cycle_multiplier
state.cycles += gain
state.entropy = min(100, state.entropy + 15)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
result = {
"outcome": "partial_success",
"gained": round(gain, 2),
Expand All @@ -181,7 +181,7 @@ def post_inject(session: Session = Depends(get_session)):
# Full success
gain = 400.0 * state.cycle_multiplier
state.cycles += gain
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
result = {
"outcome": "success",
"gained": round(gain, 2),
Expand All @@ -192,7 +192,7 @@ def post_inject(session: Session = Depends(get_session)):
update_blob(state, "/dark-ops/inject")
session.add(CallLog(
endpoint="/dark-ops/inject", method="POST", status_code=200,
result_json=json.dumps(result), timestamp=datetime.utcnow()
result_json=json.dumps(result), timestamp=datetime.now(timezone.utc).replace(tzinfo=None)
))
session.add(state)
session.commit()
Expand Down Expand Up @@ -230,7 +230,7 @@ def post_finalize(body: FinalizeRequest, session: Session = Depends(get_session)

if not hmac.compare_digest(expected, body.signature.lower()):
state.entropy = min(100, state.entropy + 30)
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(state)
session.commit()
raise HTTPException(status_code=401, detail="invalid signature — entropy +30")
Expand All @@ -239,7 +239,7 @@ def post_finalize(body: FinalizeRequest, session: Session = Depends(get_session)
gain = 99999.0 * state.cycle_multiplier
state.cycles += gain
state.synth += 10
state.updated_at = datetime.utcnow()
state.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)

result = {
"outcome": "CLASSIFIED",
Expand All @@ -253,7 +253,7 @@ def post_finalize(body: FinalizeRequest, session: Session = Depends(get_session)
update_blob(state, "/dark-ops/finalize")
session.add(CallLog(
endpoint="/dark-ops/finalize", method="POST", status_code=200,
result_json=json.dumps(result), timestamp=datetime.utcnow()
result_json=json.dumps(result), timestamp=datetime.now(timezone.utc).replace(tzinfo=None)
))
session.add(state)
session.commit()
Expand Down
Loading
Loading