feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
"""Asset sourcing + install-flow tests for webinstaller.
|
|
|
|
|
|
|
|
|
|
Phase-2 slice 1a moved every HTML/CSS/script/unit payload out of inline
|
|
|
|
|
Python string constants and into real files under furtka/assets/. Slice 1b
|
|
|
|
|
then flipped Caddy to serve from /opt/furtka/current/assets/www/, retired
|
|
|
|
|
/srv/furtka/www/, and switched systemd units from hand-written files in
|
|
|
|
|
/etc/systemd/system/ to `systemctl link` against the shipped asset tree.
|
|
|
|
|
|
|
|
|
|
These tests lock the new contract:
|
|
|
|
|
- the Caddyfile and the status.json placeholder still land via
|
|
|
|
|
_write_file_cmd and match the on-disk asset bit-for-bit,
|
|
|
|
|
- the resource-manager bootstrap extracts to /opt/furtka/versions/<ver>/,
|
|
|
|
|
creates /opt/furtka/current, and systemctl-links every unit,
|
|
|
|
|
- furtka.json is written with the installer's hostname,
|
|
|
|
|
- no write ever targets /srv/furtka/www/ (that path is retired).
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "webinstaller"))
|
|
|
|
|
import app # noqa: E402
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
ASSETS = REPO_ROOT / "furtka" / "assets"
|
|
|
|
|
|
|
|
|
|
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
# (install target path, asset path under furtka/assets/) — only the files we
|
|
|
|
|
# still copy bit-for-bit at install time. Scripts + unit files are no longer
|
|
|
|
|
# copied; they're reached via /opt/furtka/current and `systemctl link`.
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
ASSET_TARGETS = [
|
|
|
|
|
("/etc/caddy/Caddyfile", "Caddyfile"),
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
("/var/lib/furtka/status.json", "www/status.json"),
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_written_content(cmd, target):
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
"""Pull the base64 payload back out of a _write_file_cmd() shell string."""
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
match = re.search(r"printf %s (\S+) \| base64 -d > " + re.escape(target), cmd)
|
|
|
|
|
assert match, f"cmd didn't look like a write of {target}: {cmd[:100]}"
|
|
|
|
|
return base64.b64decode(match.group(1)).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
def install_cmds(tmp_path, monkeypatch):
|
|
|
|
|
# Make _resource_manager_commands return a non-empty list so the full
|
|
|
|
|
# command tree is exercised. A fake payload file is enough since the
|
|
|
|
|
# test only inspects the generated shell strings.
|
|
|
|
|
fake = tmp_path / "payload.tar.gz"
|
|
|
|
|
fake.write_bytes(b"not a real tarball")
|
|
|
|
|
monkeypatch.setattr(app, "RESOURCE_MANAGER_PAYLOAD", fake)
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
return app._post_install_commands("testhost")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("target,asset_relpath", ASSET_TARGETS)
|
|
|
|
|
def test_post_install_writes_asset_from_disk(install_cmds, target, asset_relpath):
|
|
|
|
|
expected = (ASSETS / asset_relpath).read_text(encoding="utf-8")
|
|
|
|
|
matching = [c for c in install_cmds if f" > {target}" in c]
|
|
|
|
|
assert matching, f"no command writes {target}"
|
|
|
|
|
assert _extract_written_content(matching[0], target) == expected
|
|
|
|
|
|
|
|
|
|
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
def test_no_command_writes_to_retired_srv_path(install_cmds):
|
|
|
|
|
for c in install_cmds:
|
|
|
|
|
assert "/srv/furtka/www" not in c, f"retired /srv/furtka/www write: {c[:120]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resource_manager_extracts_to_versioned_slot(install_cmds):
|
|
|
|
|
extract_cmd = next((c for c in install_cmds if "tar -xzf" in c), None)
|
|
|
|
|
assert extract_cmd is not None, "no tar extract command"
|
|
|
|
|
assert "/opt/furtka/versions" in extract_cmd
|
|
|
|
|
assert "staging-" in extract_cmd # mktemp -d pattern
|
|
|
|
|
assert 'cat "$staging/VERSION"' in extract_cmd
|
2026-04-16 14:10:07 +02:00
|
|
|
# An empty VERSION file must abort the install instead of silently
|
|
|
|
|
# moving the staging dir into versions/ as a subdir.
|
|
|
|
|
assert '[ -n "$ver" ]' in extract_cmd
|
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/
Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
|
|
|
assert 'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current' in extract_cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resource_manager_systemctl_links_every_unit(install_cmds):
|
|
|
|
|
link_cmd = next((c for c in install_cmds if c.startswith("systemctl link ")), None)
|
|
|
|
|
assert link_cmd is not None, "no systemctl link command"
|
|
|
|
|
for unit in app._FURTKA_UNITS:
|
|
|
|
|
assert f"/opt/furtka/current/assets/systemd/{unit}" in link_cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resource_manager_enables_all_units(install_cmds):
|
|
|
|
|
enable_cmd = next((c for c in install_cmds if c.startswith("systemctl enable ")), None)
|
|
|
|
|
assert enable_cmd is not None
|
|
|
|
|
for unit in app._FURTKA_UNITS:
|
|
|
|
|
assert unit in enable_cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_furtka_json_written_with_hostname(install_cmds):
|
|
|
|
|
furtka_json = next(
|
|
|
|
|
(c for c in install_cmds if "/var/lib/furtka/furtka.json" in c and "mkdir" in c),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
assert furtka_json is not None
|
|
|
|
|
# The base64-encoded JSON body should contain the hostname we passed in.
|
|
|
|
|
content = _extract_written_content(furtka_json, "/var/lib/furtka/furtka.json")
|
|
|
|
|
assert '"hostname": "testhost"' in content
|
|
|
|
|
# install_date + version placeholders — the sed below substitutes them at
|
|
|
|
|
# install time against /opt/furtka/current/VERSION + `date -Iseconds`.
|
|
|
|
|
assert "__INSTALL_DATE__" in content
|
|
|
|
|
assert "__VERSION__" in content
|
|
|
|
|
assert "date -Iseconds" in furtka_json
|
|
|
|
|
assert "/opt/furtka/current/VERSION" in furtka_json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_wrapper_script_points_at_current_symlink():
|
|
|
|
|
assert "PYTHONPATH=/opt/furtka/current" in app._FURTKA_WRAPPER_SH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_caddyfile_asset_serves_from_current():
|
|
|
|
|
caddy = (ASSETS / "Caddyfile").read_text()
|
|
|
|
|
assert "root * /opt/furtka/current/assets/www" in caddy
|
|
|
|
|
assert "/srv/furtka/www" not in caddy
|
|
|
|
|
# Runtime JSON paths served from /var/lib/furtka/ so updates don't clobber.
|
|
|
|
|
assert "root * /var/lib/furtka" in caddy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_systemd_units_reference_current_paths():
|
|
|
|
|
for unit in ("furtka-status.service", "furtka-welcome.service"):
|
|
|
|
|
body = (ASSETS / "systemd" / unit).read_text()
|
|
|
|
|
assert "/opt/furtka/current/assets/bin/" in body, (
|
|
|
|
|
f"{unit} still references a /usr/local/bin path"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
refactor(webinstaller): extract inline payload constants to furtka/assets/
Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd-
unit payload that used to live as a triple-quoted string constant inside
webinstaller/app.py now lives as a real file under furtka/assets/:
furtka/assets/Caddyfile
furtka/assets/VERSION (new — matches pyproject.toml)
furtka/assets/www/{index.html, settings/index.html, style.css, status.json}
furtka/assets/bin/{furtka-status, furtka-welcome}
furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service
furtka/assets/systemd/furtka-status.timer
The installer now pulls each file from disk via _read_asset(). Byte-for-
byte identical output at install time — a fresh-ISO install should land
the same files in the same places with the same contents, verified by
tests/test_webinstaller_assets.py which reconstructs each base64 blob
and asserts equality against the on-disk asset.
iso/build.sh also copies furtka/assets/ next to the webinstaller source
at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds
them with a "next to me" lookup. In dev the same function walks two
levels up to the repo copy, so pytest works without any env vars.
furtka-status.sh drops the /etc/furtka/version TODO — it now reads
/opt/furtka/VERSION directly, which Slice 1b will upgrade to
/opt/furtka/current/VERSION once the symlink layout lands.
_FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline;
it's tiny and not asset-shaped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
|
|
|
def test_read_asset_raises_for_missing_file():
|
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
|
|
|
app._read_asset("does/not/exist.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_assets_dir_resolves_to_repo_tree():
|
|
|
|
|
assert app._ASSETS_DIR == ASSETS
|