@rockysoft/opencode-hybrid-workflow 0.0.0-stage → 0.3.2-beta.1
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/LICENSE +21 -0
- package/README.md +252 -3
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/assets/agents/backend-engineer.md +167 -0
- package/assets/agents/code-reviewer.md +129 -0
- package/assets/agents/database-engineer.md +162 -0
- package/assets/agents/devops-engineer.md +115 -0
- package/assets/agents/frontend-engineer.md +38 -0
- package/assets/agents/fullstack-architect.md +43 -0
- package/assets/agents/security-auditor.md +132 -0
- package/assets/agents/technical-writer.md +78 -0
- package/assets/agents/test-engineer.md +154 -0
- package/assets/rules.md +93 -0
- package/assets/skills/frontend-design/INSTALLATION.json +9 -0
- package/assets/skills/frontend-design/LICENSE.txt +177 -0
- package/assets/skills/frontend-design/SKILL.md +78 -0
- package/assets/skills/ui-ux-pro-max/INSTALLATION.json +10 -0
- package/assets/skills/ui-ux-pro-max/LICENSE +21 -0
- package/assets/skills/ui-ux-pro-max/SKILL.md +221 -0
- package/assets/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
- package/assets/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
- package/assets/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/assets/skills/ui-ux-pro-max/data/colors.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
- package/assets/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
- package/assets/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
- package/assets/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/assets/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/assets/skills/ui-ux-pro-max/data/motion.csv +18 -0
- package/assets/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
- package/assets/skills/ui-ux-pro-max/data/products.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +77 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
- package/assets/skills/ui-ux-pro-max/data/styles.csv +89 -0
- package/assets/skills/ui-ux-pro-max/data/typography.csv +75 -0
- package/assets/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
- package/assets/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
- package/assets/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
- package/assets/skills/ui-ux-pro-max/scripts/core.py +993 -0
- package/assets/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
- package/assets/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
- package/assets/skills/ui-ux-pro-max/scripts/search.py +177 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +177 -0
- package/assets/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
- package/assets/skills/web-design-guidelines/INSTALLATION.json +1 -0
- package/assets/skills/web-design-guidelines/SKILL.md +18 -0
- package/cli.mjs +91 -0
- package/core/controller.mjs +367 -0
- package/core/dashboard-state.js +48 -0
- package/core/dashboard.css +2 -0
- package/core/dashboard.html +12 -0
- package/core/dashboard.js +72 -0
- package/core/decision.mjs +22 -0
- package/core/design.mjs +139 -0
- package/core/evidence.mjs +82 -0
- package/core/frontend-policy.mjs +34 -0
- package/core/images.mjs +35 -0
- package/core/monitor-data.mjs +58 -0
- package/core/monitor.mjs +24 -0
- package/core/operation-lock.mjs +82 -0
- package/core/plugin.mjs +91 -0
- package/core/posix-lock.mjs +31 -0
- package/core/posix-lock.py +23 -0
- package/core/pricing.mjs +53 -0
- package/core/process-identity.mjs +19 -0
- package/core/research-tools.mjs +37 -0
- package/core/review-context.mjs +43 -0
- package/core/snapshot.mjs +95 -0
- package/core/telemetry.mjs +107 -0
- package/core/workers.mjs +192 -0
- package/core/workflow-policy.mjs +10 -0
- package/index.js +19 -0
- package/lib/commands.mjs +48 -0
- package/lib/config.mjs +63 -0
- package/lib/install.mjs +111 -0
- package/lib/native-browser.mjs +79 -0
- package/lib/opencode-reviewer.mjs +129 -0
- package/lib/platform.mjs +14 -0
- package/lib/reviewer-browser-proxy.mjs +61 -0
- package/lib/reviewer-browser.mjs +35 -0
- package/lib/reviewers.mjs +11 -0
- package/lib/select-reviewers.mjs +58 -0
- package/lib/settings-rpc.mjs +9 -0
- package/lib/settings-service.mjs +129 -0
- package/package.json +63 -6
- package/sidebar/reader.mjs +80 -0
- package/sidebar/settings.mjs +99 -0
- package/tui.js +303 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offline contract tests for deterministic upstream catalog refreshes."""
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
REPO = next(
|
|
14
|
+
parent for parent in Path(__file__).resolve().parents
|
|
15
|
+
if all((parent / "scripts" / script).is_file() for script in (
|
|
16
|
+
"refresh-google-fonts.py", "refresh-icon-catalog.py",
|
|
17
|
+
))
|
|
18
|
+
)
|
|
19
|
+
FIXTURES = Path(__file__).parent / "fixtures" / "catalogs"
|
|
20
|
+
FONT_SCRIPT = REPO / "scripts" / "refresh-google-fonts.py"
|
|
21
|
+
ICON_SCRIPT = REPO / "scripts" / "refresh-icon-catalog.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CatalogRefreshTest(unittest.TestCase):
|
|
25
|
+
def run_command(self, *args, env=None):
|
|
26
|
+
return subprocess.run(
|
|
27
|
+
[sys.executable, *map(str, args)],
|
|
28
|
+
cwd=REPO,
|
|
29
|
+
env=env,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
check=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def font_args(self, directory, api=None, metadata=None, approve=True, existing=None, overrides=None):
|
|
36
|
+
args = [
|
|
37
|
+
FONT_SCRIPT,
|
|
38
|
+
"--api-input", api or FIXTURES / "google-api.json",
|
|
39
|
+
"--metadata-input", metadata or FIXTURES / "google-metadata.json",
|
|
40
|
+
"--existing-csv", existing or FIXTURES / "google-existing.csv",
|
|
41
|
+
"--overrides", overrides or FIXTURES / "google-overrides.json",
|
|
42
|
+
"--output-csv", directory / "google-fonts.csv",
|
|
43
|
+
"--license-output", directory / "google-font-licenses.json",
|
|
44
|
+
"--verified-at", "2026-08-13",
|
|
45
|
+
"--metadata-revision", "fixture-catalogs-v1",
|
|
46
|
+
"--expected-count", "2",
|
|
47
|
+
]
|
|
48
|
+
if approve:
|
|
49
|
+
args.append("--approve-changes")
|
|
50
|
+
return args
|
|
51
|
+
|
|
52
|
+
def icon_args(self, directory, source=None, curated=None, package=None, react_exports=None):
|
|
53
|
+
return [
|
|
54
|
+
ICON_SCRIPT,
|
|
55
|
+
"--input", source or FIXTURES / "phosphor-core.json",
|
|
56
|
+
"--package-json", package or FIXTURES / "phosphor-package.json",
|
|
57
|
+
"--react-package-json", FIXTURES / "phosphor-react-package.json",
|
|
58
|
+
"--react-exports-input", react_exports or FIXTURES / "phosphor-react-exports.json",
|
|
59
|
+
"--curated-csv", curated or FIXTURES / "icons-curated.csv",
|
|
60
|
+
"--output", directory / "phosphor-icons-upstream.json",
|
|
61
|
+
"--verified-at", "2026-08-13",
|
|
62
|
+
"--expected-count", "2",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def test_live_font_refresh_requires_environment_key(self):
|
|
66
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
67
|
+
directory = Path(raw)
|
|
68
|
+
args = self.font_args(directory)
|
|
69
|
+
args[1:3] = ["--live"]
|
|
70
|
+
env = dict(os.environ)
|
|
71
|
+
env.pop("GOOGLE_FONTS_API_KEY", None)
|
|
72
|
+
result = self.run_command(*args, env=env)
|
|
73
|
+
self.assertEqual(2, result.returncode)
|
|
74
|
+
self.assertIn("GOOGLE_FONTS_API_KEY is required for --live", result.stderr)
|
|
75
|
+
self.assertIn("use --api-input for offline CI", result.stderr)
|
|
76
|
+
|
|
77
|
+
def test_font_refresh_is_deterministic_and_preserves_reviewed_fields(self):
|
|
78
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
79
|
+
first_path, second_path = Path(first), Path(second)
|
|
80
|
+
self.assertEqual(0, self.run_command(*self.font_args(first_path)).returncode)
|
|
81
|
+
self.assertEqual(0, self.run_command(*self.font_args(second_path)).returncode)
|
|
82
|
+
self.assertEqual(
|
|
83
|
+
(first_path / "google-fonts.csv").read_bytes(),
|
|
84
|
+
(second_path / "google-fonts.csv").read_bytes(),
|
|
85
|
+
)
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
(first_path / "google-font-licenses.json").read_bytes(),
|
|
88
|
+
(second_path / "google-font-licenses.json").read_bytes(),
|
|
89
|
+
)
|
|
90
|
+
with (first_path / "google-fonts.csv").open(encoding="utf-8", newline="") as handle:
|
|
91
|
+
rows = list(csv.DictReader(handle))
|
|
92
|
+
csv_bytes = (first_path / "google-fonts.csv").read_bytes()
|
|
93
|
+
license_bytes = (first_path / "google-font-licenses.json").read_bytes()
|
|
94
|
+
licenses = json.loads(license_bytes)
|
|
95
|
+
self.assertEqual(["Alpha Sans", "Zeta Serif"], [row["Family"] for row in rows])
|
|
96
|
+
self.assertEqual("Sans Serif", rows[0]["Stroke"])
|
|
97
|
+
self.assertEqual("approved override keywords", rows[0]["Keywords"])
|
|
98
|
+
self.assertEqual("400 | 400i | 500", rows[0]["Styles"])
|
|
99
|
+
self.assertEqual("wght: 100..900", rows[0]["Variable Axes"])
|
|
100
|
+
self.assertEqual(["OFL", "APACHE2"], [item["license"] for item in licenses["families"]])
|
|
101
|
+
self.assertEqual(["Alpha Sans", "Zeta Serif"], [item["name"] for item in licenses["families"]])
|
|
102
|
+
self.assertEqual("fixture-catalogs-v1", licenses["source"]["revision"])
|
|
103
|
+
self.assertTrue(all(item["status"] == "active" for item in licenses["families"]))
|
|
104
|
+
self.assertTrue(all(item["verifiedAt"] == "2026-08-13" for item in licenses["families"]))
|
|
105
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
106
|
+
reused = Path(raw)
|
|
107
|
+
metadata_path = reused / "metadata.json"
|
|
108
|
+
metadata_path.write_bytes(license_bytes)
|
|
109
|
+
result = self.run_command(*self.font_args(
|
|
110
|
+
reused, metadata=metadata_path
|
|
111
|
+
))
|
|
112
|
+
self.assertEqual(0, result.returncode, result.stderr)
|
|
113
|
+
self.assertEqual(
|
|
114
|
+
csv_bytes, (reused / "google-fonts.csv").read_bytes(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_font_refresh_rejects_schema_size_dates_and_licenses(self):
|
|
118
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
119
|
+
directory = Path(raw)
|
|
120
|
+
api = json.loads((FIXTURES / "google-api.json").read_text())
|
|
121
|
+
metadata = json.loads((FIXTURES / "google-metadata.json").read_text())
|
|
122
|
+
cases = []
|
|
123
|
+
wrong_schema = dict(api)
|
|
124
|
+
wrong_schema["kind"] = "unexpected"
|
|
125
|
+
cases.append((wrong_schema, metadata, "webfonts#webfontList"))
|
|
126
|
+
cases.append(({**api, "items": api["items"][:1]}, metadata, "expected 2 items"))
|
|
127
|
+
bad_url = json.loads(json.dumps(api))
|
|
128
|
+
bad_url["items"][0]["files"]["regular"] = "https://example.com/font.ttf"
|
|
129
|
+
cases.append((bad_url, metadata, "https://fonts.gstatic.com"))
|
|
130
|
+
bad_date = json.loads(json.dumps(api))
|
|
131
|
+
bad_date["items"][0]["lastModified"] = "1970-01-01"
|
|
132
|
+
cases.append((bad_date, metadata, "suspicious date"))
|
|
133
|
+
bad_license = json.loads(json.dumps(metadata))
|
|
134
|
+
bad_license["families"][0]["license"] = "UNKNOWN"
|
|
135
|
+
cases.append((api, bad_license, "invalid or missing official license"))
|
|
136
|
+
for index, (api_value, metadata_value, error) in enumerate(cases):
|
|
137
|
+
api_path, metadata_path = directory / f"api-{index}.json", directory / f"metadata-{index}.json"
|
|
138
|
+
api_path.write_text(json.dumps(api_value))
|
|
139
|
+
metadata_path.write_text(json.dumps(metadata_value))
|
|
140
|
+
result = self.run_command(*self.font_args(directory, api_path, metadata_path))
|
|
141
|
+
with self.subTest(error=error):
|
|
142
|
+
self.assertEqual(2, result.returncode)
|
|
143
|
+
self.assertIn(error, result.stderr)
|
|
144
|
+
|
|
145
|
+
def test_font_refresh_fails_closed_on_concurrency_or_interrupted_pair(self):
|
|
146
|
+
for sentinel, error in (
|
|
147
|
+
(".google-font-refresh.lock", "another refresh is already running"),
|
|
148
|
+
(".google-font-refresh.incomplete.json", "incomplete prior refresh"),
|
|
149
|
+
):
|
|
150
|
+
with self.subTest(sentinel=sentinel), tempfile.TemporaryDirectory() as raw:
|
|
151
|
+
directory = Path(raw)
|
|
152
|
+
(directory / sentinel).write_text("occupied\n", encoding="utf-8")
|
|
153
|
+
result = self.run_command(*self.font_args(directory))
|
|
154
|
+
self.assertEqual(2, result.returncode)
|
|
155
|
+
self.assertIn(error, result.stderr)
|
|
156
|
+
self.assertFalse((directory / "google-fonts.csv").exists())
|
|
157
|
+
self.assertFalse((directory / "google-font-licenses.json").exists())
|
|
158
|
+
|
|
159
|
+
def test_catalog_cross_check_uses_explicit_schema_without_font_file_urls(self):
|
|
160
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
161
|
+
directory = Path(raw)
|
|
162
|
+
args = self.font_args(directory)
|
|
163
|
+
args[1:3] = ["--catalog-input", FIXTURES / "google-catalog.json"]
|
|
164
|
+
result = self.run_command(*args)
|
|
165
|
+
with (directory / "google-fonts.csv").open(encoding="utf-8", newline="") as handle:
|
|
166
|
+
rows = list(csv.DictReader(handle))
|
|
167
|
+
self.assertEqual(0, result.returncode, result.stderr)
|
|
168
|
+
self.assertEqual(["Alpha Sans", "Zeta Serif"], [row["Family"] for row in rows])
|
|
169
|
+
self.assertEqual("Geometric", rows[0]["Classifications"])
|
|
170
|
+
self.assertEqual("42", rows[0]["Popularity Rank"])
|
|
171
|
+
self.assertEqual("latin | vietnamese", rows[0]["Subsets"])
|
|
172
|
+
|
|
173
|
+
def test_official_metadata_checkout_is_strict_and_reusable(self):
|
|
174
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
175
|
+
directory = Path(raw)
|
|
176
|
+
metadata_root = directory / "google-fonts"
|
|
177
|
+
family_dir = metadata_root / "ofl" / "alphasans"
|
|
178
|
+
family_dir.mkdir(parents=True)
|
|
179
|
+
(family_dir / "METADATA.pb").write_text(
|
|
180
|
+
'name: "Alpha Sans"\n'
|
|
181
|
+
'designer: "Alpha Designer"\n'
|
|
182
|
+
'license: "OFL"\n'
|
|
183
|
+
'date_added: "2024-01-02"\n',
|
|
184
|
+
encoding="utf-8",
|
|
185
|
+
)
|
|
186
|
+
args = self.font_args(directory)
|
|
187
|
+
metadata_index = args.index("--metadata-input")
|
|
188
|
+
args[metadata_index:metadata_index + 2] = ["--metadata-root", metadata_root]
|
|
189
|
+
result = self.run_command(*args)
|
|
190
|
+
self.assertEqual(2, result.returncode)
|
|
191
|
+
self.assertIn("fewer than 90%", result.stderr)
|
|
192
|
+
|
|
193
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
194
|
+
directory = Path(raw)
|
|
195
|
+
metadata_root = directory / "google-fonts"
|
|
196
|
+
for slug, family, license_name in (
|
|
197
|
+
("alphasans", "Alpha Sans", "OFL"),
|
|
198
|
+
("zetaserif", "Zeta Serif", "APACHE2"),
|
|
199
|
+
):
|
|
200
|
+
family_dir = metadata_root / "ofl" / slug
|
|
201
|
+
family_dir.mkdir(parents=True)
|
|
202
|
+
(family_dir / "METADATA.pb").write_text(
|
|
203
|
+
f'name: "{family}"\n'
|
|
204
|
+
f'designer: "{family} Designer"\n'
|
|
205
|
+
f'license: "{license_name}"\n'
|
|
206
|
+
'date_added: "2024-01-02"\n',
|
|
207
|
+
encoding="utf-8",
|
|
208
|
+
)
|
|
209
|
+
args = self.font_args(directory)
|
|
210
|
+
metadata_index = args.index("--metadata-input")
|
|
211
|
+
args[metadata_index:metadata_index + 2] = ["--metadata-root", metadata_root]
|
|
212
|
+
result = self.run_command(*args)
|
|
213
|
+
licenses = json.loads((directory / "google-font-licenses.json").read_text())
|
|
214
|
+
self.assertEqual(0, result.returncode, result.stderr)
|
|
215
|
+
self.assertEqual(["Alpha Sans", "Zeta Serif"], [item["name"] for item in licenses["families"]])
|
|
216
|
+
|
|
217
|
+
def test_catalog_rejects_bool_rank_duplicate_axis_and_unreviewed_addition(self):
|
|
218
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
219
|
+
directory = Path(raw)
|
|
220
|
+
catalog = json.loads((FIXTURES / "google-catalog.json").read_text())
|
|
221
|
+
invalid_rank = json.loads(json.dumps(catalog))
|
|
222
|
+
invalid_rank["familyMetadataList"][0]["popularity"] = True
|
|
223
|
+
rank_path = directory / "rank.json"
|
|
224
|
+
rank_path.write_text(json.dumps(invalid_rank))
|
|
225
|
+
rank_args = self.font_args(directory)
|
|
226
|
+
rank_args[1:3] = ["--catalog-input", rank_path]
|
|
227
|
+
rank_result = self.run_command(*rank_args)
|
|
228
|
+
invalid_axis = json.loads(json.dumps(catalog))
|
|
229
|
+
axis = invalid_axis["familyMetadataList"][1]["axes"][0]
|
|
230
|
+
invalid_axis["familyMetadataList"][1]["axes"].append(dict(axis))
|
|
231
|
+
axis_path = directory / "axis.json"
|
|
232
|
+
axis_path.write_text(json.dumps(invalid_axis))
|
|
233
|
+
axis_args = self.font_args(directory)
|
|
234
|
+
axis_args[1:3] = ["--catalog-input", axis_path]
|
|
235
|
+
axis_result = self.run_command(*axis_args)
|
|
236
|
+
existing = directory / "existing.csv"
|
|
237
|
+
lines = (FIXTURES / "google-existing.csv").read_text().splitlines()
|
|
238
|
+
existing.write_text("\n".join(lines[:2]) + "\n")
|
|
239
|
+
approval_result = self.run_command(*self.font_args(directory, approve=False, existing=existing))
|
|
240
|
+
bad_overrides = directory / "overrides.json"
|
|
241
|
+
bad_overrides.write_text('{"families":{"Unknown Font":{"Keywords":"bad"}}}')
|
|
242
|
+
override_result = self.run_command(*self.font_args(directory, overrides=bad_overrides))
|
|
243
|
+
self.assertIn("invalid popularity", rank_result.stderr)
|
|
244
|
+
self.assertIn("duplicate axis tags", axis_result.stderr)
|
|
245
|
+
self.assertIn("family-set changes require --approve-changes", approval_result.stderr)
|
|
246
|
+
self.assertIn("Zeta Serif", approval_result.stdout)
|
|
247
|
+
self.assertFalse((directory / "google-fonts.csv").exists())
|
|
248
|
+
self.assertIn("overrides target unknown families", override_result.stderr)
|
|
249
|
+
|
|
250
|
+
def test_explicit_license_exclusion_is_reported_and_not_promoted(self):
|
|
251
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
252
|
+
directory = Path(raw)
|
|
253
|
+
metadata = json.loads((FIXTURES / "google-metadata.json").read_text())
|
|
254
|
+
metadata["families"] = metadata["families"][:1]
|
|
255
|
+
metadata["excludedFamilies"] = [{
|
|
256
|
+
"name": "Alpha Sans", "reason": "No matching official METADATA.pb",
|
|
257
|
+
"source": "https://github.com/google/fonts",
|
|
258
|
+
}]
|
|
259
|
+
metadata_path = directory / "metadata.json"
|
|
260
|
+
metadata_path.write_text(json.dumps(metadata))
|
|
261
|
+
result = self.run_command(*self.font_args(directory, metadata=metadata_path))
|
|
262
|
+
report = json.loads(result.stdout)
|
|
263
|
+
licenses = json.loads((directory / "google-font-licenses.json").read_text())
|
|
264
|
+
with (directory / "google-fonts.csv").open(encoding="utf-8", newline="") as handle:
|
|
265
|
+
rows = list(csv.DictReader(handle))
|
|
266
|
+
self.assertEqual(0, result.returncode, result.stderr)
|
|
267
|
+
self.assertEqual(["Zeta Serif"], [row["Family"] for row in rows])
|
|
268
|
+
self.assertEqual("needs-review", report["excludedFamilies"][0]["status"])
|
|
269
|
+
self.assertEqual("needs-review", licenses["excludedFamilies"][0]["status"])
|
|
270
|
+
|
|
271
|
+
def test_exclusion_sources_match_offline_validator_policy(self):
|
|
272
|
+
metadata = json.loads((FIXTURES / "google-metadata.json").read_text())
|
|
273
|
+
allowed = (
|
|
274
|
+
"https://fonts.google.com/specimen/Alpha+Sans",
|
|
275
|
+
"https://github.com/google/fonts",
|
|
276
|
+
"https://github.com/google/fonts/tree/main/ofl/alphasans",
|
|
277
|
+
)
|
|
278
|
+
rejected = (
|
|
279
|
+
"https://example.com/google/fonts",
|
|
280
|
+
"https://github.com/other/fonts",
|
|
281
|
+
"https://fonts.google.com:444/specimen/Alpha+Sans",
|
|
282
|
+
)
|
|
283
|
+
for index, source in enumerate((*allowed, *rejected)):
|
|
284
|
+
with self.subTest(source=source), tempfile.TemporaryDirectory() as raw:
|
|
285
|
+
directory = Path(raw)
|
|
286
|
+
candidate = json.loads(json.dumps(metadata))
|
|
287
|
+
candidate["families"] = candidate["families"][1:]
|
|
288
|
+
candidate["excludedFamilies"] = [{
|
|
289
|
+
"name": "Zeta Serif",
|
|
290
|
+
"reason": "No matching official METADATA.pb",
|
|
291
|
+
"source": source,
|
|
292
|
+
}]
|
|
293
|
+
metadata_path = directory / f"metadata-{index}.json"
|
|
294
|
+
metadata_path.write_text(json.dumps(candidate))
|
|
295
|
+
result = self.run_command(*self.font_args(directory, metadata=metadata_path))
|
|
296
|
+
self.assertEqual(source in allowed, result.returncode == 0, result.stderr)
|
|
297
|
+
|
|
298
|
+
def test_icon_manifest_normalizes_and_records_all_import_forms(self):
|
|
299
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
300
|
+
first_path, second_path = Path(first), Path(second)
|
|
301
|
+
self.assertEqual(0, self.run_command(*self.icon_args(first_path)).returncode)
|
|
302
|
+
self.assertEqual(0, self.run_command(*self.icon_args(second_path)).returncode)
|
|
303
|
+
output = first_path / "phosphor-icons-upstream.json"
|
|
304
|
+
self.assertEqual(output.read_bytes(), (second_path / output.name).read_bytes())
|
|
305
|
+
manifest = json.loads(output.read_text())
|
|
306
|
+
self.assertEqual("2.1.1", manifest["source"]["version"])
|
|
307
|
+
self.assertEqual(("active", "2026-08-13"), (manifest["status"], manifest["verifiedAt"]))
|
|
308
|
+
self.assertEqual(["thin", "light", "regular", "bold", "fill", "duotone"], manifest["weights"])
|
|
309
|
+
self.assertEqual(["acorn", "arrow-left"], [icon["name"] for icon in manifest["icons"]])
|
|
310
|
+
arrow = manifest["icons"][1]
|
|
311
|
+
self.assertEqual(["arrows", "navigation"], arrow["categories"])
|
|
312
|
+
self.assertIn('from "@phosphor-icons/react"', arrow["clientImport"])
|
|
313
|
+
self.assertIn('from "@phosphor-icons/react/ssr"', arrow["ssrImport"])
|
|
314
|
+
self.assertEqual(2, manifest["curatedValidatedCount"])
|
|
315
|
+
|
|
316
|
+
def test_icon_refresh_rejects_invalid_schema_size_and_curated_import(self):
|
|
317
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
318
|
+
directory = Path(raw)
|
|
319
|
+
icons = json.loads((FIXTURES / "phosphor-core.json").read_text())
|
|
320
|
+
invalid = json.loads(json.dumps(icons))
|
|
321
|
+
del invalid[0]["pascal_name"]
|
|
322
|
+
source = directory / "invalid.json"
|
|
323
|
+
source.write_text(json.dumps(invalid))
|
|
324
|
+
schema_result = self.run_command(*self.icon_args(directory, source))
|
|
325
|
+
size_args = self.icon_args(directory)
|
|
326
|
+
size_args[-1] = "3"
|
|
327
|
+
size_result = self.run_command(*size_args)
|
|
328
|
+
curated = (FIXTURES / "icons-curated.csv").read_text().replace("{ Acorn }", "{ Horse }")
|
|
329
|
+
curated_path = directory / "icons.csv"
|
|
330
|
+
curated_path.write_text(curated)
|
|
331
|
+
import_result = self.run_command(*self.icon_args(directory, curated=curated_path))
|
|
332
|
+
package_path = directory / "package.json"
|
|
333
|
+
package_path.write_text('{"name":"@phosphor-icons/core","version":"2.2.0"}')
|
|
334
|
+
version_result = self.run_command(*self.icon_args(directory, package=package_path))
|
|
335
|
+
alias_collision = json.loads(json.dumps(icons))
|
|
336
|
+
alias_collision[1]["alias"] = {"name": "acorn", "pascal_name": "BackArrow"}
|
|
337
|
+
alias_path = directory / "alias.json"
|
|
338
|
+
alias_path.write_text(json.dumps(alias_collision))
|
|
339
|
+
alias_result = self.run_command(*self.icon_args(directory, source=alias_path))
|
|
340
|
+
exports = json.loads((FIXTURES / "phosphor-react-exports.json").read_text())
|
|
341
|
+
exports["ssr"].remove("Acorn")
|
|
342
|
+
exports_path = directory / "exports.json"
|
|
343
|
+
exports_path.write_text(json.dumps(exports))
|
|
344
|
+
exports_result = self.run_command(*self.icon_args(directory, react_exports=exports_path))
|
|
345
|
+
self.assertIn("invalid official IconEntry schema", schema_result.stderr)
|
|
346
|
+
self.assertIn("expected 3 icons", size_result.stderr)
|
|
347
|
+
self.assertIn("import component does not match", import_result.stderr)
|
|
348
|
+
self.assertIn("version must be 2.1.1", version_result.stderr)
|
|
349
|
+
self.assertIn("alias collides", alias_result.stderr)
|
|
350
|
+
self.assertIn("React exports missing", exports_result.stderr)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
unittest.main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The catalog snapshot must not depend on the checkout's line endings.
|
|
3
|
+
|
|
4
|
+
Regression test for bd19ab9 (#462), where catalog-summary.json was regenerated
|
|
5
|
+
on a CRLF checkout. Every recorded sha256 was the CRLF hash of the source file,
|
|
6
|
+
so `verify:data` failed on every LF platform, including CI.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
REPO = next(
|
|
18
|
+
parent for parent in Path(__file__).resolve().parents
|
|
19
|
+
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
|
20
|
+
)
|
|
21
|
+
DATA = REPO / "src/ui-ux-pro-max/data"
|
|
22
|
+
SNAPSHOT_FILES = (
|
|
23
|
+
"google-fonts.csv",
|
|
24
|
+
"google-font-licenses.json",
|
|
25
|
+
"icons.csv",
|
|
26
|
+
"phosphor-icons-upstream.json",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_generator():
|
|
31
|
+
path = REPO / "scripts" / "generate-catalog-summary.py"
|
|
32
|
+
spec = importlib.util.spec_from_file_location("generate_catalog_summary", path)
|
|
33
|
+
module = importlib.util.module_from_spec(spec)
|
|
34
|
+
spec.loader.exec_module(module)
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CatalogSummaryLineEndingsTest(unittest.TestCase):
|
|
39
|
+
def test_digest_is_identical_for_lf_and_crlf(self):
|
|
40
|
+
digest = _load_generator().digest
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
42
|
+
lf = Path(tmp) / "lf.csv"
|
|
43
|
+
crlf = Path(tmp) / "crlf.csv"
|
|
44
|
+
lf.write_bytes(b"id,name\n1,alpha\n2,beta\n")
|
|
45
|
+
crlf.write_bytes(b"id,name\r\n1,alpha\r\n2,beta\r\n")
|
|
46
|
+
self.assertEqual(
|
|
47
|
+
digest(lf), digest(crlf),
|
|
48
|
+
"snapshot hashes must not change with the checkout's line endings",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def test_committed_snapshot_matches_normalized_sources(self):
|
|
52
|
+
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
|
53
|
+
for name in SNAPSHOT_FILES:
|
|
54
|
+
expected = hashlib.sha256(
|
|
55
|
+
(DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
|
56
|
+
).hexdigest()
|
|
57
|
+
self.assertEqual(
|
|
58
|
+
summary["snapshots"][name]["sha256"], expected,
|
|
59
|
+
f"{name}: committed snapshot hash does not match the LF-normalized source",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def test_crlf_checkout_produces_the_committed_hashes(self):
|
|
63
|
+
"""Simulate a Windows checkout: the recorded hashes must still validate."""
|
|
64
|
+
digest = _load_generator().digest
|
|
65
|
+
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
|
66
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
67
|
+
for name in SNAPSHOT_FILES:
|
|
68
|
+
crlf_copy = Path(tmp) / name
|
|
69
|
+
raw = (DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
|
70
|
+
crlf_copy.write_bytes(raw.replace(b"\n", b"\r\n"))
|
|
71
|
+
self.assertEqual(
|
|
72
|
+
digest(crlf_copy), summary["snapshots"][name]["sha256"],
|
|
73
|
+
f"{name}: a CRLF checkout would record a different hash",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
unittest.main()
|