main
John Lauer Publish 0.2.16 ce048dd
"""Issue #85 through the CLI: `--page owner/slug --page-dir <folder>` puts every push of the run under
that folder (the run README at <folder>/docs/runs/<id>/, the clips page at <folder>/docs/clips.md, its
media in <folder>/docs/clips/), the URLs it writes carry the folder, a bad folder is refused, and
`report` with no page writes the README locally and pushes nothing. adom-wiki is a fake on PATH that
records every `repo push --files` and checks each file exists in the push's working directory.
No CAD, desktop or dashboard.

  python3 tests/page-dir-workflow.py <adom-aiflow binary>
"""
import json, os, pathlib, subprocess, sys, tempfile
binary = str(pathlib.Path(sys.argv[1]).resolve())

FAKE_WIKI = r'''#!/usr/bin/env python3
import json, os, sys
log = os.environ["FAKE_WIKI_LOG"]
a = sys.argv[1:]
rec = {"args": a, "cwd": os.getcwd()}
if a[:2] == ["repo", "push"]:
    i = a.index("--files"); files = a[i + 1:]
    if "-m" in files: files = files[:files.index("-m")]
    rec["files"] = files; rec["missing"] = [f for f in files if not os.path.isfile(os.path.join(os.getcwd(), f))]
    rec["texts"] = {f: open(os.path.join(os.getcwd(), f)).read() for f in files if f.endswith(".md") and f not in rec["missing"]}
open(log, "a").write(json.dumps(rec) + "\n")
if a[:2] == ["repo", "push"]:
    print(json.dumps({"status": "ok", "commit": "abc1234"})); sys.exit(1 if rec["missing"] else 0)
if a[:2] == ["repo", "show"]:
    sys.exit(1)
sys.exit(0)
'''

