owfadev 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/owfa-check/SKILL.md +49 -0
- package/.agents/skills/{docs → owfa-docs}/SKILL.md +5 -5
- package/.agents/skills/{wiki_plugin_kernel → owfa-kernel}/SKILL.md +13 -12
- package/{.meta/command/plugin → .agents/skills/owfa-plugin}/SKILL.md +10 -10
- package/.agents/skills/{query → owfa-query}/SKILL.md +5 -6
- package/.owfa/VERSION +1 -0
- package/.owfa/command/owfa-check/SKILL.md +49 -0
- package/{.meta/command/docs → .owfa/command/owfa-docs}/SKILL.md +5 -5
- package/{.meta/command/wiki_plugin_kernel → .owfa/command/owfa-kernel}/SKILL.md +13 -12
- package/{.agents/skills/plugin → .owfa/command/owfa-plugin}/SKILL.md +10 -10
- package/{.meta/command/query → .owfa/command/owfa-query}/SKILL.md +5 -6
- package/{.meta → .owfa}/docs/agent-behavior.md +3 -2
- package/{.meta → .owfa}/docs/agents-example.md +1 -1
- package/{.meta → .owfa}/docs/conversion.md +2 -2
- package/{.meta → .owfa}/docs/deployment.md +51 -46
- package/{.meta → .owfa}/docs/developer.md +4 -4
- package/{.meta → .owfa}/docs/distribution.md +21 -19
- package/.owfa/docs/home.md +61 -0
- package/{.meta → .owfa}/docs/intro.md +5 -5
- package/{.meta → .owfa}/docs/log-example.md +1 -1
- package/{.meta → .owfa}/docs/map.md +4 -3
- package/{.meta → .owfa}/docs/mechanics.md +11 -11
- package/{.meta → .owfa}/docs/naming.md +7 -7
- package/{.meta → .owfa}/docs/packages.md +80 -15
- package/.owfa/docs/plugin-lifecycle.md +70 -0
- package/{.meta → .owfa}/docs/quickstart.md +12 -12
- package/{.meta → .owfa}/docs/research-user-profile.md +1 -1
- package/{.meta → .owfa}/docs/sandbox.md +3 -3
- package/{.meta → .owfa}/docs/usage.md +1 -1
- package/{.meta → .owfa}/protocol/actions.md +6 -6
- package/.owfa/protocol/registry.yaml +34 -0
- package/.owfa/scripts/owfa_home.py +273 -0
- package/{.meta/scripts/wiki_plugin_kernel.py → .owfa/scripts/owfa_kernel.py} +521 -77
- package/{.meta → .owfa}/scripts/pipeline.py +9 -9
- package/.owfa/scripts/wiki_plugin_kernel.py +15 -0
- package/{.meta → .owfa}/scripts/wikilib.py +2 -2
- package/.owfa/tests/test_capabilities.py +311 -0
- package/{.meta → .owfa}/tests/test_install_preflight.py +11 -7
- package/.owfa/tests/test_owfa_home.py +127 -0
- package/.owfa/tests/test_version_scheme.py +63 -0
- package/README.md +32 -24
- package/README.zh-CN.md +32 -24
- package/bin/owfa.js +525 -65
- package/package.json +3 -2
- package/payloads/personal/PKG.yaml +15 -0
- package/payloads/personal/README.md +7 -0
- package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.yaml +3 -3
- package/{.agents/skills/map → payloads/personal/command/owfa-map}/SKILL.md +9 -9
- package/{.meta/command/profile → payloads/personal/command/owfa-profile}/SKILL.md +11 -11
- package/{.agents/skills/save → payloads/personal/command/owfa-save}/SKILL.md +10 -10
- package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/domain/PLUGIN.md +2 -0
- package/payloads/personal/domain/PLUGIN.yaml +8 -0
- package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.yaml +1 -1
- package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.md +5 -0
- package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/link/scripts/check.py +1 -1
- package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.yaml +1 -1
- package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.md +2 -1
- package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.yaml +5 -5
- package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/structure/scripts/check.py +1 -1
- package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/tag/scripts/check.py +3 -1
- package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.yaml +6 -5
- package/payloads/personal/todo/scripts/check.py +51 -0
- package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.yaml +7 -6
- package/payloads/personal/user-profile/scripts/check.py +20 -0
- package/{.meta/plugins/core → payloads/personal}/vault/PLUGIN.md +1 -0
- package/payloads/personal/vault/PLUGIN.yaml +13 -0
- package/{.meta/plugins/core → payloads/personal}/wiki/PLUGIN.md +1 -0
- package/payloads/personal/wiki/PLUGIN.yaml +8 -0
- package/templates/AGENTS.template.md +2 -2
- package/.agents/skills/check/SKILL.md +0 -130
- package/.agents/skills/profile/SKILL.md +0 -75
- package/.meta/VERSION +0 -1
- package/.meta/command/check/SKILL.md +0 -130
- package/.meta/command/map/SKILL.md +0 -72
- package/.meta/command/save/SKILL.md +0 -94
- package/.meta/plugins/core/domain/PLUGIN.yaml +0 -8
- package/.meta/plugins/core/vault/PLUGIN.yaml +0 -13
- package/.meta/plugins/core/wiki/PLUGIN.yaml +0 -8
- package/.meta/protocol/registry.yaml +0 -75
- /package/{.meta → .owfa}/command/asset-read/SKILL.md +0 -0
- /package/{.meta → .owfa}/docs/research-landscape.md +0 -0
- /package/{.meta → .owfa}/protocol/experiments.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/device/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/instance-git/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/language/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/log/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/mapping/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/sessions/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/trust/scripts/check.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Derived-layer post-write pipeline: index / tags / hot / log / verify.
|
|
4
4
|
|
|
5
|
-
Positioning (.
|
|
5
|
+
Positioning (.owfa/protocol/actions.md execution principles): deterministic structural
|
|
6
6
|
operations go to this script; semantic judgment goes to the LLM. Rebuild and rolling
|
|
7
7
|
rules and window parameters are governed by this file's source (the script source is
|
|
8
8
|
the rule list); PLUGIN.md keeps the semantic explanation; commands uniformly end by
|
|
@@ -11,11 +11,11 @@ post-write self-attestation (the minimal attester form): the LLM executes, the s
|
|
|
11
11
|
certifies.
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
|
-
python .
|
|
15
|
-
python .
|
|
16
|
-
python .
|
|
17
|
-
python .
|
|
18
|
-
python .
|
|
14
|
+
python .owfa/scripts/pipeline.py index # rebuild indexes (overflow-offloading scheme, idempotent; includes deleting surplus indexes after merging back)
|
|
15
|
+
python .owfa/scripts/pipeline.py tags # rebuild wiki/tags.md (idempotent)
|
|
16
|
+
python .owfa/scripts/pipeline.py hot <type> <one-liner> # evict out-of-window entries + prepend a hot-cache entry
|
|
17
|
+
python .owfa/scripts/pipeline.py log <type> <one-liner> [--domain domain] # capacity archive + prepend a log line (domain tag optional — domainless transactions)
|
|
18
|
+
python .owfa/scripts/pipeline.py verify # post-write self-attestation: attached audits + derived-area drift check
|
|
19
19
|
|
|
20
20
|
Type enumeration: map / save / query / check / plugin / todo / profile / other (the log plugin).
|
|
21
21
|
Concept-page determination (what enters the index): all .md under wiki/, excluding —
|
|
@@ -54,9 +54,9 @@ LOG_MAX_ENTRIES = 100
|
|
|
54
54
|
INDEX_MAX_ENTRIES = 25 # single index-list window (page entries + directory entries; overflow-offloading split basis)
|
|
55
55
|
TYPE_ORDER = ["map", "save", "query", "check", "plugin", "todo", "profile", "other"]
|
|
56
56
|
HOT_HEADER = ("# 热缓存\n\n> 最近变更摘要;≤25 条且 <5 日,窗外即删;可整体再生。"
|
|
57
|
-
"规则见 `.
|
|
57
|
+
"规则见 `.owfa/plugins/hot/`,写走 `pipeline.py hot`。\n")
|
|
58
58
|
LOG_HEADER = ("# 运行日志\n\n> 置顶追加、只增不删;条目 = `- YYYY-MM-DD <类型> [<域>]:一句话`(域标可缺省——无域事务,log 桥)。"
|
|
59
|
-
"规则见 `.
|
|
59
|
+
"规则见 `.owfa/plugins/log/`,写走 `pipeline.py log`。\n")
|
|
60
60
|
DATE_RE = re.compile(r"^- (\d{4}-\d{2}-\d{2}) ")
|
|
61
61
|
|
|
62
62
|
|
|
@@ -391,7 +391,7 @@ def do_log(kind, text, domain=None):
|
|
|
391
391
|
def do_verify():
|
|
392
392
|
ok = True
|
|
393
393
|
r = subprocess.run(
|
|
394
|
-
[sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), "
|
|
394
|
+
[sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), "owfa_kernel.py"), "audit"])
|
|
395
395
|
if r.returncode != 0:
|
|
396
396
|
ok = False
|
|
397
397
|
print("[verify] plugin audit has errors")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Deprecation shim: the kernel was renamed owfa_kernel.py (0.4 layout).
|
|
4
|
+
|
|
5
|
+
Keeps 0.3-era paths and muscle memory working through the 0.4 cycle; the shim
|
|
6
|
+
is scheduled for removal in 0.5. The real entry is owfa_kernel.py next to it.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import runpy
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
print("[deprecated] wiki_plugin_kernel.py -> owfa_kernel.py (shim retires in 0.5)", file=sys.stderr)
|
|
14
|
+
sys.argv[0] = os.path.join(_HERE, "owfa_kernel.py")
|
|
15
|
+
runpy.run_path(os.path.join(_HERE, "owfa_kernel.py"), run_name="__main__")
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"""Wiki page-service load-bearing piece: frontmatter parsing and page traversal, assembled into ctx for the attached-audit runtime.
|
|
4
4
|
|
|
5
5
|
Plugin attached-audit scripts do not import this module themselves —
|
|
6
|
-
|
|
6
|
+
owfa_kernel.audit scans wiki/ in a single pass and puts the (relative path,
|
|
7
7
|
frontmatter, body) list into ctx.pages for everyone to share (frugal invocation).
|
|
8
|
-
The same minimal YAML subset as
|
|
8
|
+
The same minimal YAML subset as owfa_kernel.parse_manifest, but with a
|
|
9
9
|
different tolerance policy: manifests fail loudly (protocol artifacts), page
|
|
10
10
|
frontmatter skips bad lines (user content must not crash attached audits over
|
|
11
11
|
format blemishes). Keys are lenient (anything non-blank and colon-free):
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""v0.4 capability (needs/provides) + suite-kind + INDEX-extension unit tests.
|
|
2
|
+
|
|
3
|
+
Synthetic packages and disposable instances only; registry-channel cases use a
|
|
4
|
+
local git repo + a local ROOT/INDEX.yaml (load_index reads the local file first,
|
|
5
|
+
so no network is ever touched).
|
|
6
|
+
|
|
7
|
+
Run: python -m unittest discover -s .owfa/tests -p test_capabilities.py -v
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
FRAMEWORK = Path(__file__).resolve().parents[2]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CapabilityTests(unittest.TestCase):
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.temp = tempfile.TemporaryDirectory(prefix="kernel-cap-test-")
|
|
25
|
+
self.addCleanup(self.temp.cleanup)
|
|
26
|
+
self.root = Path(self.temp.name) / "instance"
|
|
27
|
+
self.root.mkdir()
|
|
28
|
+
for directory in (".owfa", ".agents"):
|
|
29
|
+
shutil.copytree(FRAMEWORK / directory, self.root / directory,
|
|
30
|
+
ignore=shutil.ignore_patterns("__pycache__", "tests", "smoke-tmp"))
|
|
31
|
+
(self.root / "AGENTS.md").write_text(
|
|
32
|
+
"# Synthetic instance\n<!-- wiki-inject:start -->\n<!-- wiki-inject:end -->\n",
|
|
33
|
+
encoding="utf-8")
|
|
34
|
+
self.source = Path(self.temp.name) / "source"
|
|
35
|
+
self.source.mkdir()
|
|
36
|
+
self.env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", PYTHONIOENCODING="utf-8")
|
|
37
|
+
self.env.pop("PYTHONPATH", None)
|
|
38
|
+
# v0.4 final shape: the framework tree ships no plugins — the synthetic
|
|
39
|
+
# instances bootstrap exactly like a real init (embedded personal suite)
|
|
40
|
+
result = self.run_kernel("install", "personal", "--path", str(FRAMEWORK / "payloads" / "personal"))
|
|
41
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
42
|
+
result = self.run_kernel("all")
|
|
43
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
44
|
+
|
|
45
|
+
def run_kernel(self, *args):
|
|
46
|
+
return subprocess.run(
|
|
47
|
+
[sys.executable, str(self.root / ".owfa/scripts/owfa_kernel.py"), *args],
|
|
48
|
+
cwd=self.root, env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
# ---- synthetic package builders ----
|
|
51
|
+
|
|
52
|
+
def manifest(self, name, inject, depends="[domain, wiki, trust, log]", parent=None):
|
|
53
|
+
directory = (parent or self.source) / name
|
|
54
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
dependencies = "".join(f" - {item.strip()}\n" for item in depends.strip("[]").split(","))
|
|
56
|
+
(directory / "PLUGIN.yaml").write_text(
|
|
57
|
+
f"id: {name}\nschema_version: 1\nversion: 0.1\nupdated: 2026-11-12\n"
|
|
58
|
+
f"depends:\n{dependencies}attachment: []\nfields:\n"
|
|
59
|
+
f"inject: {json.dumps(inject, ensure_ascii=False)}\n", encoding="utf-8")
|
|
60
|
+
(directory / "PLUGIN.md").write_text("# Synthetic test plugin\n", encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
def pkg_dir(self, pid, caps="", version="0.1"):
|
|
63
|
+
"""A package root self.source/<pid>pkg/ holding PKG.yaml + the member dir."""
|
|
64
|
+
directory = self.source / f"{pid}pkg"
|
|
65
|
+
if not (directory / pid).exists():
|
|
66
|
+
self.manifest(pid, f"{pid} synthetic member", parent=directory)
|
|
67
|
+
lines = ["schema_version: 1", f"id: {pid}", f"version: {version}", "compat: '>= 0.3'",
|
|
68
|
+
f"members: [{pid}]", "depends: [framework]"]
|
|
69
|
+
lines += [l for l in caps.splitlines() if l.strip()]
|
|
70
|
+
(directory / "PKG.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
71
|
+
return directory
|
|
72
|
+
|
|
73
|
+
def install_path(self, pid, *extra):
|
|
74
|
+
return self.run_kernel("install", pid, "--path", str(self.source / f"{pid}pkg"), *extra)
|
|
75
|
+
|
|
76
|
+
def git_pkg(self, pid, version, provides=None, needs=None):
|
|
77
|
+
"""A local git repo package (registry-channel source) + tags; returns the repo path."""
|
|
78
|
+
repo = Path(self.temp.name) / "git" / pid
|
|
79
|
+
if repo.exists():
|
|
80
|
+
shutil.rmtree(repo)
|
|
81
|
+
repo.mkdir(parents=True)
|
|
82
|
+
self.manifest(pid, f"{pid} registry member", parent=repo)
|
|
83
|
+
lines = ["schema_version: 1", f"id: {pid}", f"version: {version}", "compat: '>= 0.3'",
|
|
84
|
+
f"members: [{pid}]", "depends: [framework]"]
|
|
85
|
+
if provides:
|
|
86
|
+
lines.append(f"provides: [{', '.join(provides)}]")
|
|
87
|
+
if needs:
|
|
88
|
+
lines.append(f"needs: [{', '.join(needs)}]")
|
|
89
|
+
(repo / "PKG.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
90
|
+
self.git(repo, "init", "-q")
|
|
91
|
+
self.git(repo, "add", "-A")
|
|
92
|
+
self.git(repo, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "pkg")
|
|
93
|
+
self.git(repo, "tag", f"{pid}-v{version}")
|
|
94
|
+
return repo
|
|
95
|
+
|
|
96
|
+
def git(self, repo, *args):
|
|
97
|
+
r = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True)
|
|
98
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
99
|
+
return r
|
|
100
|
+
|
|
101
|
+
def git_head(self, repo):
|
|
102
|
+
return subprocess.run(["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
103
|
+
capture_output=True, text=True).stdout.strip()
|
|
104
|
+
|
|
105
|
+
def index(self, rows):
|
|
106
|
+
lines = ["packages:"] + [f" {name}: {{{body}}}" for name, body in rows]
|
|
107
|
+
(self.root / "INDEX.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
# ---- grammar / prefix ----
|
|
110
|
+
|
|
111
|
+
def test_grammar_and_prefix_rule(self):
|
|
112
|
+
self.pkg_dir("probe", caps="needs: [Bad.Cap]\nprovides: [other.org.v1]")
|
|
113
|
+
result = self.install_path("probe")
|
|
114
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
115
|
+
self.assertIn("fails the capability grammar", result.stdout)
|
|
116
|
+
self.assertIn("first name segment must equal the package id", result.stdout)
|
|
117
|
+
self.assertFalse((self.root / ".owfa/plugins/domains/probe").exists())
|
|
118
|
+
# the prefix message and the grammar message are both reported (one per entry)
|
|
119
|
+
|
|
120
|
+
def test_owfa_prefix_and_own_namespace_pass_grammar(self):
|
|
121
|
+
# owfa.* is the framework-owned namespace; a non-owfa capability named after
|
|
122
|
+
# the declaring package is the legal third-party form — both pass grammar
|
|
123
|
+
self.pkg_dir("probe", caps="provides: [probe.transport.v1, owfa.net.v1]")
|
|
124
|
+
result = self.install_path("probe")
|
|
125
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
126
|
+
|
|
127
|
+
# ---- exact-name matching ----
|
|
128
|
+
|
|
129
|
+
def test_v2_gap_is_reported_as_different_version(self):
|
|
130
|
+
self.pkg_dir("prov", caps="provides: [owfa.org.v1]")
|
|
131
|
+
self.assertEqual(self.install_path("prov").returncode, 0)
|
|
132
|
+
self.pkg_dir("needr", caps="needs: [owfa.org.v2]")
|
|
133
|
+
result = self.install_path("needr")
|
|
134
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
135
|
+
self.assertIn("present at different version", result.stdout)
|
|
136
|
+
self.assertIn("exact-name", result.stdout)
|
|
137
|
+
|
|
138
|
+
def test_unknown_capability_is_missing_entirely(self):
|
|
139
|
+
self.pkg_dir("needr", caps="needs: [owfa.im.v1]")
|
|
140
|
+
result = self.install_path("needr")
|
|
141
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
142
|
+
self.assertIn("capability missing entirely", result.stdout)
|
|
143
|
+
self.assertNotIn("different version", result.stdout)
|
|
144
|
+
|
|
145
|
+
# ---- exclusive slot ----
|
|
146
|
+
|
|
147
|
+
def test_exclusive_slot_refuses_second_provider(self):
|
|
148
|
+
self.pkg_dir("alpha", caps="provides: [owfa.org.v1]")
|
|
149
|
+
self.assertEqual(self.install_path("alpha").returncode, 0)
|
|
150
|
+
self.pkg_dir("beta", caps="provides: [owfa.org.v1]")
|
|
151
|
+
result = self.install_path("beta")
|
|
152
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
153
|
+
self.assertIn("exclusive", result.stdout)
|
|
154
|
+
self.assertIn("alpha", result.stdout)
|
|
155
|
+
self.assertIn("beta", result.stdout)
|
|
156
|
+
self.assertFalse((self.root / ".owfa/plugins/domains/beta").exists())
|
|
157
|
+
|
|
158
|
+
# ---- trust gate ----
|
|
159
|
+
|
|
160
|
+
def test_unverified_provider_needs_yes_and_journals(self):
|
|
161
|
+
# provider installed via --path (no INDEX row): provides are unverified
|
|
162
|
+
self.pkg_dir("prov", caps="provides: [owfa.org.v1]")
|
|
163
|
+
self.assertEqual(self.install_path("prov").returncode, 0)
|
|
164
|
+
self.pkg_dir("needr", caps="needs: [owfa.org.v1]")
|
|
165
|
+
result = self.install_path("needr")
|
|
166
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
167
|
+
self.assertIn("unverified trust", result.stdout)
|
|
168
|
+
journal = (self.root / ".owfa/kernel-journal.log").read_text(encoding="utf-8")
|
|
169
|
+
self.assertNotIn("trust-override", journal)
|
|
170
|
+
result = self.install_path("needr", "--yes")
|
|
171
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
172
|
+
journal = (self.root / ".owfa/kernel-journal.log").read_text(encoding="utf-8")
|
|
173
|
+
self.assertIn("trust-override needr", journal)
|
|
174
|
+
|
|
175
|
+
def test_reviewed_registry_provider_satisfies_needs(self):
|
|
176
|
+
repo = self.git_pkg("prov", "0.1", provides=["owfa.org.v1"])
|
|
177
|
+
self.index([("prov", f"repo: {repo}, version: 0.1, trust: reviewed-3rd-party, members: prov")])
|
|
178
|
+
self.assertEqual(self.run_kernel("install", "prov").returncode, 0)
|
|
179
|
+
self.pkg_dir("needr", caps="needs: [owfa.org.v1]")
|
|
180
|
+
result = self.install_path("needr") # no --yes: the trusted provider satisfies
|
|
181
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
182
|
+
|
|
183
|
+
# ---- prospective set (upgrade sole provider) ----
|
|
184
|
+
|
|
185
|
+
def test_upgrade_of_sole_provider_passes_prospective_preflight(self):
|
|
186
|
+
repo = self.git_pkg("prov", "0.1", provides=["owfa.org.v1"])
|
|
187
|
+
self.index([("prov", f"repo: {repo}, version: 0.1, trust: reviewed-3rd-party, members: prov")])
|
|
188
|
+
self.assertEqual(self.run_kernel("install", "prov").returncode, 0)
|
|
189
|
+
self.pkg_dir("needr", caps="needs: [owfa.org.v1]")
|
|
190
|
+
self.assertEqual(self.install_path("needr").returncode, 0)
|
|
191
|
+
# bump the provider: v0.2 still provides owfa.org.v1
|
|
192
|
+
lines = (repo / "PKG.yaml").read_text(encoding="utf-8").replace("version: 0.1", "version: 0.2")
|
|
193
|
+
(repo / "PKG.yaml").write_text(lines, encoding="utf-8")
|
|
194
|
+
self.git(repo, "add", "-A")
|
|
195
|
+
self.git(repo, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "bump")
|
|
196
|
+
self.git(repo, "tag", "prov-v0.2")
|
|
197
|
+
self.index([("prov", f"repo: {repo}, version: 0.2, trust: reviewed-3rd-party, members: prov")])
|
|
198
|
+
# mid-upgrade the disk briefly lacks the sole provider — the prospective
|
|
199
|
+
# board (installed − old provides + new provides) must let it through
|
|
200
|
+
result = self.run_kernel("upgrade", "prov", "--yes")
|
|
201
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
202
|
+
self.assertIn("0.1 -> v0.2", result.stdout)
|
|
203
|
+
self.assertEqual(self.run_kernel("validate").returncode, 0)
|
|
204
|
+
|
|
205
|
+
# ---- INDEX extension ----
|
|
206
|
+
|
|
207
|
+
def test_index_parses_needs_provides_commit_and_strips_single_quotes(self):
|
|
208
|
+
self.index([
|
|
209
|
+
("prov", "repo: 'https://x/y.git', version: 0.1, trust: 'reviewed-3rd-party', "
|
|
210
|
+
"members: a/b, needs: owfa.org.v1/qq.im.v1, provides: prov.org.v1/owfa.net.v1, commit: 'ABC123'"),
|
|
211
|
+
("bare", 'repo: "https://x/z.git", version: 0.2, trust: framework-bundled, members: z'),
|
|
212
|
+
])
|
|
213
|
+
prefix = "import sys; sys.path.insert(0, '.owfa/scripts'); import owfa_kernel as k; "
|
|
214
|
+
code = ("e,s=k.load_index([]); import json; "
|
|
215
|
+
"print(json.dumps({'trust': e['prov']['trust'], 'commit': e['prov'].get('commit'), "
|
|
216
|
+
"'needs': k._cap_list(e['prov'].get('needs')), "
|
|
217
|
+
"'provides': k._cap_list(e['prov'].get('provides')), "
|
|
218
|
+
"'bare_needs': k._cap_list(e['bare'].get('needs')), "
|
|
219
|
+
"'bare_commit': e['bare'].get('commit')}))")
|
|
220
|
+
r = subprocess.run([sys.executable, "-c", prefix + code], cwd=self.root,
|
|
221
|
+
env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
222
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
223
|
+
data = json.loads(r.stdout)
|
|
224
|
+
self.assertEqual(data["trust"], "reviewed-3rd-party") # single quotes stripped
|
|
225
|
+
self.assertEqual(data["commit"], "ABC123")
|
|
226
|
+
self.assertEqual(data["needs"], ["owfa.org.v1", "qq.im.v1"]) # '/'-separated
|
|
227
|
+
self.assertEqual(data["provides"], ["prov.org.v1", "owfa.net.v1"])
|
|
228
|
+
self.assertEqual(data["bare_needs"], []) # absent = empty set
|
|
229
|
+
self.assertIsNone(data["bare_commit"]) # absent = no pin
|
|
230
|
+
|
|
231
|
+
def test_commit_pin_mismatch_is_refused(self):
|
|
232
|
+
repo = self.git_pkg("prov", "0.1", provides=["owfa.org.v1"])
|
|
233
|
+
self.index([("prov", f"repo: {repo}, version: 0.1, trust: reviewed-3rd-party, members: prov, "
|
|
234
|
+
f"commit: {'0' * 40}")])
|
|
235
|
+
result = self.run_kernel("install", "prov")
|
|
236
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
237
|
+
self.assertIn("commit pin mismatch", result.stdout)
|
|
238
|
+
self.assertFalse((self.root / ".owfa/plugins/domains/probe").exists())
|
|
239
|
+
|
|
240
|
+
def test_commit_pin_match_installs(self):
|
|
241
|
+
repo = self.git_pkg("prov", "0.1", provides=["owfa.org.v1"])
|
|
242
|
+
self.index([("prov", f"repo: {repo}, version: 0.1, trust: reviewed-3rd-party, members: prov, "
|
|
243
|
+
f"commit: {self.git_head(repo)}")])
|
|
244
|
+
result = self.run_kernel("install", "prov")
|
|
245
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
246
|
+
|
|
247
|
+
# ---- suite kind ----
|
|
248
|
+
|
|
249
|
+
def test_rootless_suite_validates_green(self):
|
|
250
|
+
# rootless suite: parallel members, no root plugin — family-root/closure
|
|
251
|
+
# laws must not apply (the owfa-personal shape; id "synth" — the real
|
|
252
|
+
# "personal" suite is already installed by the v0.4 bootstrap setUp)
|
|
253
|
+
suite = self.source / "synthpkg"
|
|
254
|
+
suite.mkdir()
|
|
255
|
+
for name, deps in (("swiki", "[]"), ("slog", "[swiki]")):
|
|
256
|
+
member = suite / name
|
|
257
|
+
member.mkdir()
|
|
258
|
+
dependencies = ("depends: []\n" if deps == "[]" else "depends:\n - swiki\n")
|
|
259
|
+
(member / "PLUGIN.yaml").write_text(
|
|
260
|
+
f"id: {name}\nschema_version: 1\nversion: 0.1\nupdated: 2026-11-12\n"
|
|
261
|
+
f"{dependencies}attachment: []\nfields:\n"
|
|
262
|
+
f'inject: "{name} suite member (synthetic)"\n', encoding="utf-8")
|
|
263
|
+
(member / "PLUGIN.md").write_text("# member\n", encoding="utf-8")
|
|
264
|
+
(suite / "PKG.yaml").write_text(
|
|
265
|
+
"id: synth\nkind: suite\nversion: 0.1.0\ncompat: '>= 0.3'\n"
|
|
266
|
+
"members: [swiki, slog]\ndepends: [framework]\n", encoding="utf-8")
|
|
267
|
+
result = self.run_kernel("install", "synth", "--path", str(suite))
|
|
268
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
269
|
+
self.assertEqual(self.run_kernel("validate").returncode, 0)
|
|
270
|
+
# the same roster WITHOUT kind: suite must fail the family-root laws
|
|
271
|
+
(suite / "PKG.yaml").write_text(
|
|
272
|
+
"id: synth\nversion: 0.1.0\ncompat: '>= 0.3'\n"
|
|
273
|
+
"members: [swiki, slog]\ndepends: [framework]\n", encoding="utf-8")
|
|
274
|
+
self.run_kernel("uninstall", "synth", "--yes")
|
|
275
|
+
result = self.run_kernel("install", "synth", "--path", str(suite))
|
|
276
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
277
|
+
self.assertIn("family root synth must be a member", result.stdout)
|
|
278
|
+
|
|
279
|
+
def test_unknown_kind_is_rejected(self):
|
|
280
|
+
suite = self.source / "weirdpkg"
|
|
281
|
+
suite.mkdir()
|
|
282
|
+
self.manifest("weird", "weird member", parent=suite)
|
|
283
|
+
(suite / "PKG.yaml").write_text(
|
|
284
|
+
"id: weird\nkind: platform\nversion: 0.1\ncompat: '>= 0.3'\n"
|
|
285
|
+
"members: [weird]\ndepends: [framework]\n", encoding="utf-8")
|
|
286
|
+
result = self.run_kernel("install", "weird", "--path", str(suite))
|
|
287
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
288
|
+
self.assertIn("kind (platform) must be 'suite' or 'domain-family'", result.stdout)
|
|
289
|
+
|
|
290
|
+
# ---- provides command (instance view) ----
|
|
291
|
+
|
|
292
|
+
def test_kernel_provides_reports_satisfaction(self):
|
|
293
|
+
self.pkg_dir("prov", caps="provides: [owfa.org.v1, prov.util.v1]")
|
|
294
|
+
self.assertEqual(self.install_path("prov").returncode, 0)
|
|
295
|
+
self.pkg_dir("needr", caps="needs: [owfa.org.v1]")
|
|
296
|
+
self.assertEqual(self.install_path("needr", "--yes").returncode, 0)
|
|
297
|
+
result = self.run_kernel("provides")
|
|
298
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
299
|
+
self.assertIn("pkg prov: provides owfa.org.v1, prov.util.v1", result.stdout)
|
|
300
|
+
# unverified (--path) provides are annotated as such
|
|
301
|
+
self.assertIn("trust: unknown — unverified provides", result.stdout)
|
|
302
|
+
self.assertIn("needs owfa.org.v1 — satisfied by UNVERIFIED provides", result.stdout)
|
|
303
|
+
# uninstall is not a capability convergence point: the need goes unsatisfied
|
|
304
|
+
# (red validate) while the uninstall itself succeeds — the instance view reports it
|
|
305
|
+
self.assertEqual(self.run_kernel("uninstall", "prov", "--yes").returncode, 0)
|
|
306
|
+
result = self.run_kernel("provides")
|
|
307
|
+
self.assertIn("needs owfa.org.v1 — NOT satisfied: capability missing entirely", result.stdout)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
unittest.main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Offline install regressions; synthetic packages and disposable instances only.
|
|
2
2
|
|
|
3
|
-
Run: python -m unittest discover -s .
|
|
3
|
+
Run: python -m unittest discover -s .owfa/tests -p test_install_preflight.py -v
|
|
4
4
|
"""
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
@@ -22,7 +22,7 @@ class InstallPreflightTests(unittest.TestCase):
|
|
|
22
22
|
self.addCleanup(self.temp.cleanup)
|
|
23
23
|
self.root = Path(self.temp.name) / "instance"
|
|
24
24
|
self.root.mkdir()
|
|
25
|
-
for directory in (".
|
|
25
|
+
for directory in (".owfa", ".agents"):
|
|
26
26
|
shutil.copytree(FRAMEWORK / directory, self.root / directory,
|
|
27
27
|
ignore=shutil.ignore_patterns("__pycache__", "tests", "smoke-tmp"))
|
|
28
28
|
(self.root / "AGENTS.md").write_text(
|
|
@@ -32,16 +32,20 @@ class InstallPreflightTests(unittest.TestCase):
|
|
|
32
32
|
self.source.mkdir()
|
|
33
33
|
self.env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", PYTHONIOENCODING="utf-8")
|
|
34
34
|
self.env.pop("PYTHONPATH", None)
|
|
35
|
+
# v0.4 final shape: the framework tree ships no plugins — the synthetic
|
|
36
|
+
# instances bootstrap exactly like a real init (embedded personal suite)
|
|
37
|
+
result = self.run_kernel("install", "personal", "--path", str(FRAMEWORK / "payloads" / "personal"))
|
|
38
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
35
39
|
result = self.run_kernel("all")
|
|
36
40
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
37
41
|
|
|
38
42
|
def run_kernel(self, *args):
|
|
39
43
|
return subprocess.run(
|
|
40
|
-
[sys.executable, str(self.root / ".
|
|
44
|
+
[sys.executable, str(self.root / ".owfa/scripts/owfa_kernel.py"), *args],
|
|
41
45
|
cwd=self.root, env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
42
46
|
|
|
43
47
|
def run_code(self, code):
|
|
44
|
-
prefix = "import sys; sys.path.insert(0, '.
|
|
48
|
+
prefix = "import sys; sys.path.insert(0, '.owfa/scripts'); import owfa_kernel as k; "
|
|
45
49
|
return subprocess.run([sys.executable, "-c", prefix + code], cwd=self.root,
|
|
46
50
|
env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
47
51
|
|
|
@@ -87,7 +91,7 @@ class InstallPreflightTests(unittest.TestCase):
|
|
|
87
91
|
path = self.source / relative
|
|
88
92
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
93
|
path.write_text("Synthetic payload", encoding="utf-8")
|
|
90
|
-
ledger = self.root / ".
|
|
94
|
+
ledger = self.root / ".owfa/migrations/pending.json"
|
|
91
95
|
ledger.parent.mkdir(exist_ok=True)
|
|
92
96
|
ledger.write_text(json.dumps({"probe": {"kind": "teardown", "items": []}}), encoding="utf-8")
|
|
93
97
|
before = self.snapshot()
|
|
@@ -112,7 +116,7 @@ class InstallPreflightTests(unittest.TestCase):
|
|
|
112
116
|
self.manifest("child", "界" * 6000, depends="[optional]")
|
|
113
117
|
result = self.install("--without", "optional")
|
|
114
118
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
115
|
-
installed = self.root / ".
|
|
119
|
+
installed = self.root / ".owfa/plugins/domains/probe"
|
|
116
120
|
self.assertFalse((installed / "optional").exists())
|
|
117
121
|
self.assertFalse((installed / "child").exists())
|
|
118
122
|
self.assertEqual(self.run_kernel("validate").returncode, 0)
|
|
@@ -141,7 +145,7 @@ class InstallPreflightTests(unittest.TestCase):
|
|
|
141
145
|
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
142
146
|
self.assertIn("install incomplete", result.stdout)
|
|
143
147
|
self.assertIn("missing command", result.stdout)
|
|
144
|
-
self.assertTrue((self.root / ".
|
|
148
|
+
self.assertTrue((self.root / ".owfa/plugins/domains/probe/PKG.yaml").exists())
|
|
145
149
|
|
|
146
150
|
def test_projection_failure_returns_nonzero(self):
|
|
147
151
|
self.package()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""owfa_home unit tests: resolution order, bind/unbind, exclusivity, stale, migration.
|
|
4
|
+
|
|
5
|
+
Run: python3 .owfa/tests/test_owfa_home.py (pure stdlib; OWFA_HOME pointed at a temp dir)
|
|
6
|
+
"""
|
|
7
|
+
import contextlib
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"))
|
|
17
|
+
import owfa_home as h # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OwfaHomeTests(unittest.TestCase):
|
|
21
|
+
def setUp(self):
|
|
22
|
+
self._cwd = os.getcwd()
|
|
23
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
24
|
+
self.root = Path(self.tmp.name)
|
|
25
|
+
os.environ["OWFA_HOME"] = str(self.root / "machine")
|
|
26
|
+
self.ws1 = self._ws("ws1")
|
|
27
|
+
self.ws2 = self._ws("ws2")
|
|
28
|
+
os.environ.pop("OWFA_ROOT", None)
|
|
29
|
+
|
|
30
|
+
def tearDown(self):
|
|
31
|
+
# tests chdir into the temp workspace; TemporaryDirectory.cleanup()
|
|
32
|
+
# cannot delete the cwd, so restore first (review P3, PR #57 round 2)
|
|
33
|
+
os.chdir(self._cwd)
|
|
34
|
+
self.tmp.cleanup()
|
|
35
|
+
os.environ.pop("OWFA_HOME", None)
|
|
36
|
+
os.environ.pop("OWFA_ROOT", None)
|
|
37
|
+
|
|
38
|
+
def _ws(self, name):
|
|
39
|
+
p = self.root / name
|
|
40
|
+
p.mkdir(parents=True)
|
|
41
|
+
return str(p)
|
|
42
|
+
|
|
43
|
+
def test_unmatched_cwd_on_multi_binding_machine_is_loud(self):
|
|
44
|
+
# review round 3: falling back to homes/default silently on a
|
|
45
|
+
# multi-binding machine invents an unregistered home two unmatched
|
|
46
|
+
# workspaces could share — still served (degraded-but-healthy), but loud
|
|
47
|
+
h.bind("qq", self.ws1, "alpha")
|
|
48
|
+
h.bind("qq", self.ws2, "beta")
|
|
49
|
+
os.chdir(self._ws("ws3"))
|
|
50
|
+
err = io.StringIO()
|
|
51
|
+
with contextlib.redirect_stderr(err):
|
|
52
|
+
home = h.resolve_home("qq")
|
|
53
|
+
self.assertEqual(home, h.home_dir("qq", "default"))
|
|
54
|
+
self.assertIn("matches none", err.getvalue())
|
|
55
|
+
|
|
56
|
+
@unittest.skipIf(sys.platform == "win32", "mkdir mode is a POSIX no-op on Windows")
|
|
57
|
+
def test_bind_home_dir_is_0700(self):
|
|
58
|
+
# review round 3: docs/home.md promises 0600/0700 for connector homes
|
|
59
|
+
entry = h.bind("qq", self.ws1, "alpha")
|
|
60
|
+
self.assertEqual(stat.S_IMODE(os.stat(entry["home"]).st_mode), 0o700)
|
|
61
|
+
|
|
62
|
+
def test_default_home_is_zero_config(self):
|
|
63
|
+
os.chdir(self.ws1)
|
|
64
|
+
home = h.resolve_home("qq-cli")
|
|
65
|
+
self.assertEqual(home, self.root / "machine" / "connectors" / "qq-cli" / "homes" / "default")
|
|
66
|
+
|
|
67
|
+
def test_explicit_root_binding_wins(self):
|
|
68
|
+
os.chdir(self.ws1)
|
|
69
|
+
h.bind("qq-cli", self.ws1, "personal", bot="appid-1")
|
|
70
|
+
self.assertEqual(h.resolve_home("qq-cli"), Path(h.home_dir("qq-cli", "personal")))
|
|
71
|
+
entry = h.load_bindings("qq-cli")[0]
|
|
72
|
+
self.assertEqual(entry["bot"], "appid-1")
|
|
73
|
+
self.assertTrue(os.path.isdir(entry["home"]), "bind must create the home dir")
|
|
74
|
+
|
|
75
|
+
def test_resolution_order_env_then_cwd(self):
|
|
76
|
+
h.bind("qq-cli", self.ws1, "a")
|
|
77
|
+
h.bind("qq-cli", self.ws2, "b")
|
|
78
|
+
os.environ["OWFA_ROOT"] = self.ws2
|
|
79
|
+
self.assertEqual(h.resolve_home("qq-cli"), Path(h.home_dir("qq-cli", "b")))
|
|
80
|
+
os.environ.pop("OWFA_ROOT", None)
|
|
81
|
+
os.chdir(self.ws1)
|
|
82
|
+
self.assertEqual(h.resolve_home("qq-cli"), Path(h.home_dir("qq-cli", "a"))) # cwd lookup
|
|
83
|
+
self.assertEqual(h.resolve_home("qq-cli", explicit_root=self.ws2), Path(h.home_dir("qq-cli", "b"))) # explicit wins
|
|
84
|
+
|
|
85
|
+
def test_rebind_same_root_replaces(self):
|
|
86
|
+
h.bind("qq-cli", self.ws1, "first")
|
|
87
|
+
h.bind("qq-cli", self.ws1) # no id: re-binds under the existing id for this root
|
|
88
|
+
bindings = h.load_bindings("qq-cli")
|
|
89
|
+
self.assertEqual(len(bindings), 1)
|
|
90
|
+
self.assertEqual(bindings[0]["id"], "first")
|
|
91
|
+
|
|
92
|
+
def test_unbind_detaches_but_keeps_home(self):
|
|
93
|
+
b = h.bind("qq-cli", self.ws1, "keep-data")
|
|
94
|
+
h.unbind("qq-cli", "keep-data")
|
|
95
|
+
self.assertEqual(h.load_bindings("qq-cli"), [])
|
|
96
|
+
self.assertTrue(os.path.isdir(b["home"]), "unbind must not delete home data")
|
|
97
|
+
|
|
98
|
+
def test_exclusivity_violation_detected(self):
|
|
99
|
+
home = h.home_dir("qq-cli", "shared")
|
|
100
|
+
for ws, bid in ((self.ws1, "one"), (self.ws2, "two")):
|
|
101
|
+
b = h.bind("qq-cli", ws, bid)
|
|
102
|
+
b["home"] = str(home) # force two bindings onto one home
|
|
103
|
+
h.save_bindings("qq-cli", [x for x in h.load_bindings("qq-cli") if x["id"] != bid] + [b])
|
|
104
|
+
v = h.exclusivity_violations("qq-cli")
|
|
105
|
+
self.assertEqual(len(v), 1)
|
|
106
|
+
self.assertEqual(sorted(v[0]["bindings"]), ["one", "two"])
|
|
107
|
+
|
|
108
|
+
def test_stale_bindings_reported(self):
|
|
109
|
+
gone = self._ws("gone")
|
|
110
|
+
h.bind("qq-cli", gone, "dying")
|
|
111
|
+
os.rmdir(gone)
|
|
112
|
+
stale = h.stale_bindings("qq-cli")
|
|
113
|
+
self.assertEqual([s["id"] for s in stale], ["dying"])
|
|
114
|
+
|
|
115
|
+
def test_migrate_moves_and_symlinks(self):
|
|
116
|
+
legacy = Path(self._ws("legacy-home"))
|
|
117
|
+
(legacy / "credentials.json").write_text("{}", encoding="utf-8")
|
|
118
|
+
os.chdir(self.ws1)
|
|
119
|
+
entry = h.migrate_legacy_home("qq-cli", str(legacy), "default", root=self.ws1)
|
|
120
|
+
self.assertFalse(legacy.exists() and not legacy.is_symlink(), "legacy dir must move, not copy")
|
|
121
|
+
self.assertTrue((Path(entry["home"]) / "credentials.json").exists(), "contents must land in the new home")
|
|
122
|
+
self.assertTrue(legacy.is_symlink() and os.path.realpath(legacy) == os.path.realpath(entry["home"]))
|
|
123
|
+
self.assertEqual(os.path.basename(os.path.realpath(legacy)), "default")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Version-scheme unit tests (versioning scheme, 0.4): three-part anchor parse,
|
|
4
|
+
B-line extraction, and the migration staging range [prev_line, new_line).
|
|
5
|
+
|
|
6
|
+
Run: python3 -m unittest discover -s .owfa/tests -p test_version_scheme.py
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"))
|
|
14
|
+
import owfa_kernel as k # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VersionSchemeTests(unittest.TestCase):
|
|
18
|
+
def test_framework_version_parses_three_part_anchor(self):
|
|
19
|
+
# the repo's own VERSION is the full release version now
|
|
20
|
+
self.assertEqual(k.framework_version(), (0, 4, 0))
|
|
21
|
+
|
|
22
|
+
def test_framework_version_tolerates_legacy_two_part(self):
|
|
23
|
+
import tempfile
|
|
24
|
+
vpath = os.path.join(k.ROOT, ".owfa", "VERSION")
|
|
25
|
+
original = open(vpath, encoding="utf-8").read()
|
|
26
|
+
try:
|
|
27
|
+
open(vpath, "w", encoding="utf-8").write("0.3\n")
|
|
28
|
+
self.assertEqual(k.framework_version(), (0, 3))
|
|
29
|
+
finally:
|
|
30
|
+
open(vpath, "w", encoding="utf-8").write(original)
|
|
31
|
+
|
|
32
|
+
def test_vline_extracts_major_minor(self):
|
|
33
|
+
self.assertEqual(k._vline("0.3.0"), (0, 3))
|
|
34
|
+
self.assertEqual(k._vline("0.3.11"), (0, 3))
|
|
35
|
+
self.assertEqual(k._vline("0.2"), (0, 2))
|
|
36
|
+
|
|
37
|
+
def test_compat_floor_tuple_semantics(self):
|
|
38
|
+
fw = k.framework_version() # (0, 4, 0)
|
|
39
|
+
self.assertFalse((0, 3) > fw) # floor below the line passes
|
|
40
|
+
self.assertFalse((0, 4) > fw) # floor at the line passes
|
|
41
|
+
self.assertFalse((0, 4, 0) > fw) # floor equal to the release passes
|
|
42
|
+
self.assertTrue((0, 4, 1) > fw) # floor demanding a later patch refuses
|
|
43
|
+
self.assertTrue((0, 5) > fw) # floor above the line refuses
|
|
44
|
+
|
|
45
|
+
def test_staging_line_range(self):
|
|
46
|
+
# mirror of the _stage_migrations predicate: stage when prev_line <= from_line < new_line
|
|
47
|
+
def stages(name, prev, new):
|
|
48
|
+
m = re.fullmatch(r"(\d+\.\d+)-to-(\d+\.\d+)\.md", name)
|
|
49
|
+
if not m:
|
|
50
|
+
return False
|
|
51
|
+
prev_line, new_line = k._vline(prev), k._vline(new)
|
|
52
|
+
return prev_line <= k._vt(m.group(1)) < new_line
|
|
53
|
+
|
|
54
|
+
self.assertTrue(stages("0.3-to-0.4.md", "0.3.2", "0.4.0")) # line crossing stages
|
|
55
|
+
self.assertTrue(stages("0.3-to-0.4.md", "0.3", "0.4.0")) # legacy two-part prev works
|
|
56
|
+
self.assertTrue(stages("0.2-to-0.3.md", "0.2", "0.4.0")) # chained crossings stage together
|
|
57
|
+
self.assertFalse(stages("0.3-to-0.4.md", "0.3.1", "0.3.2")) # same-line C update stages nothing
|
|
58
|
+
self.assertFalse(stages("0.2-to-0.3.md", "0.3", "0.4.0")) # already past does not re-stage
|
|
59
|
+
self.assertFalse(stages("0.4-to-0.5.md", "0.3", "0.4.0")) # future lines do not stage
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
unittest.main()
|