Compare commits
8 changed files with 23 additions and 370 deletions
21
.github/workflows/canon-marker.yml
vendored
21
.github/workflows/canon-marker.yml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
name: Canon marker
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: canon-marker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
# The forge's runner carries the label `docker` (image node:22-bookworm);
|
||||
# `ubuntu-latest` matches no runner there and the job is cancelled.
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: apt-get update -qq && apt-get install -y -qq python3-yaml
|
||||
- run: python3 -m unittest scripts/tests/test_check_canon_marker.py
|
||||
- run: python3 scripts/check_canon_marker.py "origin/${{ github.base_ref }}"
|
||||
3
.github/workflows/deploy-quartz.yml
vendored
3
.github/workflows/deploy-quartz.yml
vendored
|
|
@ -17,9 +17,6 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
deploy:
|
||||
# The shared workflow publishes through GitHub Pages actions, which fail on
|
||||
# the forge's runner (capacity 1). Run it on the GitHub mirror only.
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: GameCult/GameCult-Quartz/.github/workflows/quartz-pages.yml@main
|
||||
with:
|
||||
content-dir: Aetheria
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -32,4 +32,3 @@ Thumbs.db
|
|||
/quartz-site/node_modules/
|
||||
/quartz-site/public/
|
||||
/quartz-site/*.log
|
||||
__pycache__/
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ Boss factions are marked ★, demo factions **bold**. "Ships" counts designs and
|
|||
| **Finch Cybernetics** | Passive sensors, character upgrades | Cybernetic transhumanism | Socialist technocratic; sleek, organic | 0 products | — |
|
||||
| **Aeronautics Unlimited** | All-rounder hulls, mining, dumbfire, explosives | Resource acquisition | Britain; minimalist geometric curves | 8 products | — |
|
||||
| **Alakrita** | Light fragile thrusters, fast hulls | Need for speed | Roman; maximalist, aerodynamic creases | 3 products | — |
|
||||
| **Adrasteia** | Stealth, negative entropy | The best defense is not to need one | India; sleek, angular, faceted | 1 product | [[Worldbuilding/Post-Elysium/Technology/Running Cold\|Running Cold]] |
|
||||
| **Adrasteia** | Stealth, negative entropy | The best defense is not to need one | India; sleek, angular, faceted | 1 product | [[Worldbuilding/Post-Elysium/Technology/Running Cold|Running Cold]] |
|
||||
| Ewan Hart Inc | Farming equipment | Money *does* grow on trees | USA Midwest; dieselpunk, tractors | 0 products | — |
|
||||
| Rossum & Douglas | AI, automation, compute, guided launchers | Mom's Friendly Robot Company | Germany; trapezoidal, paneled | 2 products | — |
|
||||
| **Death Monkey Explosives** | Thermal management, thermal weaponry | Some people just want to watch the world burn. We're here to help! | Australia; menacing with spikes | 5 products | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives\|note]], [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives Products\|products]] |
|
||||
| **Death Monkey Explosives** | Thermal management, thermal weaponry | Some people just want to watch the world burn. We're here to help! | Australia; menacing with spikes | 5 products | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives|note]], [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives Products|products]] |
|
||||
| **Miss Terri's Sugariffic Snack Company** | Corrosive weapons, stat consumables | Candy for all! Unexpected experimental substances for some! | Melting pot; kawaii future bass | 1 product | — |
|
||||
| Aya Collective | Self-repair, life support | Afro-futurism | West Africa, Akan language group, Afrihili | not in catalog | — |
|
||||
| Corriedales | Varied weapons | Family-friendly warfare | Lebanese; kids' toys, bubbly | not in catalog | — |
|
||||
| Odla Framgång | Knockoffs and pseudo-spiritual woo | Multi-level affiliate marketing | Sweden; retrofuturism, populuxe | not in catalog | — |
|
||||
| Pirates (name rolled per run) | Cobbled-together stolen technology | *P I R A T E S* | Emojis; junkyard, improvised | not in catalog | — |
|
||||
| Megiddo | Defensive technology | *Space Zionists* | Hebrew; choral | not in catalog | [[Worldbuilding/Post-Elysium/Factions/Megiddo\|note]] |
|
||||
| Megiddo | Defensive technology | *Space Zionists* | Hebrew; choral | not in catalog | [[Worldbuilding/Post-Elysium/Factions/Megiddo|note]] |
|
||||
|
||||
Each corporation also carries a musical genre in the source table (death metal for Zhestokost, drum and bass for Alakrita, noir jazz for Adrasteia, bluegrass for Ewan Hart, muzak for Rossum & Douglas, and so on). Useful as a tone key when writing copy for one.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,25 +12,25 @@ Ranked from most to least legible, across the factions in [[Game Design/Faction
|
|||
|
||||
| Rank | Faction | What we know | What is thin |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [[Worldbuilding/Post-Elysium/Factions/Megiddo\|Megiddo]] | Founding doctrine, daily life, exit rules, Vaults, defense and trade, internal custodians | Little; the benchmark |
|
||||
| 2 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Zhestokost\|Zhestokost]] | Supply-tail doctrine, heavy-weapons rationale, column behavior, voice | Home life, what a Zhestokost person laughs at |
|
||||
| 3 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media\|Lucent Media]] | Spectacle economy, Corriedales pipeline, duel bait (accepted 2026-10-03) | Internal ranks of headliners |
|
||||
| 4 | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives\|Death Monkey Explosives]] | Long note, products, cell structure, punk ethic | Fine as is |
|
||||
| 5 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective\|Aya Collective]] | Federation, care politics, supplier rules | Rituals, humor |
|
||||
| 6 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Sol Dominion\|Sol Dominion]] | Classification logic, jurisdiction | Everyday life inside the record |
|
||||
| 7 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cetacean Navigators\|Cetacean Navigators]] | Corridor obligations, courts, sanctuary | Fine for now |
|
||||
| 8 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Framgång\|Framgång]] | Retreat economy, Odla affiliates, the persistent salesman | Fine for now |
|
||||
| 9 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Finch Cybernetics\|Finch Cybernetics]] | Clinic dependence, passive sensing | Track-selling is still a proposal |
|
||||
| 10 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cryonix\|Cryonix]] | Thermal enclaves, scout doctrine | Culture outside the license office |
|
||||
| 11 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aeronautics Unlimited\|Aeronautics Unlimited]] | Claimshares, crews versus company | Rituals, humor |
|
||||
| 12 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Rossum & Douglas\|Rossum & Douglas]] | A real note; certification-and-asterisk logic | Its people |
|
||||
| 13 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pirate Coalition\|Pirate Coalition]] | Possession maintenance, crews, credentials | Varies by crew by design |
|
||||
| 14 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Alakrita\|Alakrita]] | Luxury hulls, licensed minds, Ship Mutiny link | Why it breaks off; what its people are like |
|
||||
| 15 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Corriedales\|Corriedales]] | Lucent's toy brand, a sharp idea | A brand, not a people; left to Lucent |
|
||||
| 16 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express\|Lightsail Express]] | Three paragraphs, a motto, trucking flavor | Why its haulers will die for cargo |
|
||||
| 17 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Ewan Hart\|Ewan Hart]] | Three paragraphs, seed lock-in | Everything human; its combat quirk was cut |
|
||||
| 18 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/NiteLife Energy\|NiteLife Energy]] | Three paragraphs, the cartridge | Why its guards never leave; its nightlife |
|
||||
| 19 | Adrasteia | Products, [[Worldbuilding/Post-Elysium/Technology/Running Cold\|Running Cold]] | No faction note at all |
|
||||
| 1 | [[Worldbuilding/Post-Elysium/Factions/Megiddo|Megiddo]] | Founding doctrine, daily life, exit rules, Vaults, defense and trade, internal custodians | Little; the benchmark |
|
||||
| 2 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Zhestokost|Zhestokost]] | Supply-tail doctrine, heavy-weapons rationale, column behavior, voice | Home life, what a Zhestokost person laughs at |
|
||||
| 3 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media|Lucent Media]] | Spectacle economy, Corriedales pipeline, duel bait (accepted 2026-10-03) | Internal ranks of headliners |
|
||||
| 4 | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives|Death Monkey Explosives]] | Long note, products, cell structure, punk ethic | Fine as is |
|
||||
| 5 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective|Aya Collective]] | Federation, care politics, supplier rules | Rituals, humor |
|
||||
| 6 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Sol Dominion|Sol Dominion]] | Classification logic, jurisdiction | Everyday life inside the record |
|
||||
| 7 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cetacean Navigators|Cetacean Navigators]] | Corridor obligations, courts, sanctuary | Fine for now |
|
||||
| 8 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Framgång|Framgång]] | Retreat economy, Odla affiliates, the persistent salesman | Fine for now |
|
||||
| 9 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Finch Cybernetics|Finch Cybernetics]] | Clinic dependence, passive sensing | Track-selling is still a proposal |
|
||||
| 10 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cryonix|Cryonix]] | Thermal enclaves, scout doctrine | Culture outside the license office |
|
||||
| 11 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aeronautics Unlimited|Aeronautics Unlimited]] | Claimshares, crews versus company | Rituals, humor |
|
||||
| 12 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Rossum & Douglas|Rossum & Douglas]] | A real note; certification-and-asterisk logic | Its people |
|
||||
| 13 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pirate Coalition|Pirate Coalition]] | Possession maintenance, crews, credentials | Varies by crew by design |
|
||||
| 14 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Alakrita|Alakrita]] | Luxury hulls, licensed minds, Ship Mutiny link | Why it breaks off; what its people are like |
|
||||
| 15 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Corriedales|Corriedales]] | Lucent's toy brand, a sharp idea | A brand, not a people; left to Lucent |
|
||||
| 16 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express|Lightsail Express]] | Three paragraphs, a motto, trucking flavor | Why its haulers will die for cargo |
|
||||
| 17 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Ewan Hart|Ewan Hart]] | Three paragraphs, seed lock-in | Everything human; its combat quirk was cut |
|
||||
| 18 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/NiteLife Energy|NiteLife Energy]] | Three paragraphs, the cartridge | Why its guards never leave; its nightlife |
|
||||
| 19 | Adrasteia | Products, [[Worldbuilding/Post-Elysium/Technology/Running Cold|Running Cold]] | No faction note at all |
|
||||
| 20 | Miss Terri's Sugariffic Snack Company | Products, flavor, the tutorial role | No faction note; the protagonist faction has no lore |
|
||||
|
||||
## Deepened
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Writers do not need custody of the whole universe. Shared notes own their stated
|
|||
## Status
|
||||
|
||||
- **Live:** the vault is the source of [aetheria.gamecult.org](https://aetheria.gamecult.org), built and deployed by `.github/workflows/deploy-quartz.yml` through the shared engine in `GameCult-Quartz`; `scripts/quartz/quartz.ps1` runs it locally.
|
||||
- **Known limitation:** Quartz mishandles `\|` inside table wikilinks (it leaves a literal backslash on the target), so tables here write `[[Target|alias]]`; Obsidian may split those cells until the engine is fixed in `GameCult-Quartz`.
|
||||
- **Not canon:** `Brainstorming/` and `Inspiration/` branches are excluded from publication. They are working material, and notes in them become setting only when a finished story or an owner note adopts them.
|
||||
|
||||
## Repository Map
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Canon/proposal marker check. Usage: check_canon_marker.py <base-ref>
|
||||
|
||||
Checks .md files under Aetheria/ added, modified or renamed against <base-ref>.
|
||||
Proves the form and presence of ruling ids, not that the ruling exists.
|
||||
"""
|
||||
import re, subprocess, sys
|
||||
|
||||
import yaml
|
||||
|
||||
ID = re.compile(r"^[a-z0-9][a-z0-9-]*:ruling:[a-z0-9][a-z0-9._-]*$")
|
||||
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(["git", *a], check=True, capture_output=True, text=True).stdout
|
||||
|
||||
|
||||
class UniqueKeyLoader(yaml.SafeLoader):
|
||||
"""safe_load that refuses a repeated key: one page, one reading of `kind`."""
|
||||
|
||||
def construct_mapping(self, node, deep=False):
|
||||
seen = set()
|
||||
for key_node, _ in node.value:
|
||||
key = self.construct_object(key_node, deep=True)
|
||||
if key in seen:
|
||||
raise yaml.constructor.ConstructorError(
|
||||
None, None, f"duplicate key {key!r}", key_node.start_mark)
|
||||
seen.add(key)
|
||||
return super().construct_mapping(node, deep)
|
||||
|
||||
|
||||
FRONT = re.compile(r"---\n(?:(.*?)\n)?---[ \t]*(?:\n|\Z)", re.S)
|
||||
|
||||
|
||||
def frontmatter(text):
|
||||
"""Return (kind, rulings or None, error). Frontmatter is real YAML."""
|
||||
if not text.startswith("---\n"):
|
||||
return None, None, None
|
||||
m = FRONT.match(text)
|
||||
if not m:
|
||||
return None, None, "unterminated frontmatter"
|
||||
try:
|
||||
data = yaml.load(m.group(1) or "", Loader=UniqueKeyLoader)
|
||||
except yaml.YAMLError as e:
|
||||
return None, None, f"frontmatter is not valid YAML ({type(e).__name__}: {e})"
|
||||
if data is None:
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
return None, None, "frontmatter must be a mapping"
|
||||
kind, rulings = data.get("kind"), data.get("rulings")
|
||||
if kind is not None and not isinstance(kind, str):
|
||||
return None, None, "kind must be a string"
|
||||
if rulings is not None and not (isinstance(rulings, list) and all(isinstance(r, str) for r in rulings)):
|
||||
return None, None, "rulings must be a list of strings"
|
||||
return kind, rulings, None
|
||||
|
||||
|
||||
def changed(base):
|
||||
"""(status letter, path) for each page added, modified or renamed (the new path)."""
|
||||
out = git("diff", "-z", "--name-status", "--diff-filter=AMR", f"{base}...HEAD", "--", "Aetheria")
|
||||
fields = out.split("\0")
|
||||
rows, i = [], 0
|
||||
while i < len(fields) and fields[i]:
|
||||
st = fields[i]
|
||||
if st[0] == "R":
|
||||
rows.append(("R", fields[i + 2]))
|
||||
i += 3
|
||||
else:
|
||||
rows.append((st[0], fields[i + 1]))
|
||||
i += 2
|
||||
return rows
|
||||
|
||||
|
||||
def main(base):
|
||||
try:
|
||||
rows = changed(base)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"CANON CHECK FAILED: cannot diff against {base!r}: {e.stderr.strip()}")
|
||||
return 1
|
||||
failed = False
|
||||
for st, path in rows:
|
||||
if not path.endswith(".md"):
|
||||
continue
|
||||
try:
|
||||
with open(path, encoding="utf-8-sig") as f:
|
||||
kind, rulings, err = frontmatter(f.read())
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
kind, rulings, err = None, None, f"cannot read page ({type(e).__name__})"
|
||||
problem = None
|
||||
if err:
|
||||
problem = err
|
||||
elif kind is None:
|
||||
if st == "A":
|
||||
problem = "new page needs `kind: canon` or `kind: proposal`"
|
||||
else:
|
||||
print(f"notice: {path} has no kind (legacy); tag it when you next touch it")
|
||||
elif kind == "canon":
|
||||
bad = [r for r in (rulings or []) if not ID.fullmatch(r)]
|
||||
if not rulings or bad:
|
||||
problem = ("kind: canon requires `rulings:` with at least one id of the form "
|
||||
f"<campaign>:ruling:<label>\n (found: {bad or 'none'})")
|
||||
elif kind == "proposal":
|
||||
if rulings:
|
||||
problem = "kind: proposal may not carry `rulings:`"
|
||||
else:
|
||||
problem = f"kind must be canon or proposal (found: {kind})"
|
||||
if problem:
|
||||
failed = True
|
||||
print(f"CANON CHECK FAILED: {path}\n {problem}\n"
|
||||
" A ruling must exist before its text enters a canon page. "
|
||||
"Put the text in a `kind: proposal` page until it does.")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main(sys.argv[1]))
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
"""Tests for scripts/check_canon_marker.py: each builds a throwaway git repo
|
||||
with a main branch, adds pages on a branch, and runs the check against main."""
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SCRIPT = os.path.join(os.path.dirname(__file__), "..", "check_canon_marker.py")
|
||||
RULING = "aetheria-lore:ruling:megiddo-origin"
|
||||
|
||||
|
||||
def git(cwd, *args):
|
||||
subprocess.run(["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
|
||||
cwd=cwd, check=True, capture_output=True)
|
||||
|
||||
|
||||
class Repo:
|
||||
def __init__(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
git(self.dir, "init", "-b", "main")
|
||||
os.makedirs(os.path.join(self.dir, "Aetheria"))
|
||||
self.write("Aetheria/Legacy.md", "old page, no marker\n" + "line of lore\n" * 20)
|
||||
git(self.dir, "add", "-A")
|
||||
git(self.dir, "commit", "-m", "base")
|
||||
git(self.dir, "checkout", "-b", "pr")
|
||||
|
||||
def write(self, path, text):
|
||||
full = os.path.join(self.dir, path)
|
||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||
with open(full, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
def commit(self):
|
||||
git(self.dir, "add", "-A")
|
||||
git(self.dir, "commit", "-m", "pr")
|
||||
|
||||
def check(self):
|
||||
p = subprocess.run(["python3", SCRIPT, "main"], cwd=self.dir,
|
||||
capture_output=True, text=True)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
|
||||
|
||||
def canon(rulings, kind="canon"):
|
||||
return f"---\nkind: {kind}\nrulings:\n{rulings}---\nbody\n"
|
||||
|
||||
|
||||
class CanonMarker(unittest.TestCase):
|
||||
def run_pages(self, pages, after=None):
|
||||
r = Repo()
|
||||
for path, text in pages.items():
|
||||
r.write(path, text)
|
||||
if after:
|
||||
after(r)
|
||||
r.commit()
|
||||
return r.check()
|
||||
|
||||
def test_a_canon_page_with_a_ruling_id_passes(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING}\n")})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_a_canon_page_without_a_ruling_fails(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: canon\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_non_ascii_path_is_checked_not_skipped(self):
|
||||
code, out = self.run_pages({"Aetheria/Framgång.md": "---\nkind: canon\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertIn("Framgång.md", out)
|
||||
|
||||
def test_a_renamed_page_is_checked(self):
|
||||
def rename(r):
|
||||
git(r.dir, "mv", "Aetheria/Legacy.md", "Aetheria/Moved.md")
|
||||
r.write("Aetheria/Moved.md", "---\nkind: canon\n---\n" + "old page, no marker\n" + "line of lore\n" * 20)
|
||||
code, out = self.run_pages({}, after=rename)
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertIn("Moved.md", out)
|
||||
|
||||
def test_quoted_values_parse(self):
|
||||
text = f'---\nkind: "canon"\nrulings:\n - "{RULING}"\n---\nx\n'
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_a_trailing_comment_on_a_ruling_is_not_part_of_the_id(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING} # megiddo\n")})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_a_flow_list_of_rulings_parses(self):
|
||||
text = f"---\nkind: canon\nrulings: [{RULING}, weksa:ruling:other]\n---\nx\n"
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_cross_campaign_ruling_ids_count(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": canon(" - weksa-body:ruling:faction-play-1\n")})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_a_malformed_ruling_id_fails(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": canon(" - megiddo-origin\n")})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_proposal_may_not_carry_rulings(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING}\n", kind="proposal")})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_proposal_without_rulings_passes(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: proposal\n---\nx\n"})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_an_unknown_kind_fails(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: draft\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_new_untagged_page_fails(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "no frontmatter\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_touched_legacy_page_passes_with_a_notice(self):
|
||||
code, out = self.run_pages({"Aetheria/Legacy.md": "old page, edited\n" + "line of lore\n" * 20})
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("notice", out)
|
||||
|
||||
def test_invalid_yaml_fails_instead_of_crashing(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: [canon\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertNotIn("Traceback", out)
|
||||
|
||||
def test_a_non_mapping_frontmatter_fails(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\n- canon\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertNotIn("Traceback", out)
|
||||
|
||||
def test_a_python_object_tag_is_refused_not_executed(self):
|
||||
text = "---\nkind: !!python/object/apply:os.getcwd []\n---\nx\n"
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertNotIn("Traceback", out)
|
||||
|
||||
def test_a_repeated_key_fails(self):
|
||||
text = f"---\nkind: proposal\nkind: canon\nrulings:\n - {RULING}\n---\nx\n"
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertIn("duplicate", out)
|
||||
|
||||
def test_a_bom_does_not_hide_the_marker(self):
|
||||
code, out = self.run_pages({"Aetheria/Legacy.md": "\ufeff---\nkind: canon\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_ruling_id_with_a_trailing_newline_is_malformed(self):
|
||||
text = f'---\nkind: canon\nrulings:\n - "{RULING}\\n"\n---\nx\n'
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_empty_frontmatter_is_a_kindless_page_not_unterminated(self):
|
||||
code, out = self.run_pages({"Aetheria/A.md": "---\n---\nx\n"})
|
||||
self.assertEqual(code, 1, out)
|
||||
self.assertNotIn("unterminated", out)
|
||||
self.assertIn("needs `kind", out)
|
||||
|
||||
def test_a_lookalike_closing_marker_does_not_end_the_frontmatter(self):
|
||||
text = "---\nkind: proposal\n----\nstill frontmatter?\n---\nx\n"
|
||||
code, out = self.run_pages({"Aetheria/A.md": text})
|
||||
self.assertEqual(code, 1, out)
|
||||
|
||||
def test_a_renamed_legacy_page_passes_with_a_notice(self):
|
||||
def rename(r):
|
||||
git(r.dir, "mv", "Aetheria/Legacy.md", "Aetheria/Moved.md")
|
||||
code, out = self.run_pages({}, after=rename)
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("notice", out)
|
||||
|
||||
def test_a_page_main_changed_after_the_branch_point_is_not_charged_to_the_pr(self):
|
||||
# Old.md is a bad canon page that main already holds; the PR merges it in
|
||||
# and does not touch it, then main edits it again. Two-dot diffing
|
||||
# would see the PR's copy as modified and fail the PR for main's page.
|
||||
r = Repo()
|
||||
r.write("Aetheria/Mine.md", "---\nkind: proposal\n---\nx\n")
|
||||
r.commit()
|
||||
git(r.dir, "checkout", "main")
|
||||
r.write("Aetheria/Old.md", "---\nkind: canon\n---\nv1\n")
|
||||
git(r.dir, "add", "-A")
|
||||
git(r.dir, "commit", "-m", "main adds Old")
|
||||
git(r.dir, "checkout", "pr")
|
||||
git(r.dir, "merge", "--no-edit", "main")
|
||||
git(r.dir, "checkout", "main")
|
||||
r.write("Aetheria/Old.md", "---\nkind: canon\n---\nv2\n")
|
||||
git(r.dir, "add", "-A")
|
||||
git(r.dir, "commit", "-m", "main edits Old")
|
||||
git(r.dir, "checkout", "pr")
|
||||
code, out = r.check()
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_an_unknown_base_ref_fails_loudly_without_a_traceback(self):
|
||||
r = Repo()
|
||||
p = subprocess.run(["python3", SCRIPT, "no-such-ref"], cwd=r.dir,
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(p.returncode, 1)
|
||||
self.assertNotIn("Traceback", p.stdout + p.stderr)
|
||||
self.assertIn("no-such-ref", p.stdout + p.stderr)
|
||||
|
||||
def test_pages_outside_aetheria_are_not_checked(self):
|
||||
code, out = self.run_pages({"README2.md": "---\nkind: canon\n---\nx\n"})
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue