docs: establish phase 0 evidence workflow
This commit is contained in:
commit
805691a71b
15 changed files with 573 additions and 0 deletions
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
* text=auto
|
||||
*.md text eol=lf
|
||||
*.py text eol=lf
|
||||
*.json text eol=lf
|
||||
*.jsonl text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
.uv-cache/
|
||||
|
||||
# Sensitive/raw acquisition data belongs in ThePirateRay-PrivateStash.
|
||||
/raw/
|
||||
/private/
|
||||
*.lbprefs
|
||||
*.lbzip
|
||||
prefs.ini
|
||||
prefs.old
|
||||
|
||||
# Local reports are reproducible and must not be committed accidentally.
|
||||
/reports/
|
||||
36
README.md
Normal file
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# The Pirate Ray
|
||||
|
||||
The Pirate Ray is Makerspace Næstved's clean-room project for learning how to
|
||||
interoperate with its laser equipment and, over time, building a safe desktop
|
||||
control application.
|
||||
|
||||
Phase 0 focuses on the **JPT 100 W E2/M7 MOPA** fiber laser with a JCZ/EZCAD2
|
||||
controller. It is deliberately limited to documentation and offline analysis:
|
||||
no USB capture, framing, machine commands, or laser output.
|
||||
|
||||
## Repository boundary
|
||||
|
||||
This public repository contains source code, plans, decisions, tests, tools,
|
||||
and sanitized research. Raw machine data, serial numbers, LightBurn exports,
|
||||
and third-party material with unclear redistribution rights live in the
|
||||
separate private `ThePirateRay-PrivateStash` repository.
|
||||
|
||||
Start with:
|
||||
|
||||
- [Project charter](docs/project-charter.md)
|
||||
- [Data handling policy](docs/data-handling.md)
|
||||
- [LightBurn collection checklist](docs/collection/lightburn.md)
|
||||
- [Sanitized machine profile](docs/research/machine-profile.yaml)
|
||||
- [Repository boundary decision](docs/adr/0001-public-private-repository-boundary.md)
|
||||
|
||||
## Validation
|
||||
|
||||
The repository uses only Python's standard library for Phase 0 tooling:
|
||||
|
||||
```powershell
|
||||
python -m unittest discover -s tests -v
|
||||
python scripts/check_public_repo.py .
|
||||
```
|
||||
|
||||
No project license has been selected yet. Until that decision is recorded,
|
||||
the contents are not offered under an open-source license.
|
||||
30
docs/adr/0001-public-private-repository-boundary.md
Normal file
30
docs/adr/0001-public-private-repository-boundary.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# ADR 0001: Public/private repository boundary
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-10-17
|
||||
|
||||
## Context
|
||||
|
||||
The project needs unusually complete evidence while some inputs contain
|
||||
identifiers, configuration secrets, or third-party files whose redistribution
|
||||
rights are unclear.
|
||||
|
||||
## Decision
|
||||
|
||||
Use exactly two authoritative repositories. `ThePirateRay` is public and holds
|
||||
all normal development work. `ThePirateRay-PrivateStash` holds sensitive raw
|
||||
data and redistribution-restricted third-party material. Google Drive is used
|
||||
only as a read-only acquisition source.
|
||||
|
||||
Public documents cite private evidence by content-derived artifact ID and
|
||||
SHA-256 digest. Git LFS is mandatory for binary files in PrivateStash. If the
|
||||
Git server cannot store and retrieve LFS objects, raw binary import stops rather
|
||||
than falling back to ordinary Git blobs.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A public clone is useful without access to private evidence.
|
||||
- Authorized researchers can reproduce claims from exact private bytes.
|
||||
- Cross-repository commits must be ordered: private evidence first, sanitized
|
||||
public analysis second.
|
||||
- The two repositories must be validated separately.
|
||||
39
docs/collection/lightburn.md
Normal file
39
docs/collection/lightburn.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# LightBurn collection checklist
|
||||
|
||||
Collect directly into a dated directory under
|
||||
`ThePirateRay-PrivateStash/raw/lightburn/`. Do not use Google Drive as an
|
||||
intermediate workspace.
|
||||
|
||||
## Before collection
|
||||
|
||||
- Confirm no marking job is active.
|
||||
- Do not enable debug logging or generate Support Data.
|
||||
- Record which lens is installed and whether LightBurn currently connects.
|
||||
|
||||
## Required evidence
|
||||
|
||||
| Directory | Evidence |
|
||||
| --- | --- |
|
||||
| `00-version/` | Screenshot of Help → About with exact version/build |
|
||||
| `01-exports/` | Full `.lbzip` bundle and exported `.lbprefs` |
|
||||
| `02-prefs/` | ZIP of `%LOCALAPPDATA%\LightBurn`, copied only after LightBurn is fully closed |
|
||||
| `03-device-settings/` | Device list and every Device Settings tab for the JPT device |
|
||||
| `04-projects/` | Two or three known-working `.lbrn2` files, including vector and MOPA/hatch examples when available |
|
||||
| `05-hardware/` | Machine, source, controller, and active-lens labels; never open an energized enclosure |
|
||||
| `06-context/` | Mounted lens, connection status, and normal startup/interlock notes |
|
||||
|
||||
## Export procedure
|
||||
|
||||
1. Use File → Bundles → Export Bundle and include every available component.
|
||||
2. Use File → Preferences → Export Prefs.
|
||||
3. Capture the UI screenshots and save existing example projects.
|
||||
4. Exit LightBurn completely.
|
||||
5. Copy `%LOCALAPPDATA%\LightBurn` into an archive without editing its contents.
|
||||
6. Run the private manifest tool and review the staged files before committing.
|
||||
|
||||
LightBurn documents bundles as containing settings, device profiles, hotkeys,
|
||||
presets, and libraries. Since LightBurn 2.0, device export is also represented
|
||||
as a bundle: <https://docs.lightburnsoftware.com/latest/Guides/Migration/>.
|
||||
|
||||
LightBurn explicitly warns not to publish generated Support Data:
|
||||
<https://docs.lightburnsoftware.com/latest/Reference/Hidden/GenerateSupportData/>.
|
||||
33
docs/data-handling.md
Normal file
33
docs/data-handling.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Data handling policy
|
||||
|
||||
## Authoritative repositories
|
||||
|
||||
| Classification | Repository | Examples |
|
||||
| --- | --- | --- |
|
||||
| `public` | `ThePirateRay` | Code, tests, plans, schemas, sanitized analysis |
|
||||
| `private-sensitive` | `ThePirateRay-PrivateStash` | Serial numbers, local usernames, license-related data, private source locators |
|
||||
| `private-third-party` | `ThePirateRay-PrivateStash` | Vendor binaries, manuals, raw LightBurn/EZCAD files with unclear redistribution rights |
|
||||
|
||||
Google Drive is a read-only acquisition source, not a project workspace. No
|
||||
project artifact is created, edited, renamed, or reorganized there.
|
||||
|
||||
## Rules
|
||||
|
||||
- Preserve raw bytes. Analysis always produces a new file.
|
||||
- Record SHA-256, byte size, media type, acquisition time, origin label, and
|
||||
classification for every acquired file.
|
||||
- Keep exact Drive IDs, URLs, machine serials, usernames, and local paths only
|
||||
in the private manifest.
|
||||
- Refer to private evidence publicly using `artifact_id` and `sha256` only.
|
||||
- Never commit LightBurn Support Data to the public repository.
|
||||
- Do not execute software acquired from the installer stick during analysis.
|
||||
- Run the public repository guard before each public push.
|
||||
|
||||
## Redaction flow
|
||||
|
||||
1. Acquire the original into the private repository.
|
||||
2. Generate and verify the private manifest.
|
||||
3. Analyze the original without modifying it.
|
||||
4. Create a sanitized derivative in the public repository.
|
||||
5. Run automated guards and manually review the staged diff.
|
||||
6. Commit and push the private evidence before the public claim that cites it.
|
||||
29
docs/project-charter.md
Normal file
29
docs/project-charter.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Project charter
|
||||
|
||||
## Goal
|
||||
|
||||
Build an evidence-backed knowledge base and, incrementally, a desktop
|
||||
application capable of safely controlling Makerspace Næstved's lasers. The
|
||||
first hardware target is the JPT 100 W E2/M7 MOPA fiber laser.
|
||||
|
||||
## Phase 0 success criteria
|
||||
|
||||
1. Public and private repositories are independently cloneable and validated.
|
||||
2. Raw evidence is immutable, hashed, and kept in the private repository.
|
||||
3. Every public technical claim is traceable to a private artifact ID and
|
||||
SHA-256 digest without exposing private locators.
|
||||
4. Collection and analysis can be repeated using committed tools and tests.
|
||||
5. No command is sent to laser hardware during Phase 0.
|
||||
|
||||
## Clean-room boundary
|
||||
|
||||
The project studies observable configuration formats and hardware behavior for
|
||||
interoperability. It does not bypass LightBurn licensing, copy proprietary
|
||||
source code or UI assets, or redistribute third-party material without a clear
|
||||
right to do so.
|
||||
|
||||
## Working practice
|
||||
|
||||
Plans, decisions, scripts, tests, reports, and collected evidence are committed
|
||||
and pushed in small, meaningful units. Commit messages explain intent. Force
|
||||
pushes and history rewriting are not part of the workflow.
|
||||
23
docs/research/installer-inventory.md
Normal file
23
docs/research/installer-inventory.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Original installer-stick inventory
|
||||
|
||||
Status: preliminary metadata inventory; content hashes are pending private
|
||||
acquisition.
|
||||
|
||||
The read-only source contains an EZCAD2 2.14.16 installation configured for a
|
||||
JPT 100 W E2/M7 source. Separate 110 mm and 300 mm configurations are present.
|
||||
|
||||
Observed evidence classes include:
|
||||
|
||||
- per-lens `markcfg7` files;
|
||||
- `CoeFile.cfg`, `EZCAD.CFG`, parameter libraries, and USB logs;
|
||||
- screenshots of field, laser-control, port, red-pointer, workspace, and rotary
|
||||
settings;
|
||||
- EZCAD executables, plugins, DLLs, driver installer material, and a manual;
|
||||
- LightBurn `.clb` material libraries for multiple lens sizes.
|
||||
|
||||
All originals and exact source locators belong in PrivateStash. This public
|
||||
inventory intentionally omits serial numbers, Drive identifiers, executable
|
||||
copies, and manual copies.
|
||||
|
||||
The detailed comparison report will be added only after the private snapshot is
|
||||
hashed and verified.
|
||||
40
docs/research/machine-profile.yaml
Normal file
40
docs/research/machine-profile.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
schema_version: 1
|
||||
profile_id: makerspace-naestved-jpt-e2-mopa-100w
|
||||
status: collection-in-progress
|
||||
machine:
|
||||
source_family: JPT
|
||||
source_model: E2/M7
|
||||
nominal_power_w: 100
|
||||
technology: mopa-fiber
|
||||
controller:
|
||||
family: JCZ
|
||||
stock_software: EZCAD2
|
||||
stock_software_version_observed: 2.14.16
|
||||
optics:
|
||||
active_field_mm:
|
||||
width: 110
|
||||
height: 110
|
||||
status: assumed-pending-physical-verification
|
||||
other_configurations_observed_mm:
|
||||
- width: 300
|
||||
height: 300
|
||||
lightburn:
|
||||
version: null
|
||||
build: null
|
||||
status: pending-collection
|
||||
usb:
|
||||
vid: null
|
||||
pid: null
|
||||
driver: null
|
||||
status: pending-collection
|
||||
safety:
|
||||
phase_0_hardware_commands_allowed: false
|
||||
phase_0_laser_output_allowed: false
|
||||
evidence:
|
||||
- artifact_id: pending-installer-110-markcfg7
|
||||
sha256: null
|
||||
- artifact_id: pending-installer-300-markcfg7
|
||||
sha256: null
|
||||
notes:
|
||||
- No complete serial numbers or private source locators are stored here.
|
||||
- Incorrect galvo or source configuration can damage hardware; values are not inferred by trial and error.
|
||||
59
schemas/artifact-manifest.schema.json
Normal file
59
schemas/artifact-manifest.schema.json
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.p20.dev/christian/ThePirateRay/schemas/artifact-manifest.schema.json",
|
||||
"title": "The Pirate Ray artifact manifest entry",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"artifact_id",
|
||||
"sha256",
|
||||
"size_bytes",
|
||||
"observed_at_utc",
|
||||
"media_type",
|
||||
"origin",
|
||||
"classification",
|
||||
"analysis_refs"
|
||||
],
|
||||
"properties": {
|
||||
"artifact_id": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256-[0-9a-f]{20}$"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"observed_at_utc": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"media_type": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"origin": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"classification": {
|
||||
"enum": ["public", "private-sensitive", "private-third-party"]
|
||||
},
|
||||
"analysis_refs": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"relative_path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"source_locator": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
1
scripts/__init__.py
Normal file
1
scripts/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Phase 0 acquisition and validation tools."""
|
||||
99
scripts/build_manifest.py
Normal file
99
scripts/build_manifest.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build a deterministic JSON Lines manifest for an immutable artifact tree."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import mimetypes
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
CLASSIFICATIONS = ("public", "private-sensitive", "private-third-party")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def manifest_entry(
|
||||
path: Path,
|
||||
root: Path,
|
||||
classification: str,
|
||||
origin: str,
|
||||
observed_at: str,
|
||||
include_private_locators: bool,
|
||||
) -> dict[str, object]:
|
||||
digest = sha256_file(path)
|
||||
entry: dict[str, object] = {
|
||||
"artifact_id": f"sha256-{digest[:20]}",
|
||||
"sha256": digest,
|
||||
"size_bytes": path.stat().st_size,
|
||||
"observed_at_utc": observed_at,
|
||||
"media_type": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
|
||||
"origin": origin,
|
||||
"classification": classification,
|
||||
"analysis_refs": [],
|
||||
}
|
||||
if include_private_locators:
|
||||
entry["relative_path"] = path.relative_to(root).as_posix()
|
||||
return entry
|
||||
|
||||
|
||||
def build_manifest(
|
||||
root: Path,
|
||||
classification: str,
|
||||
origin: str,
|
||||
observed_at: str,
|
||||
include_private_locators: bool = True,
|
||||
) -> list[dict[str, object]]:
|
||||
if classification not in CLASSIFICATIONS:
|
||||
raise ValueError(f"unsupported classification: {classification}")
|
||||
if not root.is_dir():
|
||||
raise ValueError(f"artifact root is not a directory: {root}")
|
||||
files = sorted((path for path in root.rglob("*") if path.is_file()), key=lambda p: p.relative_to(root).as_posix())
|
||||
return [
|
||||
manifest_entry(path, root, classification, origin, observed_at, include_private_locators)
|
||||
for path in files
|
||||
]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("root", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--classification", choices=CLASSIFICATIONS, required=True)
|
||||
parser.add_argument("--origin", required=True)
|
||||
parser.add_argument(
|
||||
"--observed-at",
|
||||
default=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
help="ISO-8601 acquisition timestamp; pass an explicit value for reproducible output",
|
||||
)
|
||||
parser.add_argument("--public", action="store_true", help="omit private relative paths")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
entries = build_manifest(
|
||||
args.root.resolve(),
|
||||
args.classification,
|
||||
args.origin,
|
||||
args.observed_at,
|
||||
include_private_locators=not args.public,
|
||||
)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as stream:
|
||||
for entry in entries:
|
||||
stream.write(json.dumps(entry, ensure_ascii=False, sort_keys=True) + "\n")
|
||||
print(f"wrote {len(entries)} entries to {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
68
scripts/check_public_repo.py
Normal file
68
scripts/check_public_repo.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Fail when public tracked content resembles private or vendor raw data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
DISALLOWED_SUFFIXES = {
|
||||
".7z", ".clb", ".dll", ".ezd", ".lbprefs", ".lbzip", ".plg", ".rar", ".zip"
|
||||
}
|
||||
SENSITIVE_TEXT = {
|
||||
"Google Drive URL": re.compile(
|
||||
r"https://drive" r"\.google\.com/(?:drive/folders|file/d)/[A-Za-z0-9_-]+"
|
||||
),
|
||||
"Windows user path": re.compile(r"C:\\Users\\[^\\\s]+", re.IGNORECASE),
|
||||
"License-key indicator": re.compile(
|
||||
r"light" r"burn.{0,30}license.{0,15}key", re.IGNORECASE
|
||||
),
|
||||
}
|
||||
TEXT_SUFFIXES = {"", ".json", ".jsonl", ".md", ".py", ".toml", ".txt", ".yaml", ".yml"}
|
||||
|
||||
|
||||
def tracked_files(root: Path) -> list[Path]:
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "-z"], cwd=root, check=True, capture_output=True
|
||||
)
|
||||
return [root / item.decode("utf-8") for item in result.stdout.split(b"\0") if item]
|
||||
|
||||
|
||||
def check(root: Path) -> list[str]:
|
||||
failures: list[str] = []
|
||||
for path in tracked_files(root):
|
||||
relative = path.relative_to(root).as_posix()
|
||||
suffix = path.suffix.lower()
|
||||
if suffix in DISALLOWED_SUFFIXES:
|
||||
failures.append(f"disallowed public artifact: {relative}")
|
||||
continue
|
||||
if suffix not in TEXT_SUFFIXES:
|
||||
continue
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
failures.append(f"unexpected binary content in public text file: {relative}")
|
||||
continue
|
||||
for label, pattern in SENSITIVE_TEXT.items():
|
||||
if pattern.search(content):
|
||||
failures.append(f"{label} found in {relative}")
|
||||
return failures
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("root", type=Path, nargs="?", default=Path.cwd())
|
||||
args = parser.parse_args()
|
||||
failures = check(args.root.resolve())
|
||||
if failures:
|
||||
for failure in failures:
|
||||
print(f"ERROR: {failure}")
|
||||
return 1
|
||||
print("public repository guard passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
52
tests/test_build_manifest.py
Normal file
52
tests/test_build_manifest.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import hashlib
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.build_manifest import build_manifest
|
||||
|
||||
|
||||
class BuildManifestTests(unittest.TestCase):
|
||||
def test_manifest_is_sorted_hashed_and_private_by_default(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / "nested").mkdir()
|
||||
(root / "z.txt").write_text("z", encoding="utf-8")
|
||||
(root / "nested" / "a.bin").write_bytes(b"a")
|
||||
|
||||
entries = build_manifest(
|
||||
root,
|
||||
"private-third-party",
|
||||
"test-source",
|
||||
"2026-10-17T00:00:00Z",
|
||||
)
|
||||
|
||||
self.assertEqual([entry["relative_path"] for entry in entries], ["nested/a.bin", "z.txt"])
|
||||
expected = hashlib.sha256(b"a").hexdigest()
|
||||
self.assertEqual(entries[0]["sha256"], expected)
|
||||
self.assertEqual(entries[0]["artifact_id"], f"sha256-{expected[:20]}")
|
||||
|
||||
def test_public_manifest_omits_paths(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / "item.txt").write_text("safe", encoding="utf-8")
|
||||
entries = build_manifest(
|
||||
root,
|
||||
"public",
|
||||
"test-source",
|
||||
"2026-10-17T00:00:00Z",
|
||||
include_private_locators=False,
|
||||
)
|
||||
self.assertNotIn("relative_path", entries[0])
|
||||
self.assertNotIn("source_locator", entries[0])
|
||||
|
||||
def test_invalid_classification_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
with self.assertRaises(ValueError):
|
||||
build_manifest(
|
||||
Path(temporary), "secret", "test-source", "2026-10-17T00:00:00Z"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
41
tests/test_public_repo_guard.py
Normal file
41
tests/test_public_repo_guard.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.check_public_repo import check
|
||||
|
||||
|
||||
class PublicRepoGuardTests(unittest.TestCase):
|
||||
def test_disallowed_binary_suffix_is_reported(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
candidate = root / "vendor.dll"
|
||||
candidate.write_bytes(b"MZ")
|
||||
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
||||
self.assertIn("disallowed public artifact: vendor.dll", check(root))
|
||||
|
||||
def test_private_drive_url_is_reported(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
candidate = root / "notes.md"
|
||||
candidate.write_text(
|
||||
"https://drive" + ".google.com/file/d/private_identifier", encoding="utf-8"
|
||||
)
|
||||
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
||||
failures = check(root)
|
||||
self.assertEqual(len(failures), 1)
|
||||
self.assertIn("Google Drive URL", failures[0])
|
||||
|
||||
def test_safe_text_passes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
candidate = root / "notes.md"
|
||||
candidate.write_text("artifact sha256-01234567890123456789", encoding="utf-8")
|
||||
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
||||
self.assertEqual(check(root), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Reference in a new issue