with tempfile.TemporaryDirectory(prefix='aiflow-page-dir-') as td:
    p = pathlib.Path(td)
    b = p / 'bin'; b.mkdir()
    (b / 'adom-wiki').write_text(FAKE_WIKI); (b / 'adom-wiki').chmod(0o755)
    log = p / 'wiki.jsonl'
    env = dict(os.environ, AIFLOW_DASHBOARD='off', PATH=f"{b}:{os.environ['PATH']}", FAKE_WIKI_LOG=str(log))
    def call(run, *args):
        return subprocess.run([binary, '--run', str(run), '--ai-thread', 'page-dir-test', *args], env=env, capture_output=True, text=True, timeout=60)
    def pushes():
        return [json.loads(l) for l in log.read_text().splitlines() if '"push"' in l] if log.exists() else []
    def data(run): return json.loads((run / 'run.json').read_text())

    # a bad folder is refused before a run is made
    for bad in ['/rig', 'rig/../x', 'rig//x']:
        r = call(p / 'bad', 'intake', '--prompt-time', '2026-10-21T01:00:00Z', '--engine', 'test', '--page', 'john/enc', '--page-dir', bad)
        assert r.returncode != 0 and '--page-dir' in r.stdout + r.stderr, (bad, r.stdout, r.stderr)
        assert not (p / 'bad' / 'run.json').exists()
    r = call(p / 'nopage', 'intake', '--prompt-time', '2026-10-21T01:00:00Z', '--engine', 'test', '--page-dir', 'rig')
    assert r.returncode != 0 and 'needs a page' in r.stdout + r.stderr, (r.stdout, r.stderr)

    # report with no page: written locally, nothing pushed
    local = p / 'local'
    assert call(local, 'intake', '--prompt-time', '2026-10-21T01:00:00Z', '--engine', 'test').returncode == 0
    r = call(local, 'report', '--push'); out = r.stdout + r.stderr
    assert r.returncode == 0, out
    assert (local / 'report' / 'README.md').is_file() and 'written locally' in out and 'nothing pushed' in out, out
    assert '# AI Flow run' in (local / 'report' / 'README.md').read_text()
    assert pushes() == [], pushes()

    # a run in a folder of an existing page
    run = p / 'run'; folder = 'rig/boards/rig-carrier'
    r = call(run, 'intake', '--prompt-time', '2026-10-21T01:00:00Z', '--engine', 'test', '--page', 'john/enc', '--page-dir', folder + '/')
    assert r.returncode == 0, r.stderr
    assert data(run)['page'] == 'john/enc' and data(run)['pageDir'] == folder, data(run)
    r = call(run, 'report', '--push'); out = r.stdout + r.stderr
    assert r.returncode == 0, out
    rid = data(run)['reportId']
    assert f'https://wiki.adom.inc/john/enc/files/{folder}/docs/runs/{rid}/README.md' in out, out
    ps = pushes(); assert ps, 'nothing pushed'
    files = [f for x in ps for f in x['files']]
    assert all(f.startswith(f'{folder}/docs/runs/{rid}/') for f in files) and not any(x['missing'] for x in ps), ps
    assert all(x['args'][2] == 'john/enc' for x in ps)

    # the clips page: <folder>/docs/clips.md, media under <folder>/docs/clips/, render paths with the folder
    stem = 'window-1-20261021-010000'
    for suf in ['.mp4', '-10x.mp4', '-sheet.png', '-poster.jpg']: (run / f'{stem}{suf}').write_bytes(b'x')
    d = data(run); d['captures'] = [{'file': str(run / f'{stem}.mp4'), 'step': 'intake', 'visit': 1, 'start': '2026-10-21T01:00:00Z', 'stop': '2026-10-21T01:05:00Z'}]
    (run / 'run.json').write_text(json.dumps(d))
    log.unlink()
    r = call(run, 'clips', '--push'); out = r.stdout + r.stderr
    assert r.returncode == 0, out
    assert f'https://wiki.adom.inc/john/enc/files/{folder}/docs/clips.md' in out, out
    ps = pushes(); files = [f for x in ps for f in x['files']]
    assert f'{folder}/docs/clips.md' in files and f'{folder}/docs/clips/{stem}-10x.mp4' in files, files
    assert all(f.startswith(f'{folder}/docs/') for f in files) and not any(x['missing'] for x in ps), ps
    md = next(x['texts'][f'{folder}/docs/clips.md'] for x in ps if f'{folder}/docs/clips.md' in x.get('texts', {}))
    assert f'video="/john/enc/render/{folder}/docs/clips/{stem}-10x.mp4"' in md and 'render/docs/' not in md, md
    # the remote clips index is looked up in the folder
    shows = [json.loads(l)['args'] for l in log.read_text().splitlines() if '"show"' in l]
    assert ['repo', 'show', 'john/enc', f'{folder}/docs/clips.md'] in shows, shows

    # clips --page-dir alone moves only the clips page; `.` is the page root
    log.unlink()
    assert call(run, 'clips', '--page-dir', 'rig/clips-only').returncode == 0
    assert data(run)['clipsPage'] == 'john/enc' and data(run)['clipsPageDir'] == 'rig/clips-only' and data(run)['pageDir'] == folder, data(run)
    assert call(run, 'report', '--page-dir', '.').returncode == 0 and data(run).get('pageDir') is None
    # project-page --sync: the step sub-readmes and the README section in the project page's folder
    log.unlink()
    d = data(run); d['sequence'] = ['intake', 'rules']; d.setdefault('steps', {}).setdefault('intake', {})['visits'] = [{'at': '2026-10-21T01:00:00Z'}]
    (run / 'run.json').write_text(json.dumps(d))
    r = call(run, 'project-page', '--sync', '--page', 'john/enc', '--page-dir', folder); out = r.stdout + r.stderr
    assert r.returncode == 0 and data(run)['projectPageDir'] == folder, out
    ps = pushes(); files = [f for x in ps for f in x['files']]
    assert f'{folder}/README.md' in files and f'{folder}/steps/01-intake/README.md' in files, files
    assert all(f.startswith(folder + '/') for f in files) and not any(x['missing'] for x in ps), ps
    step_md = ps[0]['texts'][f'{folder}/steps/01-intake/README.md']
    assert f'video="/john/enc/render/rig/clips-only/docs/clips/{stem}-10x.mp4"' in step_md, step_md
    assert call(run, 'project-page', '--page', 'john/enc', '--page-dir', folder, '--create').returncode != 0
print('PASS: --page-dir refused when not a plain relative folder or without a page; report with no page writes <run>/report/README.md and pushes nothing; report and clips push only under the folder with folder URLs and render paths; project-page --sync writes the step pages and README section in the folder; --create with --page-dir refused')