diff --git a/.github/workflows/soft-fold-limit.yml b/.github/workflows/soft-fold-limit.yml index 839b99a47..944dc39d3 100644 --- a/.github/workflows/soft-fold-limit.yml +++ b/.github/workflows/soft-fold-limit.yml @@ -5,6 +5,7 @@ name: Soft fold limit exact controls paths: - 'frontiers/soft_fold_limit_20261002/**' - '.github/workflows/soft-fold-limit.yml' + - 'tests/test_soft_fold_workflow.py' # bound merged sources (consumed and cited in SOURCES.json); the cited unmerged sources are bound through the API - 'frontiers/three_fronts_20260924/LIFETIME_REMAINDER.md' - 'imports/lifetime_parent_20260925/UNIFORM_MATRIX_CAP_AND_LIFETIME.md' @@ -37,6 +38,13 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with: python-version: '3.11.16' + - name: Exercise complete-shell output contracts in both modes + env: + PYTHONDONTWRITEBYTECODE: '1' + run: | + set -euo pipefail + python -B -S -m unittest discover -s tests -p test_soft_fold_workflow.py -v + python -B -O -S -m unittest discover -s tests -p test_soft_fold_workflow.py -v - name: Verify sources, replay both modes, reject mutants env: PYTHONDONTWRITEBYTECODE: '1' @@ -150,17 +158,33 @@ jobs: 'via': where, 'current_ref_checked': now, 'dependency_merged': merged})) script = str(root / 'fold_check.py') expected = (root / 'RESULTS.json').read_bytes() + # Fixed native carriers measured at original checker c2e1e77e in run37464626067. + failures = { + 'M1': b'mutant M1: failing controls F1_model_pins_and_weight\n', + 'M2': b'mutant M2: failing controls F2_lemma_FL2_generic\n', + 'M3': b'mutant M3: failing controls F3_lemma_FL1_window\n', + 'M4': b'mutant M4: failing controls F4_proposition_FL4_certificates\n', + 'M5': b'mutant M5: failing controls F5_theorem_FL_bookkeeping\n', + 'M6': b'mutant M6: failing controls F6_corollaries\n', + 'M7': b'mutant M7: failing controls F4_proposition_FL4_certificates\n', + 'M8': b'mutant M8: failing controls F7_proposition_FL7_identification\n', + } + inventory = {'M1': 'F1', 'M2': 'F2', 'M3': 'F3', 'M4': 'F4', + 'M5': 'F5', 'M6': 'F6', 'M7': 'F4', 'M8': 'F7'} + if (type(manifest['mutants']) is not dict or list(manifest['mutants']) != list(inventory) + or manifest['mutants'] != inventory): + raise RuntimeError('complete ordered M1-M8 control mapping required') + stages = [('baseline', [], 0, expected, b'')] + stages += [(m, ['--mutant', m], 1, b'', carrier) for m, carrier in failures.items()] + stages += [('M99', ['--mutant', 'M99'], 2, b'', b'unknown mutant label\n')] for flags in (['-B', '-S'], ['-B', '-O', '-S']): - out = subprocess.run([sys.executable, *flags, script], check=True, capture_output=True, timeout=600).stdout - if out != expected: - raise RuntimeError('output differs from RESULTS.json under ' + ' '.join(flags)) - for m in manifest['mutants']: - rc = subprocess.run([sys.executable, *flags, script, '--mutant', m], capture_output=True, timeout=600).returncode - if rc != 1: - raise RuntimeError('mutant ' + m + ' not rejected') - rc = subprocess.run([sys.executable, *flags, script, '--mutant', 'M99'], capture_output=True, timeout=600).returncode - if rc != 2: - raise RuntimeError('unknown mutant label not rejected with exit 2') + for label, args, code, stdout, stderr in stages: + result = subprocess.run([sys.executable, *flags, script, *args], capture_output=True, timeout=600) + if (result.returncode, result.stdout, result.stderr) != (code, stdout, stderr): + raise RuntimeError(label + ': complete exit/stdout/stderr contract differs under ' + ' '.join(flags)) + print(json.dumps({'stage': label, 'flags': flags, 'returncode': result.returncode, + 'stdout_sha256': hashlib.sha256(result.stdout).hexdigest(), + 'stderr_sha256': hashlib.sha256(result.stderr).hexdigest()}, sort_keys=True), flush=True) print(json.dumps({'scientific_effect': 'NONE', 'mathematical_acceptance': False})) PY git diff --exit-code diff --git a/reviews/soft_fold_protocol_20261006/SCOPE.md b/reviews/soft_fold_protocol_20261006/SCOPE.md new file mode 100644 index 000000000..fd026eeba --- /dev/null +++ b/reviews/soft_fold_protocol_20261006/SCOPE.md @@ -0,0 +1,151 @@ +# Soft-fold workflow output contract + +Dylan Roy — delegated AI engineering. Actual author OpenAI / GPT-6 Astra Pro, +`soft-fold-protocol-followthrough-20261006`; #322 pickup6016299453. +Scientific effect NONE; organizational-independence credit0. No author self-merge. + +## Original sources and independently measured carriers + +Source cut11f9127f00ce922e5d39b0da42af76134eb78a21. Original workflow +`.github/workflows/soft-fold-limit.yml`:10578 bytes, Git blob +839b99a47a4b9d0a79525216118717b22bad4614, SHA256 +0aec5cb657a2fa37bab5f77f4f77ca6f978fbd9297a0b56b691b0f93d6ec2c6f. +Checker `frontiers/soft_fold_limit_20261002/fold_check.py` is unchanged blob +c2e1e77e9f16bc7c62fbb855b55dc2f72e5a8cc2. RESULTS is unchanged blob +604c53fadd71312baa3b4bfdbbf1760eddc4ac66. + +An observation-only draft predecessor e710ba569feb5d66f5bac282de689758a9b22111 +first obtained actual pinned-runtime evidence. Original run37464626067/1, +job112272450916, checked out64831bd92336733780928dc40e55db5304025123 +(parents11f9127f+e710ba56), installed CPython3.11.16/Git2.55.0, authenticated +both workflow and checker, executed the exact original complete source preflight, +and ran20 authentic processes. Complete original log read; selected observations +are recorded in PR370/6016447033. No local authentic checker replay is claimed: +Git clone failed DNS and direct source download was unavailable. + +Both baseline invocations exit0 with2903-byte frozen RESULTS stdout, SHA256 +459bbdf3c84b77548e53824df889b13a8ebd0fe6b0d7d7cadf8a4d73821641e9, +and empty stderr. All eight named mutants exit1 with empty stdout and exactly: + +`mutant LABEL: failing controls CONTROL\n` + +|Label|CONTROL| +|---|---| +|M1|F1_model_pins_and_weight| +|M2|F2_lemma_FL2_generic| +|M3|F3_lemma_FL1_window| +|M4|F4_proposition_FL4_certificates| +|M5|F5_theorem_FL_bookkeeping| +|M6|F6_corollaries| +|M7|F4_proposition_FL4_certificates| +|M8|F7_proposition_FL7_identification| + +M4 and M7 name the same control, but their complete label-bearing lines differ. +M99 exits2, empty stdout, exactly `unknown mutant label\n` on stderr. +All ten corresponding stdout/stderr identities match between interpreter modes. +The native main function reports the sorted complete failing-control set, so +an extra failed control is not accepted as the intended single failure. +Observation success alone did not establish a correct validator. Its temporary +workflow is removed from the final proposed tree, retained only in history. + +## Repair and preservation + +Only the existing inline process loop changes behavior. Require the exact native +ordered M1-M8 control mapping before children; construct the fixed20-command inventory; compare each +actual exit/stdout/stderr tuple with its independently measured complete contract. +The baseline retains byte-exact RESULTS and now requires empty stderr. Mutants +and M99 require empty stdout and their entire stderr, including the newline. +Whitespace changes are deliberately rejected for these deterministic text CLIs. +No expectations come from the child under validation; no scientific checker or +RESULTS generation, parser adapter, extra production helper/import is introduced. +Launch/timeout exceptions propagate; cwd, capture_output=True,600-second child +budget, both original modes and command arguments are unchanged. Accepted calls +print stream identities. Failure raw-output durability is not newly guaranteed. + +The7599-byte source preflight remains byte-identical, SHA256 +2ad437f73e5f0c27482dd272144deadc2959d85e946c40f0226c51ce6abe5211. +The137-byte final JSON/Git postflight remains byte-identical, SHA256 +752aa03685dbbe54ec40c3e91b8709eb6453c41c146e9636b295b2129c586cdc. +Sizes include YAML indentation. All packet, proof, manifest, result and consumed +or cited dependency bytes remain unchanged. So do action/runtime pins, read-only +permissions and ten-minute job budget. One test trigger and two focused discovery +commands are added. Existing D5/Palm/factorial tests/** triggers provide their +separate complete repository discovery on the actual candidate. + +## Test-first local execution + +Actual CPython3.13.5/Git2.47.3; nine unittest methods,147 distinct committed real-Git +whole-shell scenarios. Full uninterrupted original command:111 intended assertion +failures,0 errors (132.243s). Breakdown:96 named-mutant false admissions,2 baseline +stderr false admissions,8 unknown-label false admissions,4 malformed-inventory +admissions,1 missing-wiring assertion. One valid admission and36 correct rejecting +controls remain. The original executes2504 child commands. + +The unchanged final test file runs against the repaired workflow:9/9 methods, +147/147 expected outcomes in EACH normal/optimized outer mode (65.548s/65.565s). +Each has one valid admission and146 rejections,1444 actual children. No skipped +or partitioned cases and no subprocess mocks. Across the three completed runs: +441 fixture records,882 raw shell streams and5392 child invocations. These are +repetitions of147 scenario types, not441 distinct defects. + +All eight mutant stages and both child modes are targeted. Every protocol +rejection must reach and stop at its exact ordered target; nine source/tree +controls and four malformed inventories must stop before any child. The final +tracked-output mutation reaches all20 invocations before Git rejects it. Real +Git configuration is isolated to temporary environments/commands; no user signing, +hooks or maintenance settings are changed. Synthetic baseline/carriers never +import expectations from production code. + +The local fixtures have nonempty tracked consumed and cited source pins. Their +remote-source arrays are empty; no live API replay is claimed locally. The +original hosted preflight actually resolved consumed #242 as merged, checked +current ref54c6759e8434846941eb5218601dfef69c2c9d98 and source blob +271412dbc96a29590f5f805258e15c9e335cb430 via TREE. The unchanged historical +head/path/blob fallback was not exercised by that observation. The final dedicated +workflow must supply its own actual full-packet candidate execution. + +## Required author correction after the first candidate failed + +Initial published head77b1b76f1c0b4a62e2359e83903c7b86a0c0db53 had a wrong +list-only inventory assumption, mirrored in its synthetic fixtures. Actual +SOURCES.json blob37b8a38a42a2dddbc4b2fdfdbae3a9cd4e28d7a6 stores an ordered +object: M1:F1,M2:F2,M3:F3,M4:F4,M5:F5,M6:F6,M7:F4,M8:F7. The observation +loop had correctly expanded its keys, but the first implementation wrongly +required a list. Dedicated37466840210/1/job112279898907 therefore failed +at that inventory check BEFORE any genuine checker invocation, after its +complete focused suites passed74.486s/73.334s and source preflight succeeded. +This is an author implementation/fixture error, not runner failure or a false +scientific result. Original failed execution is retained in370/6016816642. + +The positive fixture was corrected FIRST to the native object. Its valid-case +test then produced exactly one intended assertion failure and zero errors in +each mode against77b1. Only afterwards the matcher was corrected to validate +the exact dictionary type, key order AND all control values. Missing/reordered +mappings, duplicate-label lists and wrong value types remain rejecting controls. +The original scientific manifest was not edited. The complete147-case local +results above are the successor mapping-based tests, not the older list-fixture +results. The same147 keys and nine methods are retained, with four fixture +construction lines corrected. All earlier list-fixture executions and original +failed CI remain separate historical evidence; none is overwritten as success. + +The correction changes only the inventory predicate/fixed native mapping, the +fixture's native inventory construction and this scope record. Full failure +carriers, source preflight, postflight and all scientific bytes stay unchanged. +An original review of77b1 cannot be represented as final successor review. The +corrected source requires its own applicable fresh CI and exact-source review. + +## Review and integration boundary + +This is engineering validation, not the proof of FL, numerical certification, +a false theorem finding, hostile-source sandbox, bounded-output service, durable +storage guarantee or independent formal alignment. Existing mathematical scope +and status remain. No complete local repository checkout or local Lean execution +is claimed. Original failures and observation-only records remain historical; +old CI cannot be rebound to the successor repair or a later main. + +Require an actual nonauthor engineering review of this final three-file delta, +all applicable fresh dedicated/general checks on its actual candidate, and a +separate eligible nonauthor/nonreader integrator's live ownership/ref/diff/source/ +review/thread reconciliation before ordinary expected-head merge and landed/push +verification. #322 remains open for the broader wrapper class. No new owner +approval, duplicate scientific register, main-tip lock or timer is requested. diff --git a/tests/test_soft_fold_workflow.py b/tests/test_soft_fold_workflow.py new file mode 100644 index 000000000..cc23e4fb2 --- /dev/null +++ b/tests/test_soft_fold_workflow.py @@ -0,0 +1,244 @@ +"""Real whole-shell protocol fixtures; synthetic outputs, not a field theorem.""" +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import textwrap +import unittest + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / '.github/workflows/soft-fold-limit.yml' +PACKET = 'frontiers/soft_fold_limit_20261002' +MUTANTS = tuple('M' + str(i) for i in range(1, 9)) +INVENTORY = dict(zip(MUTANTS, ('F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F4', 'F7'))) +STAGES = ('baseline', *MUTANTS, 'M99') +ORDER = [[mode, stage] for mode in (0, 1) for stage in STAGES] +# Independent reference carriers from original hosted job112272450916. +CONTROLS = ('F1_model_pins_and_weight', 'F2_lemma_FL2_generic', + 'F3_lemma_FL1_window', 'F4_proposition_FL4_certificates', + 'F5_theorem_FL_bookkeeping', 'F6_corollaries', + 'F4_proposition_FL4_certificates', 'F7_proposition_FL7_identification') +EXPECTED_STDERR = {m: ('mutant ' + m + ': failing controls ' + c + '\n').encode() + for m, c in zip(MUTANTS, CONTROLS)} +EXPECTED_STDERR.update(baseline=b'', M99=b'unknown mutant label\n') +BASELINE = b'{"synthetic": "soft-fold baseline"}\n' + + +def verification_shell(text): + """Extract only the existing named step, without replacing any source guard.""" + marker = ' - name: Verify sources, replay both modes, reject mutants\n' + if text.count(marker) != 1: + raise ValueError('one original verification step required') + tail = text.split(marker, 1)[1].split(' run: |\n', 1)[1] + lines = [] + for line in tail.splitlines(): + if line and not line.startswith(' '): + break + lines.append(line[10:] if line else '') + if not lines or lines[0] != 'set -euo pipefail': + raise ValueError('original strict shell missing') + return '\n'.join(lines) + '\n' + + +def file_identity(name, data): + return {'path': name, 'bytes': len(data), + 'sha256': hashlib.sha256(data).hexdigest(), + 'git_blob': hashlib.sha1(b'blob %d\0' % len(data) + data).hexdigest()} + + +def synthetic_checker(): + return ('import json, os, signal, sys\nfrom pathlib import Path\n' + + 'messages = ' + repr(EXPECTED_STDERR) + '\n' + + 'baseline = ' + repr(BASELINE) + '\n' + textwrap.dedent(r''' + args = sys.argv[1:] + label = 'baseline' if not args else args[1] + mode = sys.flags.optimize + with open(os.environ['SOFT_FOLD_TRACE'], 'a') as stream: + stream.write(json.dumps([mode, label]) + '\n') + code = 0 if label == 'baseline' else 2 if label == 'M99' else 1 + out = baseline if label == 'baseline' else b'' + err = messages[label] + case = os.environ['SOFT_FOLD_CASE'] + if case == 'dirty': + Path(os.environ['SOFT_FOLD_RESULTS']).write_bytes(b'changed after preflight\n') + if mode == int(os.environ['SOFT_FOLD_MODE']) and label == os.environ['SOFT_FOLD_STAGE']: + if case == 'crash': + raise RuntimeError('unrelated synthetic crash') + if case == 'silent': + out = err = b'' + elif case == 'wrong-control': + err = b'unrelated failure reason\n' + elif case == 'extra-stdout': + out += b'unexpected stdout\n' + elif case == 'prefix': + err = b'prefix ' + err + elif case == 'extra-newline': + err += b'\n' + elif case == 'stderr': + err += b'unexpected stderr\n' + elif case == 'garbage': + out = b'not the frozen result\n' + elif case == 'wrong-exit': + code = 1 if label == 'baseline' else 0 + elif case == 'signal': + os.kill(os.getpid(), signal.SIGTERM) + sys.stdout.buffer.write(out) + sys.stderr.buffer.write(err) + sys.exit(code) + ''')) + + +class SoftFoldWorkflowTests(unittest.TestCase): + def run_case(self, case='valid', stage='baseline', mode=0, guard=None): + with tempfile.TemporaryDirectory(prefix='soft-fold-fixture-') as tmp: + home = Path(tmp) + repo = home / 'repo' + packet = repo / PACKET + packet.mkdir(parents=True) + trace = home / 'trace.jsonl' + script = packet / 'fold_check.py' + script.write_text(synthetic_checker(), encoding='utf-8') + (packet / 'RESULTS.json').write_bytes(BASELINE) + manifest = {'files': [file_identity(p.name, p.read_bytes()) for p in sorted(packet.iterdir())], + 'mutants': dict(INVENTORY), 'consumed': [], 'cited_only': [], + 'consumed_unmerged': [], 'cited_unmerged': []} + for kind in ('consumed', 'cited_only'): + name = kind + '.txt' + data = ('nonempty synthetic pinned ' + kind + '\n').encode() + (repo / name).write_bytes(data) + manifest[kind] = [file_identity(name, data)] + manifest_path = packet / 'SOURCES.json' + manifest_path.write_text(json.dumps(manifest)) + env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')} + env.update(HOME=str(home), GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull, + GIT_AUTHOR_NAME='Fixture', GIT_AUTHOR_EMAIL='fixture@example.invalid', + GIT_COMMITTER_NAME='Fixture', GIT_COMMITTER_EMAIL='fixture@example.invalid', + PYTHONDONTWRITEBYTECODE='1', GH_REPOSITORY='fixture/fixture', GH_TOKEN='unused', + SOFT_FOLD_CASE=case, SOFT_FOLD_STAGE=stage, SOFT_FOLD_MODE=str(mode), + SOFT_FOLD_TRACE=str(trace), SOFT_FOLD_RESULTS=str(packet / 'RESULTS.json')) + env['PATH'] = str(Path(sys.executable).parent) + os.pathsep + env.get('PATH', '') + + def git(*args): + return subprocess.run(['git', '-c', 'commit.gpgsign=false', '-c', 'maintenance.auto=false', + '-c', 'core.hooksPath=' + os.devnull, *args], + cwd=repo, env=env, check=True, capture_output=True, timeout=15) + + git('init', '-q') + git('add', '.') + git('commit', '-qm', 'synthetic authenticated fixture') + if guard == 'packet-hash': + script.write_text(script.read_text() + '\n# changed after manifest\n') + elif guard == 'missing-packet': + (packet / 'RESULTS.json').unlink() + elif guard == 'extra-member': + (packet / 'extra.txt').write_bytes(b'extra') + elif guard == 'packet-symlink': + target = home / 'copy' + target.write_bytes(BASELINE) + (packet / 'RESULTS.json').unlink() + (packet / 'RESULTS.json').symlink_to(target) + elif guard in ('consumed-hash', 'cited-hash'): + (repo / ('consumed.txt' if guard == 'consumed-hash' else 'cited_only.txt')).write_bytes(b'tampered\n') + elif guard == 'consumed-mode': + git('update-index', '--chmod=+x', 'consumed.txt') + git('commit', '-qm', 'wrong tracked mode') + elif guard == 'consumed-missing': + (repo / 'consumed.txt').unlink() + elif guard == 'untracked-pin': + git('rm', '--cached', 'consumed.txt') + git('commit', '-qm', 'untracked consumed file') + elif guard and guard.startswith('inventory-'): + variants = {'inventory-missing': {m: INVENTORY[m] for m in MUTANTS[:-1]}, + 'inventory-order': {m: INVENTORY[m] for m in reversed(MUTANTS)}, + 'inventory-duplicate': [*MUTANTS, 'M8'], + 'inventory-object': dict.fromkeys(MUTANTS, 1)} + manifest['mutants'] = variants[guard] + manifest_path.write_text(json.dumps(manifest)) + git('add', '.') + git('commit', '-qm', 'coherent inventory variant') + result = subprocess.run(['bash', '--noprofile', '--norc', '-c', verification_shell(WORKFLOW.read_text())], + cwd=repo, env=env, capture_output=True, timeout=40) + calls = [json.loads(line) for line in trace.read_text().splitlines()] if trace.exists() else [] + if destination := os.environ.get('SOFT_FOLD_SAVE'): + saved = Path(destination) / ('-'.join((case, stage, str(mode), str(guard)))) + saved.mkdir(parents=True, exist_ok=False) + (saved / 'stdout.bin').write_bytes(result.stdout) + (saved / 'stderr.bin').write_bytes(result.stderr) + record = {'case': case, 'stage': stage, 'mode': mode, 'guard': guard, + 'returncode': result.returncode, 'calls': calls, + 'stdout_sha256': hashlib.sha256(result.stdout).hexdigest(), + 'stderr_sha256': hashlib.sha256(result.stderr).hexdigest()} + (saved / 'record.json').write_text(json.dumps(record, sort_keys=True) + '\n') + return result, calls + + def reject_at(self, case, stage, mode): + result, calls = self.run_case(case, stage, mode) + prefix = ORDER[:ORDER.index([mode, stage]) + 1] + self.assertIn([mode, stage], calls, 'targeted command must actually run') + self.assertNotEqual(result.returncode, 0, 'wrong-result admission: ' + repr((case, stage, mode))) + self.assertEqual(calls, prefix, 'failure must stop at exactly its intended stage') + + def test_valid_complete_inventory(self): + result, calls = self.run_case() + self.assertEqual(result.returncode, 0, result.stderr.decode(errors='replace')) + self.assertEqual(calls, ORDER) + + def test_every_named_rejection_requires_its_full_carrier(self): + for case in ('silent', 'crash', 'wrong-control', 'extra-stdout', 'prefix', 'extra-newline'): + for mode in (0, 1): + for stage in MUTANTS: + with self.subTest(case=case, stage=stage, mode=mode): + self.reject_at(case, stage, mode) + + def test_named_wrong_exits(self): + for mode in (0, 1): + for stage in MUTANTS: + with self.subTest(stage=stage, mode=mode): + self.reject_at('wrong-exit', stage, mode) + + def test_baseline_exact_output_and_empty_stderr(self): + for case in ('stderr', 'silent', 'garbage', 'wrong-exit'): + for mode in (0, 1): + with self.subTest(case=case, mode=mode): + self.reject_at(case, 'baseline', mode) + + def test_unknown_complete_diagnostic(self): + for case in ('silent', 'wrong-control', 'extra-stdout', 'prefix', 'wrong-exit', 'signal'): + for mode in (0, 1): + with self.subTest(case=case, mode=mode): + self.reject_at(case, 'M99', mode) + + def test_source_guards_precede_children(self): + for guard in ('packet-hash', 'missing-packet', 'extra-member', 'packet-symlink', + 'consumed-hash', 'cited-hash', 'consumed-mode', 'consumed-missing', 'untracked-pin'): + with self.subTest(guard=guard): + result, calls = self.run_case(guard=guard) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, []) + + def test_ordered_inventory_before_any_child(self): + for guard in ('inventory-missing', 'inventory-order', 'inventory-duplicate', 'inventory-object'): + with self.subTest(guard=guard): + result, calls = self.run_case(guard=guard) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, []) + + def test_final_git_guard_remains_effective(self): + result, calls = self.run_case('dirty') + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, ORDER) + self.assertIn(b'changed after preflight', result.stdout) + + def test_focused_suite_is_wired(self): + text = WORKFLOW.read_text() + self.assertIn("- 'tests/test_soft_fold_workflow.py'", text) + for flags in ('-B -S', '-B -O -S'): + self.assertIn('python ' + flags + ' -m unittest discover -s tests -p test_soft_fold_workflow.py -v', text) + + +if __name__ == '__main__': + unittest.main()